File Formats
OpenGPEX v2's unified file I/O: a single decision authority for import, a single GPU pipeline for export, and full wide-gamut / high-bit-depth fidelity.
Import Pipeline
Every file passes a two-stage ingest driven by one pure function:
extractMetadata(file) // header sniff: format, size, colorSpace, bitDepth, hasAlpha, isMultiFrame
→ resolveIngestDecision(md) // the only decision authority in the codebase
→ handler.decode(...) // pure pixel extraction for the assigned channel
→ AssetService.storeBundle() // light record + optional dec:/raw: keys
Decode Channels
| Channel | Used for | Notes |
|---|---|---|
image-bitmap |
8-bit sRGB/P3 JPEG, PNG, WebP, BMP, AVIF, single-frame GIF | Zero-copy browser decode; source bytes are the display blob |
wide-gamut-8 |
8-bit Adobe RGB / ProPhoto RGB | CPU degamma → f16 raw + P3 display proxy |
vips |
16-bit PNG/TIFF, 8-bit TIFF, 32-bit float TIFF | Single pass produces proxy + f16/f32 raw |
vips-icc |
CMYK | ICC transform to sRGB; 16-bit failures throw, never silently downgrade |
libraw |
Camera RAW (20+ vendor formats) | Scene-linear f16, trc: 'linear', filmic render intent (GPU truth line + shared-constant 8-bit display proxy) |
webcodecs-heic |
HEIC 8-bit SDR on devices with HEVC hardware decode | Per-tile VideoDecoder decode in the HEIC worker; a one-shot capability probe gates the entry |
libheif-proxy |
HEIC 10/12-bit, wide gamut, or no hardware decode | libheif wasm → RGBA-8 PNG proxy with a stock iCCP profile; metadata keeps reporting the true bit depth |
avif-proxy |
AVIF ≥10-bit SDR and ≥10-bit wide gamut | jsquash/libavif wasm decodes at native depth → precise rescale into f16, plus an 8-bit iCCP-tagged proxy |
gifuct |
Animated GIF | Per-frame RGBA pages |
vector |
SVG, EPS | 1:1 rasterized proxy; original vector bytes retained |
unsupported |
HDR transfer (PQ/HLG/BT.2020), animated AVIF (moov), HEIC multi-frame/HDR, sub-8-bit sources | Fail-fast UnsupportedIngestError short-circuits at the ingest entry with the precise reason — no mis-mapped pixels, no phantom high-depth records |
Key invariants: high-depth channel failures must throw (no silent 8-bit fallback), unsupported rows never force a decode (an honest error beats wrong pixels), and retainSourceBlob holds iff the channel is not image-bitmap (Revert re-decodes the retained original and reproduces the same content hash).
Export Pipeline
Export reuses the same WebGPU RenderGraph as the screen (One Pipeline): an offscreen scene locked at dpr: 1 renders through the identical shaders, then f32 premultiplied linear pixels are read back and encoded by one shared function (unpremultiplyEncodeGamut).
Egest Channels
| Channel | Precision | Gamuts | Path |
|---|---|---|---|
canvas-8 |
8-bit | sRGB, Display-P3 | Tagged OffscreenCanvas → browser / MozJPEG encode |
raw-8 |
8-bit | + Adobe RGB, ProPhoto RGB | Naked Uint8Array → wasm-vips (no Canvas ceiling) |
raw-16 |
16-bit | All supported | Naked Uint16Array → wasm-vips |
Container capability is the single gate: unsupported intent collapses to the working gamut, then sRGB; Rec.2020 harmlessly degrades to Display-P3 (the working gamut); HEIC/RAW have no encode channel and throw. Default bit depth follows the source document (a 16-bit source defaults to 16-bit export; wide gamut is never forced to 16-bit anymore). ICC embedding is decided per container (PNG/JPEG/WebP/TIFF yes; AVIF/BMP/GIF/SVG/EPS no — the AVIF encoder has no ICC-embed API at all).
Pass-Through Fast Path
Unedited, unclipped, unscaled, format-matched documents whose requested gamut equals the source gamut skip the GPU entirely — the retained source bytes download verbatim.
Metadata Preservation
| Data | Behavior |
|---|---|
| ICC profiles | Parsed to a named color space on import; re-embedded on export when the container supports it |
| EXIF | Camera, capture, dates, GPS preserved through the edit cycle; orientation applied per channel responsibility (auto / explicit / none) |
| DPI | Frame-level plus per-layer, sourced from EXIF / PNG pHYs / BMP header / TIFF tags |
| XMP / PNG gamma | Passed through the raw metadata layer |
Native .gpex Format
The project save format is a self-contained binary container: a 20-byte header (magic GPEX, version, section sizes) followed by three sections:
- Thumbnail — WebP preview image for cloud listings
- Manifest — outer JSON metadata read by the server without touching the ZIP:
frameLocalId(the cloud upsert key), frame name, canvas size, layer/asset counts, editor version - Payload — ZIP archive with the dehydrated document state plus all content-addressed assets:
state.json dehydrated Frame
assets-manifest.json per-asset geometry, colour identity, provenance
assets/{id}.png|webp 8-bit display bitmap, one per assetId
raw/{fileHash} original encoded source file, deduplicated by content hash
dec/{id}.bin decoded high-depth naked pixels (f16/f32), per dataFormat
Legacy containers written before assets-manifest.json existed carry no per-asset metadata; unpack degrades them to plain 8-bit registration (probing dimensions via createImageBitmap when the manifest geometry is missing), which matches their historical 8-bit ceiling rather than introducing loss.
Multi-Frame Semantics
All multi-frame containers decode into a unified pages: DecodedImage[] sequence; business logic then splits by meaning:
- Animated GIF → a time-sequence model inside a single frame (per-layer frame timing via
delay), reassembled on export - Multi-page TIFF → Trunk + Branch independent artboards, one page per artboard, physically sharing one
raw:source file
WASM Modules
Heavy format work runs in dedicated workers, never blocking the UI:
| Module | Purpose | Loading |
|---|---|---|
| wasm-vips | 16/32-bit TIFF & PNG encode, CMYK ICC transform, wide-gamut raw encode | Lazy, on first use |
| libraw-wasm | Camera RAW demosaic | Lazy, on first RAW import |
| libheif-js + WebCodecs | HEIC decode (hardware when the device qualifies, wasm proxy otherwise) | Lazy, on first HEIC import |
| @jsquash/avif | AVIF decode (≥10-bit proxy) and encode | Lazy, on first AVIF import / export |
| resvg / Ghostscript | SVG / EPS rasterization | Lazy, on first vector import |
wasm-vips is codec-only by contract — all compositing lives in the GPU RenderGraph.
Last updated: 2026-10-13