HDR to BMP API
POST a .hdr file to /v2/convert/hdr/to/bmp and get an uncompressed Windows bitmap at 8 bits per channel. The RGBE pixels are clipped to 0 to 1 and written as 24-bit BGR behind the 40-byte BITMAPINFOHEADER, as a Radiance file never has transparency.
- Endpoint
- POST /v2/
convert/ hdr/ to/ bmp - Accepts
- hdr
- Sample timing
- 26 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/bmp \
-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/bmp",
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/bmp";
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.026,
"Files": [
{
"FileName": "lakeside-sunset-sky.bmp",
"FileExt": "bmp",
"FileSize": 240054,
"FileData": "Qk22qQMAAAAAADYAAAAoAAAA…"
}
]
}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.bmp (234 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 | New width; the height keeps the 2:1 ratio. ImageWidth=120 wrote a 120 × 60 bitmap, 21,654 bytes.Values: 1–20000 |
ImageHeightinteger | New height. ImageHeight=100 produced 200 × 100 and 60,054 bytes.Values: 1–20000 |
ScaleProportionsboolean, default true | false with 200 × 200 stretches the sky vertically into a square, 120,054 bytes. |
ScaleIfLargerboolean, default false | true stops the upscale that ImageWidth=800 would do (800 × 400, 960,054 bytes). |
ScaleImageboolean, default true | false crops: 200 × 100 from the top left is mostly sky, with the tops of the hills in its last rows. |
ColorSpaceenum, default default | Gray or black-and-white pixel values in the same 24-bit layout; the file size stays 240,054 bytes.Values: default, rgb, gray, monochrome; cmyk returns 400 |
ImageResolutioninteger | Fills the header's pixels-per-metre fields (11,811 for 300 dpi); left out, they are 0.Values: 10–800 |
FileNamestring | Changes the output name from lakeside-sunset-sky.bmp. |
StoreFileboolean, default false | Asks for a link, kept 3 hours, in place of the data. |
What carries over, and what doesn't
Kept
- The 400 × 200 grid, stored bottom-up as the positive height in the header says
- Values from 0 to 1, each rounded to one of 256 levels: the BMP matches the 16-bit PNG route divided by 257
- 9,031 distinct colors from the sky's gradients
Lost or not yet supported
- Light above 1.0. The sun, the core of its glow and the middle of the lake streak are 255 in every channel (333 pixels); 2,791 more, around them, are cut to 255 in red, or red and green, and keep an orange or yellow tint.
- Size against the source: the bitmap is 2.6 times the 92,651-byte .hdr.
- The linear-light meaning of the numbers, since nothing in a BMP says how to display them.
Limits
- A single .hdr per request.
- Requests up to 10 MB on the Free plan or 100 MB on paid plans.
- Pictures over 100,000,000 pixels: 413.
- Only top-down
-Y … +X …Radiance files are read. - 120 seconds, then 504.
Where teams use it
Sky images for Windows-only software
Simulators, kiosk software and older Windows tools that load BMP backgrounds can use a converted copy of an HDR sky, without an HDR loader.
Raw pixels in test code
A BMP's rows can be read with a few lines of code and no decoder, which suits tests that check a lighting render's pixel values.
Questions
Does the BMP have an alpha channel?
No. A Radiance file is always opaque, so the route writes a 24-bit BMP with the plain 40-byte header and no bit masks. The 400 × 200 sample is 240,000 bytes of pixels plus 54 bytes of headers, and ColorSpace=gray or monochrome keeps that layout.
Is the EXPOSURE line used?
No. Pixels are taken as stored. A file that says EXPOSURE=2.0 converts to the same pixels as one without the line.
How do the RGBE values map to BMP bytes?
Each pixel is decoded to linear light, clipped at 1.0 and scaled to 0 to 255. A value of 0.18 becomes 46 and 0.5 becomes 128; nothing is gamma encoded.
Convert your first HDR file in a minute.