Skip to content

feat(gainmap): ISO 21496-1 URN + PRIMARY_APP2_BODY + JpegApp2BodyWithUrn/JxlJhgm#15

Merged
lilith merged 1 commit intomainfrom
feat-iso21496-urn
Apr 21, 2026
Merged

feat(gainmap): ISO 21496-1 URN + PRIMARY_APP2_BODY + JpegApp2BodyWithUrn/JxlJhgm#15
lilith merged 1 commit intomainfrom
feat-iso21496-urn

Conversation

@lilith
Copy link
Copy Markdown
Member

@lilith lilith commented Apr 21, 2026

Summary

Promotes the ISO 21496-1 namespace URN and the primary-image signal marker
into zencodec::gainmap, and restructures Iso21496Format so each variant
names the container that consumes exactly those bytes.

New format variants

Variant Discriminant Bytes Canonical consumer
JpegApp2 (deprecated) 0 same as JxlJhgm historical alias
AvifTmap 1 version(u8=0) + <JxlJhgm> AVIF tmap item payload
JxlJhgm 2 min_ver + writer_ver + flags + channel data JXL jhgm bundle gain_map_metadata field
JpegApp2BodyWithUrn 3 URN(28) + <JxlJhgm> body of a JPEG APP2 segment (after FF E2 + u16 BE length)

The Body qualifier on JpegApp2BodyWithUrn signals explicitly that the
FF E2 marker and u16 BE length envelope are NOT included — those stay
with zenjpeg as pure JPEG syntax.

Discriminants are pinned with explicit = 0..3 values plus a const _: () = assert!(...)
block so future variant changes trip at compile time instead of silently
shifting as u8 results.

Deprecation

  • Iso21496Format::JpegApp2 → misleading (bytes lack the URN). Deprecated
    alias for JxlJhgm, kept at discriminant 0 so as u8 casts keep working.

Public constants at zencodec::gainmap:: (re-exported at the crate root)

  • ISO_21496_1_URN: &[u8; 28] — the urn:iso:std:iso:ts:21496:-1\0 namespace
    string. Byte-identical to libultrahdr's kIsoNameSpace.
  • ISO_21496_1_PRIMARY_APP2_BODY: &[u8; 32] — the full JPEG APP2 body
    (URN + min_version=0 + writer_version=0) that the primary image of a
    canonical Ultra HDR JPEG carries. Callers memcmp against this to emit or
    detect; no need to concatenate URN + version bytes themselves.

GainMapParseError::UrnMismatch added for URN-aware parsing failures.

Agent survey of codec needs

Before settling on the single 32-byte PRIMARY_APP2_BODY constant vs. a
4-byte VERSION_SIGNAL + helper function, I had agents audit zenjpeg,
zenavif, and zenjxl. Result:

Codec Needs signal? Reads version fields separately?
zenjpeg Yes (primary APP2 emit/detect) No — discards writer_version, delegates min_version check to parse_iso21496_fmt
zenavif No No — uses ISOBMFF iref/dimg for signaling; version bytes are always bundled in the tmap payload
zenjxl No No — gain_map_metadata is an opaque round-trip blob

One codec memcmps the full APP2 body; zero codecs extract version fields in
isolation. The 32-byte constant is what callers actually need.

Single-allocation serialization

Extracted serialized_size(params, format) plus *_into(params, &mut Vec<u8>)
appenders so each public path does exactly one allocation. JpegApp2BodyWithUrn
shares the same buffer as its payload — no intermediate Vec.

Why this and not the full JPEG APP2 marker (FF E2 + length)

That envelope is pure JPEG syntax and belongs in zenjpeg; only the URN/payload
pieces are ISO-defined and cross-codec. This PR is intentionally scoped to
zencodec; a follow-up will DRY up zenjpeg's three copies of the
push 0xFF; push 0xE_; push length_hi; push length_lo; pattern in its
XMP / MPF / ISO APP2 writers and rewire the ISO writers to consume
Iso21496Format::JpegApp2BodyWithUrn and ISO_21496_1_PRIMARY_APP2_BODY
from zencodec directly.

Test plan

  • cargo test --all-targets (423 + 100 + 29 + 0 pass)
  • cargo test --doc (13 pass)
  • cargo clippy --all-targets -- -D warnings (clean)
  • cargo semver-checks check-release (clean — discriminants pinned and preserved, deprecated variant kept as alias, new variants appended; enum is #[non_exhaustive])
  • cargo build --target wasm32-wasip1 --lib (no_std clean)
  • New tests: URN shape, primary APP2 body shape, primary body not parseable as full metadata, roundtrip JpegApp2BodyWithUrn, URN-mismatch / wrong-URN / short-input rejection, deprecated JpegApp2 alias equivalence, discriminants pinned to explicit values

@lilith lilith force-pushed the feat-iso21496-urn branch 3 times, most recently from 273fe20 to 2e8b3c7 Compare April 21, 2026 07:25
@lilith lilith changed the title feat(gainmap): add ISO 21496-1 URN constant and URN-aware helpers feat(gainmap): add ISO 21496-1 URN and JpegApp2BodyWithUrn/JxlJhgm format variants Apr 21, 2026
@lilith lilith force-pushed the feat-iso21496-urn branch from 2e8b3c7 to d9470b1 Compare April 21, 2026 07:42
@lilith lilith changed the title feat(gainmap): add ISO 21496-1 URN and JpegApp2BodyWithUrn/JxlJhgm format variants feat(gainmap): ISO 21496-1 URN + PRIMARY_APP2_BODY + JpegApp2BodyWithUrn/JxlJhgm Apr 21, 2026
…Urn/JxlJhgm

- Add `Iso21496Format::JxlJhgm` (bare payload, canonical name) and
  `Iso21496Format::JpegApp2BodyWithUrn` (URN + payload = body of a JPEG
  APP2 segment after the FF E2 marker and u16 BE length have been
  stripped). Discriminants pinned with explicit `= 0..3` values plus a
  `const _: () = assert!(...)` block so future variant changes trip at
  compile time instead of silently shifting `as u8` results.
- Deprecate `Iso21496Format::JpegApp2` — misleading name (the bytes
  aren't a standalone APP2 body, they lack the URN). Kept at its
  original discriminant `0` so existing `as u8` casts keep working. It
  produces and accepts identical bytes to `JxlJhgm` but is a distinct
  variant (Rust does not allow shared discriminants).
- Add `ISO_21496_1_URN: &[u8; 28]` public constant — the ISO-defined
  namespace URN. Cross-codec; byte-identical to libultrahdr's
  `kIsoNameSpace`.
- Add `ISO_21496_1_PRIMARY_APP2_BODY: &[u8; 32]` public constant — the
  complete JPEG APP2 body (URN + `min_version=0, writer_version=0`)
  that the primary image of a canonical Ultra HDR JPEG carries to
  advertise ISO 21496-1 awareness. One constant emit-and-detect rather
  than requiring callers to concatenate URN + version bytes themselves.
- `parse_iso21496_fmt` / `serialize_iso21496_fmt` now handle all four
  variants with a single allocation.
- Add `serialize_iso21496_fmt_into(params, format, &mut Vec<u8>)` for
  callers that want to embed the ISO payload in a larger buffer without
  an intermediate `Vec` (e.g., building a JPEG APP2 marker + length +
  body in one allocation).
- Add `GainMapParseError::UrnMismatch` for URN-aware parsing failures.

Rationale: the URN is ISO-defined and belongs next to `Iso21496Format`,
which lets us fold URN framing into the enum and collapse the API
surface to one parse/serialize pair. Each variant now names the
container that consumes exactly those bytes (JxlJhgm, AvifTmap,
JpegApp2BodyWithUrn). The `Body` qualifier signals explicitly that the
FF E2 marker and u16 BE length envelope are NOT included — those stay
with zenjpeg as pure JPEG syntax. `ISO_21496_1_PRIMARY_APP2_BODY` hands
callers the full 32 bytes they need rather than a 4-byte
`VERSION_SIGNAL` that was only meaningful when concatenated with the
URN. Agent survey of zenjpeg/zenavif/zenjxl confirmed no codec needs
the version tail in isolation.
@lilith lilith force-pushed the feat-iso21496-urn branch from d9470b1 to 5e31270 Compare April 21, 2026 07:48
@lilith lilith merged commit 945b694 into main Apr 21, 2026
13 of 14 checks passed
@lilith lilith deleted the feat-iso21496-urn branch April 21, 2026 07:52
@lilith lilith self-assigned this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant