Launch OpenGPEX

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 (50+ formats) Scene-linear output, trc: 'linear', filmic render intent
heic-to HEIC JPEG q0.95 proxy
gifuct Animated GIF Per-frame RGBA pages
vector SVG, EPS 1:1 rasterized proxy; original vector bytes retained
unsupported Sub-8-bit PNG, animated WebP/AVIF, unknown tags Fail-fast error, no phantom high-depth records

Key invariants: high-depth channel failures must throw (no silent 8-bit fallback), AVIF is pre-clamped to 8-bit, 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
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-12