QOI to PNM API
POST a QOI to /v2/convert/qoi/to/pnm and get raw 8-bit RGB in a binary PPM file (P6). Transparent and soft-edged pixels are blended onto AlphaColor first; ColorSpace=gray or monochrome gives a one-channel P5 file instead.
- Endpoint
- POST /v2/
convert/ qoi/ to/ pnm - Accepts
- qoi
- Sample timing
- 1 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/pnm \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@trail-map-card.qoi \
-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/pnm",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("trail-map-card.qoi", "rb"))],
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);
const url = "https://api.pdfconversionapi.com/v2/convert/qoi/to/pnm";
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.001,
"Files": [
{
"FileName": "trail-map-card.pnm",
"FileExt": "pnm",
"FileSize": 360015,
"FileData": "UDYKNDAwIDMwMCAyNTUK////…"
}
]
}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.pnm (352 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 |
|---|---|
AlphaColorstring, default white | What the card's clear corners and soft shadow are blended onto. White by default, which is what the sample shows.Values: #RRGGBB, R,G,B or white, black, red, green, blue, gray |
BackgroundColorstring, default white | Used when no AlphaColor is sent.Values: Same as AlphaColor |
ImageWidthinteger | Rescales first; the file is then 3 bytes per pixel plus the header (128 × 96: 36,878 bytes).Values: 1–20000 |
ImageHeightinteger | Rescales by height, or fits a box together with the width.Values: 1–20000 |
ScaleProportionsboolean, default true | false stretches to the exact box. |
ScaleIfLargerboolean, default false | true prevents enlarging. |
ScaleImageboolean, default true | false crops from the top-left. |
ColorSpaceenum, default default | gray or monochrome writes a one-channel P5 graymap of 120,015 bytes, a third of the color file; monochrome holds only the levels 0 and 255.Values: default, rgb, gray, monochrome; cmyk returns 400 |
FileNamestring | Name of the .pnm file. |
StoreFileboolean, default false | Store the result for 3 hours behind a URL. |
What carries over, and what doesn't
Kept
- Exact colors for every opaque pixel and an exact blend for the rest: all 120,000 pixels match the QOI flattened onto white
- Dimensions and row order: 400 × 300, top row first
- 8 bits per channel (MAXVAL 255)
Lost or not yet supported
- Alpha. A P6 file has three channels and no alpha, so the clear corners and the shadow are blended onto the matte for good.
- Size: 360,015 bytes (a 15-byte header and 360,000 bytes of RGB), more than five times the QOI.
- The QOI colorspace byte. Netpbm has no field for it, and a copy of the card marked linear gave a byte-identical file.
Limits
- One QOI per request.
- 100,000,000 pixels per image (413 above).
- Requests up to 10 MB on the Free plan or 100 MB on paid plans; 120 seconds per conversion.
Where teams use it
Pixels for numeric work
Computer-vision or teaching code that wants a flat array of RGB bytes can load the P6 output with a few lines and no image library.
Netpbm pipelines
Shell pipelines built on Netpbm tools read P6 and P5 directly, so a QOI can enter them through one HTTP call.
Questions
What does the header look like?
Two text lines: P6, then 400 300 255 (width, height and the largest sample value), 15 bytes in all. The 360,000 bytes after it are RGB triples, row by row from the top. With ColorSpace=gray the first line is P5 and each pixel takes one byte.
My tool expects a .ppm. Can I use this file?
Yes. The color output is a binary PPM with MAXVAL 255, the layout .ppm readers expect, so only the extension differs; rename the file if your tool goes by the name. A gray or black-and-white request gives a P5 file, which tools read as .pgm.
Why are the card's corners white?
PNM output has no alpha, so transparent pixels take the AlphaColor, white by default. Send another color, such as AlphaColor=black, to change it.
Convert your first QOI file in a minute.