# Flat OpenEXR storage and interchange

MoonEXR 0.2.0 adds tiled images, PIZ and flat multipart containers to the
MoonBit runtime. Scanline entry points remain available. See the
[OpenEXR file layout](https://openexr.com/en/latest/OpenEXRFileLayout.html).

## Data model and workflows

- `ExrDocument::open` indexes the container. `part` and `named_part` return
  immutable views that share the input bytes. Each part has its own data
  window, channels, codec and storage layout.
- `ExrFile::read` reads the base resolution. `read_level(lx, ly, region=...,
  channels=...)` selects a stored resolution and clips the request in that
  level's coordinates. The origin is the base data window's minimum x/y.
  `plan_level` reports the offset, encoded length and decoded length of each
  intersecting block. A scan of a tiled base level processes one row of
  intersecting tiles before advancing to the next.
- `Image::nearest_levels` builds mipmap or ripmap levels using exact copies of
  samples at base-relative coordinates `(x * 2^lx, y * 2^ly)`. This is useful
  for integer labels and other data that must retain sample bits. For filtered
  colour levels, construct the level images separately and pass them in
  `TileDescription::levels` order.
- `encode_tiled` writes single-level, mipmap and ripmap images. In
  `encode_multipart`, the caller chooses each part's codec and storage.
  Images for a tiled part share channels, display window and metadata. Different
  parts may use different channels and data windows. The file has one display
  window and pixel aspect ratio.
- `extract` exports a selected part, level, region and channel set as a
  standalone scanline or single-level tiled EXR. `ExrDocument::transcode`
  changes compression while preserving the part names, storage layout,
  stored levels and sample bits. Output uses increasing chunk order.
  Transcode materializes the decoded images for all parts and levels.

Run `moon run examples/storage --target native` for a mixed file with a PIZ
object-ID pyramid and a ZIP scanline depth part, level-specific ROI extraction,
and container-wide recompression.

## Correctness and cost

The old scanline directory inferred a full-width span from each offset-table
index. On a tiled or multipart file that assumption would misinterpret chunk
headers and sample row strides. Each directory entry now carries its own
window and payload start. The reader validates the table slot against the
part number, tile coordinates or scanline position, checks all physical spans
for overlap, and budgets the sum of logical bytes across parts and levels.

For a selected level, integer division converts the ROI to a tile rectangle.
Enumeration costs O(K) for K intersecting chunks and uses O(K) planning space.
Level selection currently scans O(L) level records. Opening a file costs
O(C log C) time and O(C + L) additional directory space for C chunks and L
levels, excluding header storage. Arbitrary physical chunk order is allowed.
The input remains in memory. A channel projection still decompresses the
entire selected chunk.

PIZ transposes chunk rows into channel planes, maps the active 16-bit values
to a compact range, transforms each channel's 16-bit lanes with the OpenEXR
Haar lifting rules, and writes a canonical Huffman stream. Decode reverses
these steps. FLOAT and UINT use two lanes and keep the original bit pattern.
The encoder sorts used symbol frequencies once, then merges from ordered
leaf and branch queues. For N words, symbol span A and U used symbols, cost
is O(N + A + U log U + B) time, where B is the encoded bit count, with
O(N + A) working storage plus a fixed 8192-entry bitmap.
Decode uses O(N + A + B) time and O(N + A) working storage. The writer stores
raw bytes when a PIZ payload would not shrink the block. Very small or
high-entropy blocks may gain little from PIZ. Compare ZIP and PIZ on the
asset set before standardizing on a codec.

Deep data, subsampled channels, PXR24, B44/B44A, DWA and newer codecs are
outside this release. Pixel arrays and in-memory byte buffers are bounded by
logical limits, with temporary allocations in addition to those limits.
Files with missing chunk offsets are rejected.

## Verification

`scripts/interop.py verify CLI` generates independent fixtures using official
OpenEXR 3.4.2 and checks both directions. It covers actual compressed PIZ
payloads, full-range modular lifting, raw fallback, negative origin,
non-finite payloads, clipped tiles, random tile line order and a mixed
scanline/tiled multipart document.

`scripts/verify_corpus.py CLI CORPUS` verifies SHA-256 of the following
downloaded files, transcodes them and compares channel dtypes, windows and
every sample bit with OpenEXR. For multi-resolution files, each level's
compressed tiles are reframed into a temporary single-level file so the
official Python binding can decode that level. The source images are
downloaded separately from the official corpus under their original terms.

| Official sample | Input | Coverage |
| --- | --- | --- |
| [GoldenGate.exr](https://openexr.com/en/latest/test_images/Tiles/GoldenGate.html) | tiled PIZ | 70 tiles, including clipped edges |
| [multipart.0001.exr](https://openexr.com/en/latest/test_images/Beachball/multipart.0001.html) | multipart ZIPS | 10 named parts, with their individual data windows |
| [Blobbies.exr](https://openexr.com/en/latest/test_images/ScanLines/Blobbies.html) | scanline ZIP | Negative origin, 5 channels, PIZ output |
| [MirrorPattern.exr](https://openexr.com/en/latest/test_images/MultiResolution/MirrorPattern.html) | tiled mipmap ZIP | All 10 levels, PIZ output |
| [Kapaa.exr](https://openexr.com/en/latest/test_images/MultiResolution/Kapaa.html) | tiled ripmap ZIP | All 121 levels, PIZ output |

MoonBit tests cover both rounding modes, odd dimensions, cross-tile ROI
scans, malformed offsets and PIZ streams, aggregate resource limits, and
32-line PIZ batch boundaries. The synthetic suite runs in CI. The existing
asset suite is opt-in to avoid downloading external images during CI.
