PNM to GIF API
POST a netpbm image to /v2/convert/pnm/to/gif and get a single-frame GIF89a. The 1,934-color sample render was reduced to a 256-entry palette (183 entries in use) and came back as 9,879 bytes.
- Endpoint
- POST /v2/
convert/ pnm/ to/ gif - Accepts
- pnm
- 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/pnm/to/gif \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@three-spheres-render.ppm \
-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/pnm/to/gif",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("three-spheres-render.ppm", "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 = "three-spheres-render.ppm";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/pnm/to/gif";
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": "three-spheres-render.gif",
"FileExt": "gif",
"FileSize": 9879,
"FileData": "R0lGODlhQAHwAIAAAAAAAAAA…"
}
]
}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
- three-spheres-render.ppm (225 KB)
- Output
- three-spheres-render.gif (9.6 KB)
- Source
- A 320 x 240 frame from a small ray tracer we wrote for this page (three spheres on a checkered floor), saved as a binary PPM (P6) with 8 bits per channel. (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 of the GIF. At ImageWidth=128 the render is 128 × 96 and 6,077 bytes.Values: 1–20000 |
ImageHeightinteger | Height of the GIF; with a width as well, the frame is fitted inside both.Values: 1–20000 |
ScaleProportionsboolean, default true | false forces the exact width and height, changing the frame's shape. |
ScaleIfLargerboolean, default false | With true, only downscaling happens. |
ScaleImageboolean, default true | false crops the requested size from the top-left corner. |
ColorSpaceenum, default default | monochrome makes a two-color GIF of the render at 795 bytes; gray gives 10,186 bytes.Values: default, rgb, gray, monochrome |
TransparentColorstring | Marks one exact color as the GIF's transparent color. #92969D makes the 19,013 pixels of exactly that gray see-through, the unshaded parts of the dark floor tiles; shadowed, distant and edge pixels of those tiles have other grays and stay opaque.Values: Color name, #RRGGBB or R,G,B |
ImageResolutioninteger | Accepted and range-checked; GIF has nowhere to store it.Values: 10–800 |
ImageInterpolationboolean, default false | Accepted; resampling always uses Lanczos. |
FileNamestring | Name of the .gif; the upload's name otherwise. |
StoreFileboolean, default false | Store the GIF for 3 hours and reply with a link. |
What carries over, and what doesn't
Kept
- Shapes and shading closely: the largest error against the PPM was 38 levels, the average 0.11
- Exact colors when the image has 256 or fewer: a PBM keeps its 2 colors and a gray test PGM its 158 gray levels
- Full transparency from PAM files, where alpha is 0
Lost or not yet supported
- Colors beyond 256. The render's 1,934 colors were mapped to 183 palette entries.
- Partial transparency from PAM: rows with alpha 128 in a test file became fully opaque.
- 16-bit depth and the original maxval.
Limits
- One file per request; the GIF has one frame.
- Requests up to 10 MB on the Free plan or 100 MB on paid plans, all files and fields together.
- Images over 100,000,000 pixels return 413.
- Each side can be at most 65,535 px: a 65,536 × 2 PPM returned 413 "GIF images can be at most 65,535 pixels on a side, and this one is 65,536×2", while the 65,535 × 2 one converted.
- 120 seconds per conversion, then 504.
Where teams use it
Line art and masks saved as PBM
Black-and-white PBM bitmaps convert to very small GIFs: a 320 × 240 test bitmap came back as 795 bytes with its two colors exact.
Quick previews where only GIF is accepted
Where a forum, wiki or mail template accepts GIF but not PNG or WebP, a render converts to a 10 KB preview, at the cost of some color precision.
Questions
How are the colors reduced?
If the image has 256 colors or fewer, the palette is made of exactly those colors. Otherwise a quantizer chooses 256 entries, which is what happened to the render.
Can I make an animated GIF from several PPM frames?
Not on this route. It takes one file and writes one frame.
Does a PAM with alpha keep its transparency?
Only where alpha is 0. GIF has a single transparent color, so every partly transparent pixel (alpha 1 to 254) is stored as opaque.
Convert your first PNM file in a minute.