HDR to GIF API
POST a .hdr to /v2/convert/hdr/to/gif and get a GIF89a of the same size. The linear RGBE pixels are clipped at 1.0, rounded to 8 bits and mapped to a palette chosen for the image: 233 entries for the sample sky, holding 227 distinct colors.
- Endpoint
- POST /v2/
convert/ hdr/ to/ gif - Accepts
- hdr
- Sample timing
- 82 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/hdr/to/gif \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@lakeside-sunset-sky.hdr \
-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/hdr/to/gif",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("lakeside-sunset-sky.hdr", "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 = "lakeside-sunset-sky.hdr";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/hdr/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.082,
"Files": [
{
"FileName": "lakeside-sunset-sky.gif",
"FileExt": "gif",
"FileSize": 15217,
"FileData": "R0lGODlhkAHIAIAAAAAAAAAA…"
}
]
}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
- lakeside-sunset-sky.hdr (90 KB)
- Output
- lakeside-sunset-sky.gif (15 KB)
- Source
- A sunset sky panorama we drew in code and saved as a 400 × 200 Radiance RGBE file with run-length scanlines; the sun disc is 60.0. (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. ImageWidth=120 gave a 120 × 60 file of 4,084 bytes with 213 palette entries (203 distinct colors).Values: 1–20000 |
ImageHeightinteger | Height of the GIF; ImageHeight=100 came back 200 × 100 (7,089 bytes, 218 entries, 207 distinct colors).Values: 1–20000 |
ScaleProportionsboolean, default true | false lets a 200 × 200 request stretch the panorama (11,694 bytes, 238 entries, 220 distinct colors). |
ScaleIfLargerboolean, default false | true keeps the GIF at 400 × 200 when you ask for more; ImageWidth=800 alone gave 800 × 400 at 33,264 bytes. |
ScaleImageboolean, default true | false crops instead: the upper-left 200 × 100 quarter needed only 120 palette entries (118 distinct colors). |
ColorSpaceenum, default default | gray gave 240 shades in 20,808 bytes; monochrome 2 colors in 858 bytes.Values: default, rgb, gray, monochrome; cmyk returns 400 |
TransparentColorstring | Adds a transparent index for one exact color. #FFFFFF made 333 white pixels clear: 101 at the sun and 232 along its streak on the lake.Values: #RRGGBB, R,G,B or a color name |
FileNamestring | Output name; the upload's name is reused by default. |
StoreFileboolean, default false | true replaces the data with a 3-hour link. |
What carries over, and what doesn't
Kept
- The overall picture: against the 8-bit image, the average error is under one level per channel, the largest 24
- An opaque frame: Radiance has no alpha, and the GIF has no transparent index unless you ask for one
- The 400 × 200 size
Lost or not yet supported
- Most of the 9,031 colors: the palette keeps 227 in 233 entries, so neighbouring shades in a gradient can share one entry.
- Highlights over 1.0. Each channel stops at 255: the glow keeps an orange or yellow tint with its red channel flat, and the 333 pixels bright in all three channels are plain white.
- All precision beyond 8 bits, and the linear-light meaning of the values.
- The
EXPOSUREandGAMMAheader lines, which are not read.
Limits
- One .hdr file per call; one frame out.
- 10 MB on the Free plan or 100 MB on paid plans per request.
- 100,000,000 pixels, else 413.
- A GIF side can't exceed 65,535 px: a 70,000 × 8 .hdr returned 413
GIF images can be at most 65,535 pixels on a side, and this one is 70,000×8. - Standard top-down orientation only; other .hdr layouts return 422.
- 120 seconds per conversion.
Where teams use it
Previews in GIF-only places
Older forums, help desks and newsletter tools that take GIF but not HDR can still show a sky or lighting render.
Small stills of simple skies
Smooth, low-detail skies map well onto 256 colors; a 120 px wide copy of the sample is about 4 KB.
Questions
How are the 256 colors picked?
A palette is built for each image from its own pixels after they are cut to 8 bits. This sky used 233 entries. There is no option for a fixed palette, a smaller count or dithering.
Will the sky show bands?
It can, in slow gradients, because each palette entry stands for a range of similar colors. Against the 8-bit image, the sample's average difference was 0.78 levels and the largest 24 levels in one channel. Use hdr/to/png or hdr/to/webp when gradients must stay exact.
Can the GIF have a transparent area?
Only with TransparentColor, which clears one exact color. Sending #FFFFFF clears 333 pixels, the sun's clipped core and the middle of its streak on the lake, which is rarely what you want for a sky.
Convert your first HDR file in a minute.