EXR to PNG API
POST an .exr file to /v2/convert/exr/to/png and get a 16-bit PNG back as base64 in the JSON, with alpha when the render has it. Each channel is written as stored, clipped to 0 to 1: a channel above 1.0 becomes 65535, and linear light is not converted to sRGB.
- Endpoint
- POST /v2/
convert/ exr/ to/ png - Accepts
- exr
- Sample timing
- 27 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/exr/to/png \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@glowing-bulb-render.exr \
-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/exr/to/png",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("glowing-bulb-render.exr", "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 = "glowing-bulb-render.exr";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/exr/to/png";
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.027,
"Files": [
{
"FileName": "glowing-bulb-render.png",
"FileExt": "png",
"FileSize": 252885,
"FileData": "iVBORw0KGgoAAAANSUhEUgAA…"
}
]
}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
- glowing-bulb-render.exr (187 KB)
- Output
- glowing-bulb-render.png (247 KB)
- Source
- A glowing light bulb we drew in code and saved as a 240 × 300 OpenEXR render: half-float RGBA, premultiplied alpha, ZIP compression, values up to 12.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 PNG in pixels; the height follows the render. ImageWidth=120 returned the 240 × 300 bulb at 120 × 150 (78,735 bytes), still 16 bits per channel.Values: 1–20000 |
ImageHeightinteger | Height in pixels. ImageHeight=100 gave 80 × 100. Sent together with ImageWidth, the two form a box: 200 × 200 fitted the render at 160 × 200.Values: 1–20000 |
ScaleProportionsboolean, default true | false stretches the frame to the exact box, so 200 × 200 came back square. Without a width or height it is accepted and changes nothing. |
ScaleIfLargerboolean, default false | true refuses to enlarge: ImageWidth=800 with it returned the original 252,885-byte PNG, while without it the API upscaled to 800 × 1000 (2,670,791 bytes). |
ScaleImageboolean, default true | false crops instead of scaling. ImageWidth=200 and ImageHeight=100 kept the top-left 200 × 100 pixels: the upper glass and its halo. |
ColorSpaceenum, default default | gray writes an 8-bit gray PNG that keeps alpha (29,609 bytes); monochrome thresholds at level 128 (15,232 bytes). Both leave 16 bits behind.Values: default, rgb, gray, monochrome (also srgb, truecolors, grayscale); cmyk returns 400 |
ImageResolutioninteger | Adds a pHYs chunk with this DPI; 300 made the file 21 bytes longer. Left out, the PNG records no resolution.Values: 10–800 |
TransparentColorstring | Pixels of exactly this color become fully clear, and the PNG is written at 8 bits. #FFFFFF cleared 2,232 pixels in the filament and the core of the glass, the ones that are white at 8 bits (2,216 of them clipped from above 1.0).Values: #RRGGBB, R,G,B or white, black, red, green, blue, gray |
FileNamestring | Base name of the result. Without it, glowing-bulb-render.exr comes back as glowing-bulb-render.png. |
StoreFileboolean, default false | true keeps the PNG for 3 hours and puts a link in Files[].Url instead of base64 FileData. |
What carries over, and what doesn't
Kept
- Every value between 0 and 1, at 16 bits: the PNG matched our own decode of the EXR, clipped to that range, on all 72,000 pixels and all four channels
- The alpha channel, value for value: 18,978 fully clear and 28,204 partly clear pixels in the sample
- Half-float and 32-bit float files alike; NONE, RLE, ZIPS and ZIP compression and a tiled file all gave identical PNGs
- A luminance-only EXR (just a Y channel) as a 16-bit grayscale PNG
Lost or not yet supported
- Highlights above 1.0. The 12.0 filament and the glass around it reach 65535; 10,345 pixels of the sample hit the maximum in at least one channel. There is no exposure or tone-mapping option.
- The display curve. Linear values go in unchanged and no gAMA, sRGB or ICC chunk is written, so viewers show mid-tones dark: a stored 0.18 becomes 18% where an sRGB encode gives about 46%.
- Premultiplication is not undone. A halo pixel stored as (0.60, 0.40, 0.17) at 48% alpha keeps those numbers as straight color, so soft edges and glows look dimmer than the render intends.
- Depth (Z) and other extra channels, layers beyond the first by name, and every part after the first in a multi-part file.
- The display window. The PNG covers the data window, so overscan stays in and a smaller data window is not padded out.
- Color primaries. An EXR tagged with ACES chromaticities gave the same PNG values as an untagged one.
Limits
- One EXR per request, with up to 10 MB on the Free plan or 100 MB on paid plans for the file and fields together.
- 100,000,000 pixels per image. An 8,193 × 8,192 render converted; 10,001 × 10,000 got a 413.
- 120 seconds per conversion, then 504.
- Resize targets from 1 to 20,000 px per side.
- Tested so far: NONE, RLE, ZIPS and ZIP compression, scanline and tiled files. PIZ, PXR24, B44 and DWA files have not been tested yet.
Where teams use it
Render previews in a browser
Lighting and lookdev artists write frames as half-float EXR, which browsers can't show. A PNG per frame lets a review page, a ticket or a pull request display the shot, alpha included, over a checkerboard.
A 16-bit hand-off for scripts
When a later step crops, grades or composites the frame, the PNG keeps 65,536 levels per channel inside the 0 to 1 range, so dark gradients stay finer than an 8-bit copy allows.
Thumbnails in an asset library
Libraries that store EXR textures, decals and renders can create a PNG beside each file when it is uploaded, for search results and pickers that only understand web formats.
Questions
Why does the PNG look darker than the render in my compositor?
EXR pixels are linear light, and the PNG receives the same numbers with no sRGB curve and no profile. Viewers treat PNG values as sRGB, so a mid-gray of 0.18 displays at 18% brightness instead of about 46%. Apply the sRGB transfer to the 16-bit values afterwards, or bake your view transform into the frame before you send it.
What happens to values above 1.0?
They are cut to 65535 in each channel; nothing is tone mapped. In the sample, the 12.0 filament and the brightest glass became pure white. Lower the exposure in your renderer or pipeline first if the highlight detail matters.
Which channels does the API read?
R, G, B and A. A file with only Y gives a gray PNG, and Y with A gives gray plus alpha. Depth and other named channels are ignored. With no plain R, G and B, the layer whose name sorts first is used: of diffuse.R/G/B and specular.R/G/B, the PNG showed diffuse.
Is the alpha in the PNG premultiplied?
PNG alpha is straight, and the API copies the EXR's colors without dividing them by alpha. Opaque and fully clear pixels come out right; partly clear ones come out darker in proportion to their alpha.
Does ConvertAPI convert EXR to PNG?
Its service catalogue, as we fetched it on 2026-11-01, has no EXR input. This route takes the same multipart File field and returns the same Files[] JSON as the other image routes on this site.
Convert your first EXR file in a minute.