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