ORA to PNG API
Upload an .ora to /v2/convert/ora/to/png and the API reads stack.xml, composites the layer PNGs with their opacity, visibility, offsets, groups and blend modes, and returns the result as one PNG. The merged image stored in the archive is not used.
- Endpoint
- POST /v2/
convert/ ora/ to/ png - Accepts
- ora
- 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/ora/to/png \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@garden-plan.ora \
-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/ora/to/png",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("garden-plan.ora", "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 = "garden-plan.ora";
form.append("File", new Blob([await readFile(file)]), file);
const url = "https://api.pdfconversionapi.com/v2/convert/ora/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.046,
"Files": [
{
"FileName": "garden-plan.png",
"FileExt": "png",
"FileSize": 66293,
"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
- garden-plan.ora (123 KB)
- Output
- garden-plan.png (65 KB)
- Source
- A backyard vegetable garden plan that we drew and saved as an OpenRaster file with seven layers and a layer group, including a hidden layer and a multiply 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 of the merged PNG. The plan is 400 × 300; ImageWidth=1200 returned 1200 × 900 (466,787 bytes) for a print-size copy.Values: 1–20000 |
ImageHeightinteger | Height in pixels; ImageHeight=150 alone gave 200 × 150.Values: 1–20000 |
ScaleProportionsboolean, default true | With a width and a height, true fits the plan inside them (200 × 200 gave 200 × 150); false stretches it to exactly 200 × 200. |
ScaleIfLargerboolean, default false | true refuses to enlarge: ImageWidth=800 with it returned the original 66,293-byte PNG. |
ScaleImageboolean, default true | false crops rather than scales; 120 × 90 kept the corner with the start of the title. |
ColorSpaceenum, default default | gray gives an 8-bit grayscale plan (24,036 bytes); monochrome a pure black and white one (2,993 bytes).Values: default, rgb, gray, monochrome (also srgb, truecolors, grayscale); cmyk returns 400 |
ImageResolutioninteger | Writes the DPI into a pHYs chunk. The .ora declares 150 ppi, but the PNG only says so when you send ImageResolution=150.Values: 10–800 |
TransparentColorstring | Turns one exact color transparent and the PNG into RGBA. On the plan, #FFFFFF hit only 13 pixels: the label boxes are white at about 90% opacity, so they blend to off-white over the paper.Values: #RRGGBB, R,G,B or white, black, red, green, blue, gray |
FileNamestring | Base name of the PNG; garden-plan.ora becomes garden-plan.png otherwise. |
StoreFileboolean, default false | Keep the result for 3 hours and get Files[].Url back instead of base64. |
What carries over, and what doesn't
Kept
- The composite of the visible stack: our own composite of the sample's layers differed from the PNG by at most half a level on all 120,000 pixels
- Opacity and blend modes: the 60% drip line and the 50% Multiply fence shade
- Groups and offsets: the Beds layer sits at 40, 60 inside the Planting group
- Alpha when the canvas has transparent areas; the opaque sample gives an RGB PNG with 3,371 colors
- 16 bits per channel when the layer PNGs are 16-bit
Lost or not yet supported
- Layers. The PNG is flat; the hidden 'Old layout' layer is left out, as it should be.
- The masking operators dst-in, dst-out, src-atop and dst-atop, which are drawn as normal layers without an error.
- The 150 ppi in stack.xml and any color profile embedded in a layer PNG.
- Grayscale and paletted layers stay correct but come out as RGB.
Limits
- One .ora per request; the canvas may have up to 100,000,000 pixels and 300,000 px a side (a stack.xml w of 300001 returned 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.
- Layers must be raster images. Test layers stored as PNG, JPEG, WebP, BMP, GIF, TIFF, PPM and TGA were all composited; an SVG layer returns 422.
- Our test files come from our own OpenRaster writer, not yet from MyPaint, Krita or GIMP.
Where teams use it
Previews for apps that save OpenRaster
OpenRaster is the layered format shared by open-source painting apps such as MyPaint and Krita. A web app or file browser that accepts .ora uploads can show a PNG of the full canvas instead of the 256 px thumbnail stored inside.
Layered plans and diagrams
Plans drawn in layers, like the sample garden with its separate irrigation, planting and label layers, can be published as a single PNG while the .ora stays the editable source.
High bit depth exchange
Layers saved as 16-bit PNGs inside the .ora come back as a 16-bit PNG, so the depth chosen in the painting app is not cut to 8 bits on the way out.
Questions
Does the API just return mergedimage.png from the .ora?
No. We replaced the sample's mergedimage.png and thumbnail with solid red and got the same PNG, byte for byte. The layers listed in stack.xml are composited again, so an .ora without a merged image converts too.
Which composite-op values are supported?
These seventeen, each of which gave the value its formula predicts in our tests: src-over, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, color, luminosity, hue, saturation and plus. The masking operators dst-in, dst-out, src-atop and dst-atop are drawn as src-over.
How are groups with isolation='auto' handled?
As pass-through: their layers blend straight into what lies below, so a Multiply layer inside the group darkens the background. Groups marked isolate, or with no isolation attribute, are merged on their own first.
What if a layer file is missing from the archive?
That layer is treated as empty and the request still returns 200. A layer the API can't decode as a raster image, such as an SVG, fails the whole request with 422 the input file could not be read: it is not an ORA file, although only that layer is the problem.
Convert your first ORA file in a minute.