XCF to SVG API
POST an .xcf to /v2/convert/xcf/to/svg and get an SVG made of filled paths traced from the merged layers. It suits flat-color artwork; gradients turn into flat fills or bands, and small text breaks up.
- Endpoint
- POST /v2/
convert/ xcf/ to/ svg - Accepts
- xcf
- Sample timing
- 66 ms median of 5 runsHow we measure
- Request size
- Up to 10 MB on the Free plan or 100 MB on paid plans, all files together
Request and response
Every sample below ran against the API before this page was published. Set PDFCONVERSIONAPI_KEY to your key and run it as is.
curl -sS -X POST \
https://api.pdfconversionapi.com/v2/convert/xcf/to/svg \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@gull-rock-lighthouse.xcf \
--form-string Preset=poster \
-o response.json
# FileData is base64. Save the first file in converted/,
# or print the JSON error.
if jq -e '.Files' response.json > /dev/null; then
mkdir -p converted
name=$(jq -r '.Files[0].FileName' response.json)
jq -r '.Files[0].FileData' response.json \
| base64 --decode > "converted/$name"
else
cat response.json
fiimport base64
import os
import requests
key = os.environ["PDFCONVERSIONAPI_KEY"]
response = requests.post(
"https://api.pdfconversionapi.com/v2/convert/xcf/to/svg",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("gull-rock-lighthouse.xcf", "rb"))],
data={
"Preset": "poster",
},
timeout=180,
)
result = response.json()
if not response.ok:
raise SystemExit(f"{result['Code']}: {result['Message']}")
os.makedirs("converted", exist_ok=True)
for file in result["Files"]:
path = os.path.join("converted", file["FileName"])
with open(path, "wb") as out:
out.write(base64.b64decode(file["FileData"]))
print(path, file["FileSize"], "bytes")
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const form = new FormData();
const file = "gull-rock-lighthouse.xcf";
form.append("File", new Blob([await readFile(file)]), file);
form.append("Preset", "poster");
const url = "https://api.pdfconversionapi.com/v2/convert/xcf/to/svg";
const key = process.env.PDFCONVERSIONAPI_KEY;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
});
const result = await response.json();
if (!response.ok) {
throw new Error(`${result.Code}: ${result.Message}`);
}
await mkdir("converted", { recursive: true });
for (const file of result.Files) {
const path = join("converted", file.FileName);
await writeFile(path, Buffer.from(file.FileData, "base64"));
console.log(path, file.FileSize, "bytes");
}
{
"ConversionCost": 1,
"ConversionTime": 0.066,
"Files": [
{
"FileName": "gull-rock-lighthouse.svg",
"FileExt": "svg",
"FileSize": 43410,
"FileData": "PD94bWwgdmVyc2lvbj0iMS4w…"
}
]
}Response from our test run on 2026-11-03. ConversionTime shows the median of 5 runs. FileData is shortened here; the real response carries the whole file as base64.
The sample, converted
The same file the code sends. Download the input and the output and compare them yourself.

- Input
- gull-rock-lighthouse.xcf (203 KB)
- Output
- gull-rock-lighthouse.svg (42 KB)
- Source
- A 400 x 300 harbor poster we drew and wrote as a layered XCF file ourselves: 11 layers, including a two-layer group, a layer mask, Multiply and Screen modes and a hidden guide layer. (CC0-1.0). How it was made
- Made by
- 2026-11-04, API build
9eeeb0640aba
Parameters
Send these as form fields next to the file. Names are case-insensitive. If you set one of the documented options below that we don't support yet, the request fails with a 400 that names it instead of returning a file made without it.
| Parameter | What it does |
|---|---|
Presetenum, default none | Starting settings. The sample sends poster, which keeps 8 bits of color: 116 paths and 43,410 bytes, with the sun glow as rings of color. photo gave 34 paths, bw 18 black paths.Values: none, bw, poster, photo |
ColorModeenum, default color | bw traces in black and white only, the same as the bw preset for this poster (8,441 bytes).Values: color, bw |
ColorPrecisioninteger, default 6 | Bits kept per color channel before tracing. 4 merged more shades: 59 paths and 14,939 bytes.Values: 1–8 |
FilterSpeckleinteger, default 4 | Drops patches smaller than this many pixels across. 16 removed the waves and lettering details, leaving 15 paths.Values: 0–128 |
GradientStepinteger, default 16 | Color difference between stacked layers of the trace; 32 gave 58 paths instead of 66.Values: 0–128 |
Layeringenum, default stacked | cutout makes shapes that do not overlap, instead of stacking them: 36 paths for the poster.Values: stacked, cutout |
CurveModeenum, default spline | polygon uses straight segments (7,548 bytes); pixel follows pixel edges exactly (15,769 bytes).Values: spline, polygon, pixel |
CornerThresholdinteger, default 60 | Smallest angle in degrees that counts as a corner; gentler turns are smoothed into curves. 90 gave a 21,190-byte file.Values: 0–180 |
SegmentLengthnumber, default 4 | Curves are subdivided until each segment is shorter than this many pixels. 8 gave 16,418 bytes.Values: 3.5–10 |
SpliceThresholdinteger, default 45 | Smallest change of direction, in degrees, at which a curve is split in two.Values: 0–180 |
ImageWidthinteger | Scales the merged image before tracing; 200 px gave 49 paths and 14,767 bytes. Also needed for canvases over 12,000,000 pixels.Values: 1–20000 |
FileNamestring | Name of the .svg file. |
StoreFileboolean, default false | true returns a 3-hour link instead of the SVG in the JSON. |
What carries over, and what doesn't
Kept
- Shapes and flat colors of the merged layers: the lighthouse stripes, the headland and both boats come out as clean filled paths
- Large lettering: the 30 px title stays readable
- The canvas size, as
width="400"andheight="300"on the root element
Lost or not yet supported
- Gradients. The poster's sky became one flat fill with the default settings and with
poster, and the faint light beam disappeared in both.posterkeeps the sun glow over the sea as rings of color. - Small text: the 15 px subtitle broke into fragments with every setting we tried.
- Exact colors. Rendered back to pixels, the default trace differed from the PNG by 11.3 levels per channel on average, and the
postersample by 9.5. - A
viewBox. The SVG sets onlywidthandheight, so add aviewBoxbefore scaling it with CSS.
Limits
- One XCF per request. The canvas may hold up to 67,108,864 pixels (8,192 × 8,192); 8,193 × 8,192 gets a 422. A 16,384 × 4,096 canvas is read, but needs
ImageWidthto fit the tracing limit below. - Tracing is limited to 12,000,000 pixels; larger images get 413 until you scale them down with
ImageWidth. - Requests up to 10 MB on the Free plan or 100 MB on paid plans, all files and fields together.
- 120 seconds per conversion, then 504.
Where teams use it
Vector versions of flat artwork
Logos, badges and signs painted in GIMP with a few flat colors can be traced to SVG for web pages or for print at any size, without redrawing them in a vector editor.
Paths for cutting machines
Vinyl and laser cutters take vector paths. With ColorMode=bw, a black-and-white GIMP drawing comes back as black shapes ready to be cut.
Questions
Are GIMP's paths exported as SVG paths?
No. Paths stored in the XCF do not reach the output. The SVG is traced from the merged pixels, so you get outlines of color areas, not your original paths.
Which settings work best for a logo?
Start with no preset for flat logos, or poster if the artwork has soft shading. Raise FilterSpeckle to drop noise and lower ColorPrecision to merge similar shades. Each setting change on the poster shifted the result between 15 and 116 paths.
Why is my SVG so large?
Every area of distinct color becomes a path. Photos and gradients produce many of them. Try CurveMode=polygon, which made the poster's default trace 7,548 bytes instead of 19,217, or scale down first with ImageWidth.
Convert your first XCF file in a minute.