Skip to content

gamut-png: the XMP packet is an iTXt held outside the text list, so its framing needs a parallel field #600

Description

@justin13888

What

gamut-png surfaces a file's XMP packet as its own field — DecodedPng::xmp / PngMetadata::xmp,
raw bytes — rather than as an entry in texts. The packet is nonetheless an ordinary iTXt chunk
(§11.3.3.4) under the keyword §11.3.3.1 Table 21 reserves for it, so everything a TextChunk
already carries applies to it: a compression flag, a language tag, a translated keyword, and a
position in the file's chunk order.

Because the field carries only bytes, PR #550 had to add a second public type, XmpFraming, and a
second field, xmp_framing, to hold the rest of the chunk. That closed the immediate defect (a
compressed packet was being rewritten uncompressed — a 354-byte iTXt became 3 734 bytes) but
left three seams that consolidating the packet into texts would close together:

  1. Two fields that must agree. xmp and xmp_framing are Some together when the decoder
    produces them, but nothing in the type system says so, and a caller assembling a PngMetadata
    by hand can set one without the other.
  2. The packet is a single-value payload implemented inside a list. Ancillary::add_xmp has to
    retain away a previous packet so that setting it twice writes one chunk, which every other
    single-value payload (iccp, exif) gets for free from being an Option field.
  3. File order is lost. A carry emits the packet first among the annotations regardless of
    where its chunk sat in the source, because it is set before the texts loop.

Why it is a separate decision

It reshapes a public type on a crate that is about to take its first version bump: xmp would
either disappear or become a convenience accessor over texts, and XmpFraming would be retired.
Whether the packet should keep a dedicated read surface at all — callers hand it straight to
gamut_metadata::MetadataBlock::Xmp, which is why the field exists — is the question to settle
first, and it is not one PR #550's record covers.

Options to weigh

  • Keep texts as the single list, and make xmp a #[must_use] accessor that finds the reserved
    keyword. Costs a scan; keeps the ergonomic read path.
  • Keep both surfaces but make the pairing unrepresentable: replace xmp: Option<Vec<u8>> +
    xmp_framing: Option<XmpFraming> with one Option<XmpPacket>.
  • Leave it as it is and document the three seams, which is what feat(png)!: preserve metadata across a re-encode, and name what could not come along #550 does.

Spec

references/png/png-3.html §11.3.3.1 Table 21 (the reserved keyword and its recommended framing),
§11.3.3.4 (iTXt), §14.3.2 (a reader may not assume ancillary chunk ordering — which bounds how
much (3) actually costs).

Where

crates/gamut-png/src/decoded.rs, crates/gamut-png/src/ancillary.rs,
crates/gamut-png/src/encoder.rs, crates/gamut-png/tests/preservation.rs,
crates/gamut-png/STATUS.md.

Refs #483. Raised by the review of #550.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions