XCF to PNM API
POST an .xcf to /v2/convert/xcf/to/pnm and get the merged image as a binary PPM (P6) of raw 8-bit RGB. With ColorSpace=gray or monochrome you get a one-channel PGM (P5) instead.
- Endpoint
- POST /v2/
convert/ xcf/ to/ pnm - Accepts
- xcf
- Sample timing
- 46 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/pnm \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@gull-rock-lighthouse.xcf \
-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/pnm",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("gull-rock-lighthouse.xcf", "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 = "gull-rock-lighthouse.xcf";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/xcf/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.046,
"Files": [
{
"FileName": "gull-rock-lighthouse.pnm",
"FileExt": "pnm",
"FileSize": 360015,
"FileData": "UDYKNDAwIDMwMCAyNTUK9sSW…"
}
]
}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.pnm (352 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 |
|---|---|
ImageWidthinteger | Width in pixels; the file size follows exactly. 200 px gave 200 × 150 and 90,015 bytes.Values: 1–20000 |
ImageHeightinteger | Height in pixels, or with ImageWidth a box that keeps the proportions.Values: 1–20000 |
ScaleProportionsboolean, default true | false resizes to both values exactly, for models that need a fixed input shape. |
ScaleIfLargerboolean, default false | true only shrinks. |
ScaleImageboolean, default true | false crops from the top-left corner instead. |
ColorSpaceenum, default default | gray and monochrome write a one-channel PGM (P5): 120,015 bytes for the poster, a third of the RGB file. monochrome holds only the values 0 and 255.Values: default, rgb, gray, monochrome; cmyk returns 400 |
AlphaColorstring, default white | PNM has no alpha, so transparent parts of the canvas are blended onto this color.Values: A color name, #RRGGBB or R,G,B |
BackgroundColorstring, default white | Used when AlphaColor is absent.Values: Same as AlphaColor |
FileNamestring | Name of the .pnm file without extension. |
StoreFileboolean, default false | true returns a link that works for 3 hours. |
What carries over, and what doesn't
Kept
- The merged pixels exactly: the RGB values match the PNG of the same XCF
- 8 bits per channel, maximum value 255
- The canvas size, written in the header as 400 300
- Reader support: both readers we tried opened the P6 file and read the same pixels as the PNG route
Lost or not yet supported
- Transparency: alpha is blended onto
AlphaColor, white unless you set it. - Compression, bit depth above 8, resolution and color profile.
- Gray storage for grayscale XCF files: without
ColorSpace=gray, a grayscale test XCF came back as P6 with three equal channels.
Limits
- One XCF per request. The canvas may hold up to 67,108,864 pixels (8,192 × 8,192): 16,384 × 4,096 converts, to a 201,326,610-byte PNM, and 8,193 × 8,192 gets a 422.
- 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.
- The subtype follows
ColorSpace: P6 for color, P5 for gray and monochrome, always 8-bit. There is no ASCII or PAM output.
Where teams use it
Image processing code
Research and computer-vision code often reads Netpbm because the pixels follow a short text header with no compression. Converting GIMP-edited test images to PNM gives such code raw RGB without a PNG decoder.
Fixtures with a known byte size
The size is the header plus width × height × 3 bytes, so PNM output is easy to check in tests. The header is 15 bytes when both sides have three digits: the poster is 15 + 360,000 = 360,015 bytes, while a 64 × 48 canvas gets a 13-byte header.
Questions
Is the output PPM (P6)?
Yes. The header reads P6 on the first line and 400 300 255 on the second, 15 bytes in all. The raw RGB bytes follow, 3 per pixel from the top row down. With ColorSpace=gray the header starts with P5 and each pixel takes one byte.
Why is the PNM larger than the XCF?
PNM stores every pixel uncompressed, 3 bytes each. The sample's 207,799-byte XCF became 360,015 bytes.
What happens to transparent areas?
They are blended onto a solid color, white by default, before the pixels are written. Send AlphaColor, for example #000000, to change it.
Convert your first XCF file in a minute.