QOI to SVG API
POST a QOI to /v2/convert/qoi/to/svg and get an SVG traced from the image's color regions. The sample uses Preset=poster, which keeps more shades: 104 paths in 99 colors.
- Endpoint
- POST /v2/
convert/ qoi/ to/ svg - Accepts
- qoi
- Sample timing
- 55 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/qoi/to/svg \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@trail-map-card.qoi \
--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/qoi/to/svg",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("trail-map-card.qoi", "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 = "trail-map-card.qoi";
form.append("File", new Blob([await readFile(file)]), file);
form.append("Preset", "poster");
const url = "https://api.pdfconversionapi.com/v2/convert/qoi/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.055,
"Files": [
{
"FileName": "trail-map-card.svg",
"FileExt": "svg",
"FileSize": 73359,
"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
- trail-map-card.qoi (66 KB)
- Output
- trail-map-card.svg (72 KB)
- Source
- A 400 x 300 trail map card with a sunrise sky, rounded transparent corners and a soft drop shadow, drawn by us and saved as a 4-channel QOI. (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 | poster raises color precision to 8 bits (73,359 bytes, 104 paths for the card); photo merges more (22 paths); bw traces a single-color silhouette (1 path); none uses the defaults (60 paths, 41,340 bytes).Values: none, bw, poster, photo |
ColorPrecisioninteger, default 6 | Bits per channel used to decide which pixels share a color. 8 matches Preset=poster here; 3 cut the card to 32 paths.Values: 1–8 |
GradientStepinteger, default 16 | How different two stacked layers must be. Raising it to 64 gave fewer layers (48 paths); 0 gave 62.Values: 0–128 |
FilterSpeckleinteger, default 4 | Merges away patches of at most this value squared, in pixels. 0 keeps every speck and gave 3,292 paths, 265,342 bytes; 20 left 13 paths.Values: 0–128 |
ColorModeenum, default color | bw traces in black and white only.Values: color, bw |
Layeringenum, default stacked | cutout writes shapes that don't overlap (25 paths for the card).Values: stacked, cutout |
CurveModeenum, default spline | polygon uses straight segments, which made the card's SVG 8,524 bytes; pixel follows pixel edges (35,872 bytes).Values: spline, polygon, pixel |
CornerThresholdinteger, default 60 | A turn of at least this angle is kept as a corner; gentler turns are smoothed into the curve.Values: 0–180 (degrees) |
SegmentLengthnumber, default 4 | Curve segment length; 10 gave a lighter 31,275-byte file with looser outlines.Values: 3.5–10 |
SpliceThresholdinteger, default 45 | Angle at which a curve is split into separate splines.Values: 0–180 (degrees) |
ImageWidthinteger | Rescale before tracing. Needed for images over 12 million pixels; at 128 px the card traced to 33 paths.Values: 1–20000 |
ImageHeightinteger | Height to rescale to before tracing.Values: 1–20000 |
FileNamestring | Name of the SVG without extension. |
StoreFileboolean, default false | Keep the SVG for 3 hours and return a URL. |
What carries over, and what doesn't
Kept
- Flat shapes and their colors: mountains, lake, trees and the red pin trace as clean filled paths
- Transparent surroundings: the clear pixels outside the card are left out of the SVG
- The image size as the SVG's width and height, 400 by 300
Lost or not yet supported
- Smooth gradients. With default options the sunrise sky becomes one flat color and the sun's glow disappears;
posterkeeps it as visible bands. - Partial transparency. The soft shadow is traced as opaque dark paths, so the card gets a hard dark rim.
- Thin detail while
FilterSpeckleis above 0. At the defaults and in the sample, five of the lake's six 2 px reflection lines and some trail dashes are merged away, although each line covers far more than the default 16 px speckle area;FilterSpeckle=0keeps them all. - A viewBox. Add
viewBox="0 0 400 300"to the root element if the SVG must scale with CSS.
Limits
- One QOI per request.
- 12,000,000 pixels for tracing: a 4000 × 3001 QOI returned 413 until
ImageWidth=2000was added. - 100,000,000 pixels per decoded image; requests up to 10 MB on the Free plan or 100 MB on paid plans.
- 120 seconds per conversion.
Where teams use it
Flat illustrations to editable vectors
Flat-shaded art saved as QOI, such as icons, maps or diagrams, traces into paths you can recolor and edit in a vector editor.
Poster-style versions of screenshots
Preset=poster turns a game or app capture into a banded, posterized vector image that stays sharp at any print size.
Questions
Why does the sky look flat in the SVG?
Tracing groups similar pixels into regions of one color. A smooth gradient has no edges, so with the defaults the whole sky merges into one region. Use Preset=poster or a higher ColorPrecision to keep more steps, at the cost of more paths.
How big will the SVG be compared with the QOI?
It depends on the settings, not on the QOI's size. The 67,304-byte card traced to 8,524 bytes with CurveMode=polygon, 41,340 with the defaults and 265,342 with FilterSpeckle=0.
Is there a size limit for tracing?
Yes, 12,000,000 pixels after any resizing. Larger images get a 413 that says to set ImageWidth; sending one in the same request is enough.
Convert your first QOI file in a minute.