Skip to content

feat(jpeg): preserve/expose XMP metadata, Extended XMP, and uhdr_strip_gain_map - #493

Closed
DichenZhang1 wants to merge 6 commits into
mainfrom
feat/preserve-xmp-metadata
Closed

DichenZhang1 wants to merge 6 commits into
mainfrom
feat/preserve-xmp-metadata

Conversation

@DichenZhang1

Copy link
Copy Markdown
Collaborator

Summary

This PR completes end-to-end JPEG XMP metadata preservation, Adobe XMP Part 3 Extended XMP support, namespace-aware XMP merging, and lossless gain-map stripping (uhdr_strip_gain_map) across libultrahdr.

Key Changes

  1. Preserve & Expose JPEG XMP Metadata Across Encode and Decode (uhdr_enc_set_xmp_data, uhdr_dec_get_xmp):

    • Preserves caller-supplied and base-image XMP metadata across API-0 through API-4 JPEG encoding workflows.
    • Exposes the primary image's XMP metadata (with Extended XMP reassembled into the primary <rdf:RDF> packet) via uhdr_dec_get_xmp.
    • Unifies JPEG segment header/limit constants (kJpegMaxSegmentLength, kMaxStandardXmpPayload, kMaxExtendedXmpChunkPayload, kExtendedXmpHeaderSize) in jpegr.h.
  2. Adobe XMP Part 3 Multi-Segment Extended XMP Support (> 65,503 Bytes):

    • Supports arbitrary-sized XMP payloads up to kMaxTotalXmpBytes (16 MiB) by splitting payloads larger than 65,503 bytes into 65,458-byte http://ns.adobe.com/xmp/extension/\0 APP1 chunks linked by an upper-case 32-hex-character MD5 digest (xmpNote:HasExtendedXMP).
    • Reassembles multi-segment Extended XMP chunks in JpegDecoderHelper with strict validation (MD5 format, consistent total_length, out-of-order and duplicate identical chunk handling, and rejection of conflicting/overlapping/incomplete chunks).
    • Handles the near-limit Standard XMP boundary (~64.9 KB – 65,503 B) when kWriteXmpMetadata (-DUHDR_WRITE_XMP=ON) is active: if merging the Ultra HDR Container:Directory pushes a standard-sized user XMP packet over kMaxStandardXmpPayload (65,503 bytes), JpegR::appendGainMap spills the unmerged user XMP cleanly into Extended XMP chunks and writes only the Ultra HDR primary XMP + xmpNote:HasExtendedXMP GUID into the Standard XMP segment (avoiding duplicate Container:Directory entries). uhdr_encode sizes xmpAllowance to accommodate both the Standard XMP segment and Extended XMP chunk headers.
  3. Namespace-Aware Primary XMP Merging (MergePrimaryXmp in jpegrutils.cpp):

    • Replaces fragility of raw prefix/substring matching (find("</rdf:RDF>")) with a namespace-aware XML token/span parser (XmpMergeXmlHandler built on image_io::XmlReader).
    • Safely preserves custom namespace prefixes (<r:RDF>, <x:xmpmeta>), <?xpacket?> processing instructions, XML comments, CDATA sections, and non-primary descriptions (rdf:about="http://...", rdf:ID, rdf:nodeID), while replacing stale primary-image Container:Directory and hdrgm:Version attributes/elements.
    • In ISO-only mode (kWriteXmpMetadata == false), API-4 (encodeJPEGR(sdr_intent_compressed, gainmap_compressed, metadata, dest)) passes through the base JPEG's XMP byte-for-byte without injecting redundant GContainer / hdrgm properties.
  4. Lossless Gain-Map Stripping Public C API (uhdr_strip_gain_map):

    • Adds UHDR_EXTERN uhdr_error_info_t uhdr_strip_gain_map(uhdr_compressed_image_t* in_stream, uhdr_mem_block_t* out_stream) in ultrahdr_api.h and JpegR::stripGainMap in jpegr.cpp.
    • Losslessly copies the primary SDR JPEG bitstream (SOI..EOI, including DRI and RST0..RST7 restart markers in entropy-coded scan data) without re-encoding pixels.
    • Removes the APP2 MPF directory segment, APP2 ISO 21496-1 segment, and everything after the primary image's EOI marker (secondary gain-map JPEG stream and any trailing container bytes).
    • Sanitizes the Standard XMP APP1 segment via stripGainMapFromXmp: strips Container (http://ns.google.com/photos/1.0/container/), Item (http://ns.google.com/photos/1.0/container/item/), hdrgm (http://ns.adobe.com/hdr-gain-map/1.0/), and apdi (http://ns.apple.com/HDRGainMap/1.0/) properties while preserving all unrelated user XMP attributes/elements (dc, xmp, exif, GCamera:MicroVideo, xmpNote:HasExtendedXMP, etc.). Drops the Standard XMP APP1 segment completely if it only contained generated gain-map metadata.
  5. Fuzzer & Unit Test Coverage:

    • Added uhdr_strip_gain_map and uhdr_dec_get_xmp calls to ultrahdr_dec_fuzzer.cpp and ultrahdr_enc_fuzzer.cpp without altering FuzzedDataProvider consumption order.
    • Added unit tests in jpegr_test.cpp, jpegdecoderhelper_test.cpp, and ultrahdr_api_test.cpp covering:
      • Namespace-aware XMP merging with custom prefixes, comments/CDATA, non-primary rdf:about descriptions, stale Container:Directory replacement, and rejection of malformed/unsupported XML constructs
      • Near-limit Standard XMP (65,200 bytes) spilling cleanly into Extended XMP in -DUHDR_WRITE_XMP=ON mode (NearLimitStandardXmpSpillsCleanlyToExtendedXmp)
      • Extended XMP multi-segment encoding, decoding, out-of-order reassembly, duplicate identical chunks, and malformed/conflicting chunk rejection
      • uhdr_strip_gain_map on synthetic and real-world fixtures (apple_gainmap_new.jpg, apple_gainmap_old.jpg), verifying byte-identical primary entropy scan preservation, EXIF/ICC/Extended XMP retention, trailing trailer removal, and corrupt marker rejection

Test Plan

  • Built and ran ultrahdr_unit_test in both CMake configurations:
    1. Default ISO mode (-DUHDR_BUILD_TESTS=ON -DUHDR_WRITE_ISO=ON -DUHDR_WRITE_XMP=OFF): All UltraHdrApiTest (21/21), JpegRTest, and JpegDecoderHelperTest suites pass.
    2. Legacy XMP + ISO dual mode (-DUHDR_BUILD_TESTS=ON -DUHDR_WRITE_ISO=ON -DUHDR_WRITE_XMP=ON): All UltraHdrApiTest (26/26), JpegRTest (34/34), and JpegDecoderHelperTest (17/17) suites pass.

DichenZhang1 and others added 6 commits September 12, 2026 03:56
- Expose uhdr_enc_set_xmp_data and uhdr_dec_get_xmp in public C API
- Support passing custom XMP metadata into encodeJPEGR and appendGainMap
- In ISO metadata mode, pass through user/base XMP packet directly
- In XMP metadata mode, merge Ultra HDR container directory into user/base XMP RDF
- Automatically extract and preserve base JPEG XMP in API-2/3/4 encoding workflows
- Wire -X <xmp_file> and probe XMP display into ultrahdr_app CLI
- Add unit tests verifying XMP encoding, decoding, and preservation across API-0 and API-2
- Support encoding and decoding Extended XMP metadata (> 64 KB) in JPEG
- Compute 128-bit MD5 GUID and inject xmpNote:HasExtendedXMP into Standard XMP
- Chunk serialized Extended XMP payload across multiple http://ns.adobe.com/xmp/extension/\0 APP1 segments
- Reassemble Extended XMP chunks during JPEG header parsing and expose reconstructed buffer
- Calculate dynamic encoder output buffer allowance based on total XMP size and chunk count
- Add unit tests validating multi-segment Extended XMP encoding, decoding, and bit-for-bit roundtrip integrity
…ripping

- Add public C API uhdr_strip_gain_map and JpegR::stripGainMap to losslessly strip secondary gain map bitstream, MPF APP2, ISO 21496-1 APP2, and Ultra HDR XMP tags
- Preserve primary JPEG entropy-coded scan data bit-for-bit along with Exif, ICC, APP13, Extended XMP, and user XMP metadata
- Support size-query mode when out_stream->data is nullptr and out_stream->capacity is 0
- Add namespace-aware XML parsing for primary XMP merging and gain-map tag stripping (MergePrimaryXmp and stripGainMapFromXmp)
- Add unit tests covering uhdr_strip_gain_map and primary XMP merging across namespace prefixes and re-encoding
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.

2 participants