#Development
See also: docs/metadata_support.md for current container/block/decode support.
- CMake
>= 3.20 - A C++20 compiler (Clang is recommended; fuzzing requires Clang)
- Optional: Ninja (
-G Ninja)
OpenMeta discovers optional dependencies via find_package(...). If you install
deps into a custom prefix, pass it via CMAKE_PREFIX_PATH (example:
-DCMAKE_PREFIX_PATH=/mnt/f/UBSd).
OpenMeta's core scanning and EXIF/TIFF decoding do not require third-party libraries. Some metadata payloads are compressed or structured; these optional dependencies let OpenMeta decode more content:
- Expat (
OPENMETA_WITH_EXPAT): parses XMP RDF/XML packets (embedded blocks and.xmpsidecars). Expat is used as a streaming parser so OpenMeta can enforce strict limits and avoid building a full XML DOM from untrusted input. - zlib (
OPENMETA_WITH_ZLIB): inflates Deflate-compressed payloads, including PNGiCCP(ICC profiles) and compressed text/XMP chunks (iTXt,zTXt). - Brotli (
OPENMETA_WITH_BROTLI): decompresses JPEG XLbrob"compressed metadata" boxes so wrapped metadata payloads can be decoded. - Draft C2PA verify scaffold (
OPENMETA_ENABLE_C2PA_VERIFY,OPENMETA_C2PA_VERIFY_BACKEND): enables backend selection/reporting fields (none|auto|native|openssl) and draft verification flow. Native backend availability is platform-based (Windows/macOS), while OpenSSL availability is discovered viafind_package(OpenSSL)when needed.
If you link against dependencies that were built with libc++ (common when
using Clang), configure OpenMeta with:
-DOPENMETA_USE_LIBCXX=ONVERSION is the single source of truth for the project version:
- CMake reads
VERSIONand setsPROJECT_VERSION. - The Python wheel version is derived from
VERSION(via scikit-build-core metadata).
metavalidate checks decode-status health and DNG/CCM validation:
#Basic validation
./build/metavalidate input.dng
#Strict mode : warnings fail the file
./build/metavalidate --strict input.dng
#Machine - readable JSON output
./build/metavalidate --json input.dng
#Validate with sidecar + MakerNotes + C2PA verify status
./build/metavalidate --xmp-sidecar --makernotes --c2pa-verify input.jpgmetavalidate CLI is a thin wrapper over openmeta::validate_file(...).
Machine-readable JSON output includes issue codes suitable for gating, for
example xmp/output_truncated and xmp/invalid_or_malformed_xml_text.
metadump is the general dump/save tool:
#Lossless sidecar
./build/metadump --format lossless input.jpg output.xmp
#Portable sidecar
./build/metadump --format portable --portable-include-existing-xmp input.jpg output.xmp
#Portable sidecar with ExifTool GPS time alias compatibility
./build/metadump --format portable --portable-exiftool-gpsdatetime-alias input.jpg output.xmp
#Portable sidecar + draft C2PA verify scaffold status reporting
./build/metadump --format portable --c2pa-verify --c2pa-verify-backend auto input.jpg output.xmp
#Explicit input / output form
./build/metadump -i input.jpg -o output.xmp
#Extract first embedded preview
./build/metadump --extract-preview --first-only input.jpg preview.jpg
#If multiple previews exist, --out gets auto - suffixed:
#preview_1.jpg, preview_2.jpg, ...
./build/metadump --extract-preview input.arq preview.jpgPortable sidecar note:
exif:GPSTimeStampis emitted as XMP date-time text (YYYY-MM-DDThh:mm:ssZ) only whenGPSDateStampis available; otherwise it is skipped.- Compatibility mode
--portable-exiftool-gpsdatetime-aliasemitsexif:GPSDateTimeinstead ofexif:GPSTimeStamp. - Portable IPTC-IIM mapping covers
dc:*plus selectedphotoshop:*andIptc4xmpCore:*fields (for example city/state/country/headline/credit and location/country-code).
metatransfer is a transfer smoke tool for JPEG/TIFF packaging:
#read -> prepare -> emit simulation
./build/metatransfer input.jpg
#Portable vs lossless transfer-prepared XMP block
./build/metatransfer --format portable input.jpg
./build/metatransfer --format lossless input.jpg
#Write prepared payload bytes for inspection
./build/metatransfer --unsafe-write-payloads --out-dir payloads input.jpg
#Prepare once, emit many times (same bundle)
./build/metatransfer --emit-repeat 100 input.jpg
#Patch prepared EXIF time fields before emit
./build/metatransfer --time-patch DateTimeOriginal="2026:03:06 12:34:56" input.jpg
#Select explicit transfer policy for raw-sensitive families
./build/metatransfer --makernote-policy keep --jumbf-policy drop --c2pa-policy drop input.jpg
#Emit a draft unsigned C2PA invalidation payload for JPEG output
./build/metatransfer --no-exif --no-xmp --no-icc --no-iptc \
--c2pa-policy invalidate input_with_c2pa.jpg
#Append one logical raw JUMBF payload into a prepared JPEG bundle
./build/metatransfer --no-exif --no-xmp --no-icc --no-iptc \
--jpeg-jumbf payload.jumbf input.jpg
#Stage externally signed logical C2PA into a JPEG rewrite flow
./build/metatransfer --no-exif --no-xmp --no-icc --no-iptc \
--jpeg-c2pa-signed signed_c2pa.jumb \
--c2pa-manifest-output manifest.bin \
--c2pa-certificate-chain chain.bin \
--c2pa-key-ref signer-key \
--c2pa-signing-time 2026-03-09T00:00:00Z \
-o output.jpg input_with_c2pa.jpg
#Persist the semantic transfer payload batch for cross-process handoff
./build/metatransfer --dump-transfer-payload-batch payloads.omtpld input.jpg
#Load and inspect one persisted semantic transfer payload batch
./build/metatransfer --load-transfer-payload-batch payloads.omtpld
#Persist one final transfer package batch
./build/metatransfer --dump-transfer-package-batch package.omtpkg input.jpg
#Load and inspect one persisted final transfer package batch
./build/metatransfer --load-transfer-package-batch package.omtpkg
#Plan edit strategy without writing output
./build/metatransfer --mode auto --dry-run input.jpg
#Write edited JPEG output (metadata rewrite mode)
./build/metatransfer --mode metadata_rewrite -o output.jpg input.jpg
#Use separate metadata source and JPEG target stream
./build/metatransfer \
--source-meta source.tif \
--target-jpeg target.jpg \
--mode metadata_rewrite \
-o injected.jpg
#Use separate metadata source and TIFF target stream
./build/metatransfer \
--source-meta source.jpg \
--target-tiff target.tif \
-o injected.tifmetatransfer is a thin CLI wrapper over the public transfer APIs. It uses
prepare_metadata_for_target_file(...) for source read/decode plus
execute_prepared_transfer(...) for time patching, route compile/emit, and
optional JPEG/TIFF edit plan/apply. When --jpeg-jumbf is used, the CLI also
calls append_prepared_bundle_jpeg_jumbf(...) before execute. The core also
exposes
compile_prepared_transfer_execution(...) plus
execute_prepared_transfer_compiled(...) for
prepare once -> compile once -> patch/emit many workflows. When -o is
used, the CLI passes a TransferByteWriter sink into the shared execution
path so edited output can stream directly to disk instead of always
materializing a full output buffer.
Current v1 behavior is:
- JPEG edit output is streamed directly from the shared core path.
- JPEG metadata-only emit can also stream marker bytes directly through the
shared core API.
- TIFF edit output uses the same sink API and only buffers the appended metadata tail; it no longer materializes a second full-file output buffer.
- JPEG XL prepare/emit now shares the same transfer contract for backend
emitter use:
prepare_metadata_for_target(..., TransferTargetFormat::Jxl, ...)currently buildsExif,xml, and boundedjumbbox payloads plus the encoder ICC profile fromMetaStorecompile_prepared_bundle_jxl(...)andemit_prepared_bundle_jxl_compiled(...)provide the sameprepare once -> compile once -> emit manyshape as JPEG/TIFFexecute_prepared_transfer(...)andemit_prepared_transfer_compiled(..., JxlTransferEmitter&)now accept JXL bundlesjxl:icc-profileis emitted throughJxlTransferEmitter::set_icc_profile(...)and stays separate from the JXL box path- file-based prepare can preserve source generic JUMBF payloads and raw OpenMeta draft C2PA invalidation payloads as JXL boxes
- store-only prepare can project decoded non-C2PA
JumbfCborKeyroots into generic JXLjumbboxes when no raw source payload is available - IPTC requested for JXL is projected into the existing
xmlXMP box; OpenMeta does not create a raw IPTC-IIM JXL carrier build_prepared_jxl_encoder_handoff_view(...)is the explicit encoder-side ICC handoff contract for JXL, andbuild_prepared_jxl_encoder_handoff(...)/serialize_prepared_jxl_encoder_handoff(...)add an owned persisted handoff object for cross-process reuse: at most one preparedjxl:icc-profilepayload plus the remaining JXL box countsinspect_prepared_transfer_artifact(...)is now the shared inspect entry point across persisted transfer artifacts: payload batches, package batches, persisted C2PA handoff/signed packages, and persisted JXL encoder handoffs- the JXL compile/emit path now rejects multiple prepared ICC profiles so the encoder handoff and backend execution contracts match
build_prepared_transfer_emit_package(...)pluswrite_prepared_transfer_package(...)can serialize direct JXL box bytes from prepared bundles, andbuild_prepared_transfer_package_batch(...)can materialize those bytes into one owned replay batch- bounded JXL file edit now uses the same package layer: it preserves the signature and non-managed top-level boxes, replaces only the metadata families present in the prepared bundle, and appends the prepared JXL boxes to an existing JXL container file
- unrelated source JXL metadata boxes are preserved, and uncompressed
source
jumbboxes are distinguished as generic JUMBF vs C2PA for that replacement decision - when Brotli support is available, the same distinction is applied to
compressed
brob(realtype=jumb)source boxes before deciding whether to preserve or replace them - the package writer remains box-only, so it still rejects
jxl:icc-profile; ICC remains encoder-only on JXL - CLI/Python
metatransferwrappers now expose this bounded edit path through--target-jxl ... --source-meta ... --output ...when the prepared bundle does not requirejxl:icc-profile - JXL transfer now supports generated draft C2PA invalidation for
content-bound source payloads, plus the bounded external-signer path:
sign-request derivation, binding-byte materialization, signed-payload
validation, staged
jxl:box-jumbapply, and bounded file-helper edit execution
- WebP prepare/emit now uses the same bounded transfer contract:
prepare_metadata_for_target(..., TransferTargetFormat::Webp, ...)currently buildsEXIF,XMP,ICCP, and boundedC2PARIFF metadata chunks fromMetaStorecompile_prepared_bundle_webp(...)andemit_prepared_bundle_webp_compiled(...)provide the sameprepare once -> compile once -> emit manyshape as JPEG/TIFF/JXLexecute_prepared_transfer(...)andemit_prepared_transfer_compiled(..., WebpTransferEmitter&)now accept WebP bundles- IPTC requested for WebP is projected into the existing
XMPchunk; OpenMeta does not create a raw IPTC-IIM WebP carrier - draft OpenMeta invalidation payloads and generated invalidation output
use the
C2PARIFF chunk path build_prepared_transfer_emit_package(...)pluswrite_prepared_transfer_package(...)can serialize direct WebP chunk bytes from prepared bundles, andbuild_prepared_transfer_package_batch(...)can materialize those bytes into one owned replay batch- full WebP file rewrite/edit and signed C2PA rewrite remain follow-up work
- ISO-BMFF metadata-item transfer now uses the same bounded contract for
HEIF/AVIF/CR3targets:prepare_metadata_for_target(..., TransferTargetFormat::{Heif,Avif,Cr3}, ...)currently buildsbmff:item-exif,bmff:item-xmp, boundedbmff:item-jumb, boundedbmff:item-c2pa, andbmff:property-colr-iccpayloads- EXIF is prepared as a BMFF item payload with the 4-byte big-endian
TIFF-offset prefix plus full
Exif\0\0bytes - IPTC requested for BMFF is projected into
bmff:item-xmp; OpenMeta does not create a raw IPTC-IIM BMFF carrier - ICC requested for BMFF uses the bounded property path:
bmff:property-colr-icccarriesu32be('prof') + <icc-profile>as the payload bytes for acolrproperty, not a BMFF metadata item - file-based prepare can preserve source generic JUMBF payloads and raw OpenMeta draft C2PA invalidation payloads as BMFF metadata items
- store-only prepare can project decoded non-C2PA
JumbfCborKeyroots intobmff:item-jumbwhen no raw source payload is available compile_prepared_bundle_bmff(...),emit_prepared_bundle_bmff(...),emit_prepared_bundle_bmff_compiled(...), andemit_prepared_transfer_compiled(..., BmffTransferEmitter&)provide the reusable item/property-emitter path- the shared package-batch persistence/replay layer can own and hand off those stable BMFF item and property payload bytes
- OpenMeta also supports a bounded append-style BMFF edit path:
it preserves existing top-level BMFF boxes, strips prior
OpenMeta-authored metadata-only
metaboxes, and appends one new OpenMeta-authored metadata-onlymetabox carrying the prepared BMFF items/properties - CLI/Python
metatransferwrappers expose both BMFF summaries and this bounded edit path;--target-heif,--target-avif, and--target-cr3now accept--source-meta <path>plus--output <path>for metadata transfer onto an existing BMFF target file - the same bounded BMFF edit contract now also participates in the core /
file-helper C2PA signer path:
build_prepared_c2pa_sign_request(...),build_prepared_c2pa_sign_request_binding(...),validate_prepared_c2pa_sign_result(...), andapply_prepared_c2pa_sign_result(...)can reconstruct rewrite binding from preserved source ranges plus one prepared metadata-onlymetabox and can stage validated signed logical C2PA back asbmff:item-c2pabefore bounded BMFF edit - CLI/Python signer-input options now support JPEG, JXL, and bounded
BMFF targets; the legacy option name
--jpeg-c2pa-signedis kept for compatibility even when the target is JXL or BMFF
TransferProfilenow uses explicitTransferPolicyActionvalues formakernote,jumbf, andc2pa.PreparedTransferBundle::policy_decisionsrecords the resolved per-family transfer decision during prepare.- CLI and Python transfer probes now expose those resolved policy decisions directly, and JPEG edit plans report how many existing APP11 JUMBF/C2PA segments will be removed during rewrite.
- C2PA decisions now expose three explicit fields:
TransferC2paModeTransferC2paSourceKindTransferC2paPreparedOutputso callers can tell whether prepare saw decoded-only C2PA, content-bound raw C2PA, or a raw draft invalidation payload, and whether the prepared output was dropped, preserved raw, or generated as a draft invalidation.
PreparedTransferBundle::c2pa_rewriteis the future-facing signer contract forc2pa=rewrite.- It is separate from
policy_decisions. - Current JPEG, JXL, and bounded BMFF prepare fill
state,source_kind, matched decoded-entry count, existing carrier segment count, and the required signer inputs. - Current rewrite prep also emits
content_binding_chunks, a deterministic sequence that describes the rewrite output before any new C2PA payload is inserted: preserved source ranges plus prepared JPEG segments for JPEG, preserved source ranges plus prepared JXL boxes for JXL, or preserved source ranges plus one prepared metadata-onlymetabox for the bounded BMFF edit path. - Current state is usually
SigningMaterialRequired; it advances toReadyonce an external signed payload is staged back into the bundle.
- It is separate from
build_prepared_c2pa_sign_request(...)derives an explicit external signer request fromPreparedTransferBundle::c2pa_rewrite.- It reports carrier route, manifest label, source-range chunk count, prepared-segment chunk count, and the full content-binding chunk list.
- CLI/Python thin wrappers expose this as
c2pa_sign_request.
build_prepared_c2pa_sign_request_binding(...)reconstructs the exact content-binding byte stream from the request plus the target container bytes.- It fails closed on stale requests, bad source ranges, and block/size mismatches.
- Current bounded targets are JPEG, JXL, and BMFF.
metatransfer --dump-c2pa-bindingandunsafe_transfer_probe(include_c2pa_binding_bytes=True)are thin wrapper entry points for JPEG, JXL, and bounded BMFF targets.
build_prepared_c2pa_handoff_package(...)bundles the signer request and exact content-binding bytes into one public handoff object.- Callers can persist or pass one object to an external signer.
- Wrappers still use the same core helper instead of rebuilding either part on their own.
serialize_prepared_c2pa_handoff_package(...)anddeserialize_prepared_c2pa_handoff_package(...)persist that handoff object as one stable binary package.build_prepared_c2pa_signed_package(...)packages the sign request plus signer material and returned logical payload for a second persisted round-trip object.serialize_prepared_c2pa_signed_package(...)anddeserialize_prepared_c2pa_signed_package(...)persist that signed package.validate_prepared_c2pa_sign_result(...)validates a returned signed logical C2PA payload before bundle mutation.- It reports payload kind, logical payload size, staged carrier size, staged segment count, semantic validation status/reason, and validation errors.
- Current semantic validation requires a manifest, at least one claim,
at least one signature, linked-signature consistency, no unresolved or
ambiguous explicit claim references, exactly one manifest for the
current sign request,
claim_generatorwhen the request requires manifest-builder output, at least one decoded assertion when the request requires content binding, the primary signature linking back to the prepared primary claim under that same content-binding contract, no primary-signature explicit-reference ambiguity under that same request, and no multi-signature drift where the primary claim is referenced by more than one signature under the current sign request, and no extra linked signatures beyond the prepared sign request, manifest/claim/signature projection shape under the prepared manifest contract, and an exact match between the signer-providedmanifest_builder_outputbytes and the primary CBOR manifest payload embedded in the returned signed JUMBF. apply_prepared_c2pa_sign_result(...)uses the same validation path.- Current JPEG validation now also checks that the staged APP11 sequence reconstructs the logical payload byte-for-byte, that APP11 sequence numbers are contiguous, that repeated APP11 C2PA headers stay consistent, and that the logical root type plus BMFF declared size stay internally consistent before final emit/write.
- Current bounded BMFF validation also checks that the staged
bmff:item-c2pacarrier reconstructs the logical payload byte-for-byte before bounded BMFF edit applies it. - Final JPEG emit/write also validates the prepared APP11 C2PA carrier
against the bundle's own C2PA contract.
GeneratedDraftUnsignedInvalidationmust carry a draft invalidation payload.SignedRewritemust carry content-bound C2PA andPreparedTransferBundle::c2pa_rewritemust already beReady.DroppedandNotPresentmay not leave a prepared APP11 C2PA carrier.- Missing required carriers fail before backend bytes are written.
apply_prepared_c2pa_sign_result(...)is the first bundle-level handoff point back from an external signer.- It validates the signer request against the current prepared bundle.
- It requires explicit signer material fields plus a content-bound logical C2PA payload.
- On success it replaces prepared
jpeg:app11-c2pablocks orbmff:item-c2paitems and upgrades the resolved C2PA policy toSignedRewritewithTransferPolicyReason::ExternalSignedPayload. - CLI/Python thin wrappers expose the validation result as
c2pa_stage_validateand the stage result asc2pa_stagefor both JPEG and bounded BMFF signer-input paths.
- Current policy resolution for JPEG/TIFF prepare is:
- MakerNote:
Keepby default,Dropwhen requested,Invalidateresolves toDrop, andRewritecurrently resolves to raw-preserve (Keep) with a warning. - JUMBF:
prepare_metadata_for_target(...)can now project decoded non-C2PAJumbfCborKeyroots into generic JPEG APP11 JUMBF payloads.- The projected path is intentionally bounded: ambiguous numeric map keys and decoded-CBOR bool/simple/sentinel and large-negative fallback forms are rejected, while tagged CBOR values are preserved.
prepare_metadata_for_target_file(...)can preserve source JUMBF payloads for JPEG targets by repacking them into APP11 segments.append_prepared_bundle_jpeg_jumbf(...)is the explicit public helper for adding one logical raw JUMBF payload to a prepared JPEG bundle, andmetatransfer --jpeg-jumbf file.jumbfis the thin CLI path over it.
- C2PA:
c2pa=invalidateon JPEG targets now resolves to a draft unsigned APP11 C2PA invalidation payload instead of drop-only behavior.- The generated draft payload now includes an explicit OpenMeta contract marker and contract version in its CBOR map.
- File-based JPEG prepare can preserve an existing OpenMeta draft
invalidation payload as raw APP11 C2PA (
TransferC2paMode::PreserveRaw). Rewriteresolves toDropwithTransferPolicyReason::SignedRewriteUnavailableuntil re-sign support exists, whilePreparedTransferBundle::c2pa_rewritereports the signer prerequisites that would be needed to perform it.- OpenMeta still does not sign internally, but it can now stage externally signed logical C2PA payloads back into prepared JPEG APP11 carrier blocks after request validation.
build_prepared_c2pa_handoff_package(...)andvalidate_prepared_c2pa_sign_result(...)are the public handoff and pre-stage validation helpers for that external-signer path.metatransfercan now dump a persisted handoff package, dump a persisted signed package, and load a persisted signed package back into the same prepare/validate/apply flow.
- JPEG edit/rewrite now recognizes existing APP11 JUMBF/C2PA carrier
segments.
- Existing C2PA APP11 payloads are dropped automatically when the output metadata changes.
- Existing JUMBF APP11 payloads are removed when the resolved transfer
policy for JUMBF is
Drop.
- C2PA raw preserve still resolves to
Dropbecause signed content-bound metadata has no safe preserve path without re-sign support.
thumdump is preview-only and optimized for batch preview extraction:
#Positional input / output
./build/thumdump input.jpg preview.jpg
#Explicit input / output
./build/thumdump -i input.jpg -o preview.jpg
#Batch mode
./build/thumdump --out-dir previews --first-only input1.jpg input2.cr2
#If multiple previews exist, --out gets auto - suffixed:
#preview_1.jpg, preview_2.jpg, ...
./build/thumdump input.arq preview.jpgOpenMeta tools now default to no hard file-size cap (--max-file-bytes 0).
Resource control is expected to come from parser/decode budgets:
metaread/metavalidate/metadump/metatransfer:--max-payload-bytes,--max-payload-parts--max-exif-ifds,--max-exif-entries,--max-exif-total--max-exif-value-bytes,--max-xmp-input-bytes
metadump/thumdumppreview scan:--max-preview-ifds,--max-preview-total,--max-preview-bytes
This policy surface is intentionally marked draft and may be refined.
- Core EXIF/TIFF decoding:
src/openmeta/exif_tiff_decode.cc - Normalized DNG/RAW CCM query surface:
src/include/openmeta/ccm_query.h,src/openmeta/ccm_query.cc(collect_dng_ccm_fields(...)) with DNG-oriented validation diagnostics (CcmIssue) in warning mode and non-finite numeric field rejection. Current warning taxonomy also includes practical checks such asinvalid_illuminant_code,white_xy_out_of_range, and unusually large matrix-like field counts. - ICC tag interpretation helpers:
src/include/openmeta/icc_interpret.h,src/openmeta/icc_interpret.cc(icc_tag_name(...),interpret_icc_tag(...)fordesc/text/sig/mluc/dtim/view/meas/chrm/sf32/uf32/ui08/ui16/ui32/mft1/mft2/mAB/mBA/XYZ/curv/para, plusformat_icc_tag_display_value(...)for shared CLI/Python rendering) - ISO-BMFF (HEIF/AVIF/CR3) container-derived fields:
src/openmeta/bmff_fields_decode.cc- Emitted during
simple_meta_read(...)asMetaKeyKind::BmffFieldentries. - Current fields:
ftyp.*, primary item properties (meta.primary_item_id,primary.width,primary.height,primary.rotation_degrees,primary.mirrorfrompitm+iprp/ipco ispe/irot/imir+ipma), item-info rows fromiinf/infe(item.info_count,item.id,item.type,item.name,item.content_type,item.content_encoding,item.uri_type; emitted even whenmetahas nopitm, plusprimary.item_type,primary.item_name,primary.content_type,primary.content_encoding,primary.uri_typealiases whenpitmis present), boundediref.*relation fields (ref_type,ref_type_name,from_item_id,to_item_id,edge_count), typed derived relation rows (iref.auxl.*,iref.dimg.*,iref.thmb.*,iref.cdsc.*, and other safe ASCII FourCC relation families), per-type relation counters (iref.<type>.edge_count) and per-type unique source/target counters (iref.<type>.from_item_unique_count,iref.<type>.to_item_unique_count), per-type graph-summary aliases (iref.graph.<type>.edge_count,iref.graph.<type>.from_item_unique_count,iref.graph.<type>.to_item_unique_count), typed relation item summaries (iref.<type>.item_count,iref.<type>.item_id,iref.<type>.item_out_edge_count,iref.<type>.item_in_edge_count), relation-graph summaries (iref.item_count,iref.from_item_unique_count,iref.to_item_unique_count, row-wiseiref.item_id+iref.item_out_edge_count+iref.item_in_edge_count), bounded primary-linked image-role rows (primary.linked_item_role_count, row-wiseprimary.linked_item_id+primary.linked_item_type+primary.linked_item_name+primary.linked_item_rolewheniinf/infedata exists), andauxC-based aux semantics (aux.item_count,aux.item_id,aux.semantic,aux.type,aux.subtype_hex,aux.subtype_kind,aux.subtype_text,aux.subtype_uuid,aux.subtype_u32,aux.subtype_u64,aux.alpha_count,aux.depth_count,aux.disparity_count,aux.matte_count,primary.auxl_count,primary.auxl_semantic,primary.depth_count,primary.depth_item_id,primary.alpha_count,primary.alpha_item_id,primary.disparity_count,primary.disparity_item_id,primary.matte_count,primary.matte_item_id,primary.dimg_count,primary.dimg_item_id,primary.thmb_count,primary.thmb_item_id,primary.cdsc_count,primary.cdsc_item_id, ...). Full multi-image scene modeling beyond that primary-linked role surface is still follow-up work. auxCsubtype interpretation now includesascii_zandu64bekinds in addition to earlier numeric/FourCC/UUID/ASCII forms.- Parsing is intentionally bounded (depth/box count caps) and ignores unknown properties.
- Emitted during
- JUMBF/C2PA decode (draft phase-3):
src/openmeta/jumbf_decode.cc- Routed from container scan blocks tagged as
ContainerBlockKind::Jumbf(BMFFjumb/C2PA hints and JXLjumbboxes). - Emits structural fields as
MetaKeyKind::JumbfField(box.*,c2pa.*) and decoded CBOR keys asMetaKeyKind::JumbfCborKey(*.cbor.*). - Current CBOR path supports bounded definite and indefinite forms, with
composite-key fallback naming (
k{map_index}_{ major}) and broader scalar decode coverage (simple values + half/float/double bit-preserving paths). - Draft semantic projection emits stable
c2pa.semantic.*fields (manifest_present,active_manifest_present,active_manifest_count,active_manifest.prefix,claim_present,assertion_present,ingredient_present,signature_present,assertion_key_hits,ingredient_key_hits,cbor_key_count,signature_count,claim_generatorwhen ASCII-safe), plus draft per-claim fields (claim_count,assertion_count,ingredient_count,claim.{i}.prefix,claim.{i}.assertion_count,claim.{i}.key_hits,claim.{i}.ingredient_count,claim.{i}.signature_count,claim.{i}.signature_key_hits,claim.{i}.claim_generatorwhen ASCII-safe), per-assertion fields (claim.{i}.assertion.{j}.prefix,claim.{i}.assertion.{j}.key_hits), and per-ingredient fields (claim.{i}.ingredient.{j}.prefix,claim.{i}.ingredient.{j}.key_hits,claim.{i}.ingredient.{j}.title,claim.{i}.ingredient.{j}.relationship,claim.{i}.ingredient.{j}.thumbnail_urlwhen ASCII-safe), plus bounded ingredient summary counts (ingredient_relationship_count,ingredient_thumbnail_url_count,ingredient_claim_count,ingredient_claim_with_signature_count,ingredient_claim_referenced_by_signature_count, per-claim linked-ingredient summary fields such asclaim.{i}.linked_ingredient_signature_count,claim.{i}.linked_ingredient_title_count,claim.{i}.linked_ingredient_relationship_count,claim.{i}.linked_ingredient_thumbnail_url_count,claim.{i}.linked_ingredient_relationship_kind_count, and explicit-reference split variants likeclaim.{i}.linked_ingredient_explicit_reference_title_count, plus aggregate linked-claim topology counts likeingredient_linked_claim_countandingredient_linked_claim_direct_source_count,ingredient_manifest_count,ingredient_signature_count,ingredient_linked_signature_count,ingredient_linked_direct_claim_count,ingredient_linked_cross_claim_count,ingredient_linked_signature_direct_source_count,ingredient_linked_signature_cross_source_count,ingredient_linked_signature_mixed_source_count,ingredient_linked_signature_direct_title_count,ingredient_linked_signature_cross_title_count,ingredient_linked_signature_direct_relationship_count,ingredient_linked_signature_cross_relationship_count,ingredient_linked_signature_direct_thumbnail_url_count,ingredient_linked_signature_cross_thumbnail_url_count,ingredient_linked_signature_title_count,ingredient_linked_signature_relationship_count,ingredient_linked_signature_relationship_kind_count,ingredient_linked_signature_relationship.<value>_count,ingredient_linked_signature_thumbnail_url_count,ingredient_linked_signature_explicit_reference_direct_title_count,ingredient_linked_signature_explicit_reference_cross_title_count,ingredient_linked_signature_explicit_reference_title_count,ingredient_linked_signature_explicit_reference_relationship_count,ingredient_linked_signature_explicit_reference_relationship_kind_count,ingredient_linked_signature_explicit_reference_relationship.<value>_count,ingredient_linked_signature_explicit_reference_thumbnail_url_count,ingredient_linked_signature_explicit_reference_direct_claim_count,ingredient_linked_signature_explicit_reference_cross_claim_count,ingredient_linked_signature_explicit_reference_direct_source_count,ingredient_linked_signature_explicit_reference_cross_source_count,ingredient_linked_signature_explicit_reference_mixed_source_count, plus corresponding..._unresolved_*and..._ambiguous_*split aggregates,ingredient_explicit_reference_signature_count,ingredient_explicit_reference_unresolved_signature_count,ingredient_explicit_reference_ambiguous_signature_count,claim.{i}.ingredient_relationship_count,claim.{i}.ingredient_thumbnail_url_count,manifest.{i}.ingredient_relationship_count,manifest.{i}.ingredient_thumbnail_url_count,manifest.{i}.ingredient_claim_count) and path-sanitized relationship alias counts such asingredient_relationship.parentOf_count, plus per-manifest active-state fields (manifest.{i}.is_active,manifest.{i}.ingredient_count), plus draft per-claim signature fields (claim.{i}.signature.{k}.prefix,claim.{i}.signature.{k}.key_hits,claim.{i}.signature.{k}.algorithmwhen available), plus draft per-signature fields (signature_count,signature_key_hits,signature.{k}.prefix,signature.{k}.key_hits,signature.{k}.algorithmwhen available,signature.{k}.reference_key_hits,signature.{k}.linked_claim_count,signature.{k}.linked_ingredient_claim_count,signature.{k}.linked_direct_ingredient_claim_count,signature.{k}.linked_cross_ingredient_claim_count,signature.{k}.linked_ingredient_title_count,signature.{k}.linked_ingredient_relationship_count,signature.{k}.linked_ingredient_relationship_kind_count,signature.{k}.linked_ingredient_relationship.<value>_count,signature.{k}.linked_ingredient_thumbnail_url_count,signature.{k}.direct_claim_has_ingredients,signature.{k}.cross_claim_link_count,signature.{k}.explicit_reference_present,signature.{k}.explicit_reference_resolved_claim_count,signature.{k}.explicit_reference_unresolved,signature.{k}.explicit_reference_ambiguous,signature.{k}.linked_claim.{m}.prefix), plus reference-link counters (reference_key_hits,cross_claim_link_count,explicit_reference_signature_count,explicit_reference_unresolved_signature_count,explicit_reference_ambiguous_signature_count,claim.{i}.referenced_by_signature_count), and linkage counters (signature_linked_count,signature_orphan_count). - Draft verify scaffold (
c2pa.verify.*) now includes:- signature-shape validation (
invalid_signature) for malformed payloads; - OpenSSL-backed cryptographic verification (
verified/verification_failed) when a signature entry provides algorithm + signing input + public key material (public_key_der/public_key_pemorcertificate_der). - COSE_Sign1 support (array or embedded CBOR byte-string forms): extracts
algfrom protected headers, reconstructs Sig_structure signing bytes when payload is present, extractsx5chainfrom unprotected headers, and accepts raw ECDSA signatures (r||s) by converting to DER for OpenSSL. - detached payload resolution (
payload=null) using explicit reference-linked candidates first (for exampleclaims[n]/ claim-label references in decoded claim/signature fields, scalar index references, and indexed array-element reference keys such asclaimRef[0]), then including plural reference-key variants (references,refs,claim_references) plus hyphenated variants (claim-reference,claim-uri,claim-ref-index), nested URI-like map fields such asreferences[].href/references[].link, query-style index tokens in URI text (claim-index=...,claim_ref=...), and percent-encoded URI/label forms where present. Candidate ordering is deterministic with sorted index-like references resolved before sorted label-based references, then best-effort fallback probing via claim bytes, single-claimclaims[*]arrays, nearby/nested claim JUMBF boxes, and additional cross-manifest candidates. Current tests include conflicting mixed references and multi-claim/multi-signature cross-manifest precedence cases, nestedreferences[]map forms, duplicate overlapping explicit references, unresolved explicit-reference no-fallback behavior, conflict/consistentindex + claim_reference + hrefnested-map ambiguity/consistency cases, and percent-encoded query-index URI variants. - draft profile checks (
profile_status/profile_reason) from decodedc2pa.semantic.*shape fields (manifest/claim/signature linkage); - draft certificate trust checks (
chain_status/chain_reason) whencertificate_deris present (certificate parse, time validity, and OpenSSL trust-store verification). Full C2PA/COSE manifest binding and policy validation is still pending.
- signature-shape validation (
- Routed from container scan blocks tagged as
- GeoTIFF GeoKey decoding (derived keys):
src/openmeta/geotiff_decode.cc - Vendor MakerNote decoders:
src/openmeta/exif_makernote_*.cc(Canon, Nikon, Sony, Olympus, Pentax, Casio, Panasonic, Kodak, Ricoh, Samsung, FLIR, etc.) - Shared internal-only helpers:
src/openmeta/exif_tiff_decode_internal.h(not installed; used to keep vendor logic out of the public API) - Tests:
tests/makernote_decode_test.ccandtests/jumbf_decode_test.cc
When adding or changing MakerNote code, prefer extending the vendor files and keeping the EXIF/TIFF core container-agnostic. Add/adjust a unit test for any new subtable or decode path.
Internal helper conventions (used by vendor decoders):
read_classic_ifd_entry(...)+ClassicIfdEntry: parse a single 12-byte classic TIFF IFD entry.resolve_classic_ifd_value_ref(...)+ClassicIfdValueRef: compute the value location/size for a classic IFD entry (inline vs out-of-line), usingMakerNoteLayout+OffsetPolicy.MakerNoteLayout+OffsetPolicy: makes "value offsets are relative to X" explicit for vendor formats.OffsetPolicysupports both the common unsigned base (default) and a signed base for vendors that require it (eg Canon).ExifContext: a small, decode-time cache for frequently accessed EXIF values (avoids repeated linear scans ofstore.entries()).- MakerNote tag-name tables are generated from
registry/exif/makernotes/*.jsonland looked up via binary search (exif_makernote_tag_names.cc). - Canonical EXIF names stay context-free via
exif_tag_name(...). When corpus compatibility requires a decode-time alias split, stamp the variant on theEntryprovenance and resolve it only on explicit display surfaces throughexif_entry_name(..., ExifTagNamePolicy::ExifToolCompat). - Photoshop IRB stays lossless at the raw-resource layer (
PhotoshopIrb). Add interpreted IRB fields only for fixed-layout resources and emit them as separatePhotoshopIrbFieldentries instead of weakening the raw payload surface. The current bounded interpreted subset includesResolutionInfo,VersionInfo,PrintFlags,EffectiveBW,TargetLayerID,LayersGroupInfo,JPEG_Quality,CopyrightFlag,URL,GlobalAngle,Watermark,ICC_Untagged,EffectsVisible,IDsBaseValue,IndexedColorTableCount,TransparentIndex,GlobalAltitude,SliceInfo,WorkflowURL,URL_List,IPTCDigest,PrintScaleInfo,PixelInfo,LayerSelectionIDs,LayerGroupsEnabledID,ChannelOptions,PrintFlagsInfo, andClippingPathName. - Legacy 8-bit Photoshop text stays opt-in and explicit. The IRB decoder
exposes a bounded
PhotoshopIrbStringCharsetpolicy and currently uses it only forClippingPathName, defaulting to Latin for ExifTool-compatible behavior instead of guessing from bytes. ChannelOptionsstays bounded and explicit: emit one count row, then oneChannelIndexrow plus the channel fields in stable order for each 13-byte record instead of inventing dynamic field names.PrintFlagsInfois bounded to the stableexiv2-documented 10-byte layout: version, center-crop flag, bleed-width value, and bleed-width scale.- GeoTIFF key-name table is generated from
registry/geotiff/keys.jsonland looked up via binary search (geotiff_key_names.cc).
Requirements:
- A GoogleTest package that provides
GTest::gtest_main(orGTest::Main).
Note: if your GoogleTest was built against libc++ (common with Clang),
build OpenMeta against the same C++ standard library. Otherwise you may see
link errors involving std::__1 vs std::__cxx11.
Build + run:
cmake -S . -B build-tests -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/mnt/f/UBSd \
-DOPENMETA_BUILD_TESTS=ON
cmake --build build-tests
ctest --test-dir build-tests --output-on-failureOptional CLI integration test for preview index suffixing:
cmake -S . -B build-tests -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/mnt/f/UBSd \
-DOPENMETA_BUILD_TESTS=ON \
-DOPENMETA_MULTI_PREVIEW_SAMPLE=/path/to/file_with_multiple_previews
cmake --build build-tests
ctest --test-dir build-tests -R openmeta_cli_preview_index --output-on-failureIf OPENMETA_MULTI_PREVIEW_SAMPLE is not set (or the file is missing),
openmeta_cli_preview_index is skipped.
Fast public smoke gate for metavalidate (self-contained, no corpus needed):
cmake --build build-tests --target openmeta_gate_metavalidate_smoke
ctest --test-dir build-tests -R openmeta_cli_metavalidate_smoke --output-on-failureFast public smoke gate for metaread safe-text placeholder behavior:
cmake --build build-tests --target openmeta_gate_metaread_safe_text_smokeFast public smoke gate for metatransfer thin wrapper behavior:
cmake --build build-tests --target openmeta_gate_metatransfer_smoke
ctest --test-dir build-tests -R openmeta_cli_metatransfer_smoke --output-on-failureFast public smoke gate for Python openmeta.transfer_probe thin wrapper
behavior (requires -DOPENMETA_BUILD_PYTHON=ON):
cmake --build build-tests --target openmeta_gate_python_transfer_probe_smoke
ctest --test-dir build-tests -R openmeta_python_transfer_probe_smoke --output-on-failureFast public smoke gate for Python openmeta.python.metatransfer edit mode
behavior (requires -DOPENMETA_BUILD_PYTHON=ON):
cmake --build build-tests --target openmeta_gate_python_metatransfer_edit_smoke
ctest --test-dir build-tests -R openmeta_python_metatransfer_edit_smoke --output-on-failureCoverage note:
- Public tree tests focus on deterministic unit/fuzz/smoke behavior.
- Corpus-scale compare/baseline workflows are external to the public tree and should be run in your CI/release validation pipeline.
Requirements:
- Clang with libFuzzer support.
Notes:
- On Linux, Clang's bundled libFuzzer runtime is typically built against
libstdc++. WhenOPENMETA_USE_LIBCXX=ON, OpenMeta keeps tests/tools onlibc++but builds fuzz targets againstlibstdc++to match libFuzzer. - libFuzzer treats metadata as untrusted input; always run under sanitizers and with explicit size limits.
Build + run (example 5s smoke run):
cmake -S . -B build-fuzz -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DOPENMETA_BUILD_FUZZERS=ON
cmake --build build-fuzz
ASAN_OPTIONS=detect_leaks=0 ./build-fuzz/openmeta_fuzz_exif_tiff_decode -max_total_time=5If you pass corpus directories to libFuzzer, it treats the first directory as the main corpus and may add/reduce files there. To avoid modifying your seed corpus directories, use an empty output directory first:
mkdir -p build-fuzz/_corpus_out
ASAN_OPTIONS=detect_leaks=0 ./build-fuzz/openmeta_fuzz_container_scan \
build-fuzz/_corpus_out \
/path/to/seed-corpus-a /path/to/seed-corpus-b \
-runs=1000Public seed corpus is available in-tree:
mkdir -p build-fuzz/_corpus_out
ASAN_OPTIONS=detect_leaks=0 ./build-fuzz/openmeta_fuzz_container_scan \
build-fuzz/_corpus_out \
tests/fuzz/corpus/container_scan \
-runs=1000The container_scan seed set includes BMFF iloc method-2 edge cases:
- valid
irefv1 (32-bititem-id) resolution, - missing
irefmapping, - out-of-range explicit
extent_index, idx_size=0extent/reference mismatch fallback behavior.
Requirements:
- A FuzzTest package that provides
fuzztest::fuzztestandfuzztest::fuzztest_gtest_main.
Build + run:
cmake -S . -B build-fuzztest -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/mnt/f/UBSd \
-DOPENMETA_BUILD_FUZZTEST=ON -DOPENMETA_FUZZTEST_FUZZING_MODE=ON
cmake --build build-fuzztest
ASAN_OPTIONS=detect_leaks=0 ./build-fuzztest/openmeta_fuzztest_metastore --list_fuzz_tests
ASAN_OPTIONS=detect_leaks=0 ./build-fuzztest/openmeta_fuzztest_metastore --fuzz=MetaStoreFuzz.meta_store_op_stream --fuzz_for=5sRequirements:
- Python
>= 3.9+ development headers/libraries nanobindinstalled as a CMake package (findable viaCMAKE_PREFIX_PATH)
Build:
cmake -S . -B build-py -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/mnt/f/UBS \
-DOPENMETA_BUILD_PYTHON=ON -DOPENMETA_BUILD_TOOLS=OFF
cmake --build build-py
PYTHONPATH=build-py/python python3 -c "import openmeta; print(openmeta.read('file.jpg').entry_count)"Notes:
openmeta.read(...)releases the Python GIL while doing file I/O and decode, so it can be called from multiple Python threads in parallel (useful for corpus comparisons).openmeta.validate(...)is the library-backed validation API used byopenmeta.python.metavalidate; it returns decode/CCM issue summaries without Python-side validation logic.- Python bindings are thin wrappers over C++ decode logic. Resource/safety
limits should be configured via
openmeta.ResourcePolicyand passed toopenmeta.read(...).
Example policy usage:
PYTHONPATH=build-py/python python3 - <<'PY'
import openmeta
policy = openmeta.ResourcePolicy()
policy.max_file_bytes = 0
policy.exif_limits.max_total_entries = 200000
doc = openmeta.read("file.jpg", policy=policy)
print(doc.entry_count)
PYC++ policy setup:
#include "openmeta/resource_policy.h"
openmeta::OpenMetaResourcePolicy policy
= openmeta::recommended_resource_policy();
policy.jumbf_limits.max_box_depth = 24; // optional overrideJUMBF preflight depth estimate (before full decode):
#include "openmeta/jumbf_decode.h"
const openmeta::JumbfStructureEstimate est
= openmeta::measure_jumbf_structure(bytes, policy.jumbf_limits);
if (est.status == openmeta::JumbfDecodeStatus::LimitExceeded) {
// reject or route to stricter handling
}Other preflight estimate entry points follow the same limit model:
#include "openmeta/container_scan.h"
#include "openmeta/exif_tiff_decode.h"
#include "openmeta/exr_decode.h"
#include "openmeta/icc_decode.h"
#include "openmeta/iptc_iim_decode.h"
#include "openmeta/jumbf_decode.h"
#include "openmeta/photoshop_irb_decode.h"
#include "openmeta/xmp_decode.h"
const openmeta::ScanResult scan_est
= openmeta::measure_scan_auto(file_bytes);
const openmeta::ExifDecodeResult exif_est
= openmeta::measure_exif_tiff(exif_bytes, exif_options);
const openmeta::XmpDecodeResult xmp_est
= openmeta::measure_xmp_packet(xmp_bytes, xmp_options);
const openmeta::IccDecodeResult icc_est
= openmeta::measure_icc_profile(icc_bytes, icc_options);
const openmeta::IptcIimDecodeResult iptc_est
= openmeta::measure_iptc_iim(iptc_bytes, iptc_options);
const openmeta::PhotoshopIrbDecodeResult irb_est
= openmeta::measure_photoshop_irb(irb_bytes, irb_options);
const openmeta::ExrDecodeResult exr_est
= openmeta::measure_exr_header(exr_bytes, exr_options);
const openmeta::JumbfDecodeResult jumbf_est
= openmeta::measure_jumbf_payload(jumbf_bytes, jumbf_options);Example scripts (repo tree):
PYTHONPATH=build-py/python python3 -m openmeta.python.openmeta_stats file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metaread file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metavalidate file.dng
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump file.jpg output.xmp
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump --format portable file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump --format portable --portable-exiftool-gpsdatetime-alias file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump --format portable --c2pa-verify --c2pa-verify-backend auto file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metadump --format portable --portable-include-existing-xmp --xmp-sidecar file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer file.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --target-jpeg target.jpg -o edited.jpg source.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --target-tiff target.tif --dry-run source.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --jpeg-c2pa-signed signed_c2pa.jumb --c2pa-manifest-output manifest.bin --c2pa-certificate-chain chain.bin --c2pa-key-ref signer-key --c2pa-signing-time 2026-03-09T00:00:00Z -o edited.jpg input_with_c2pa.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --c2pa-policy rewrite --dump-c2pa-handoff handoff.omc2ph input_with_c2pa.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --jpeg-c2pa-signed signed_c2pa.jumb --c2pa-manifest-output manifest.bin --c2pa-certificate-chain chain.bin --c2pa-key-ref signer-key --c2pa-signing-time 2026-03-09T00:00:00Z --dump-c2pa-signed-package signed.omc2ps input_with_c2pa.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --load-c2pa-signed-package signed.omc2ps --target-jpeg target.jpg -o edited.jpg input_with_c2pa.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --dump-transfer-payload-batch payloads.omtpld input.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --load-transfer-payload-batch payloads.omtpld
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --dump-transfer-package-batch package.omtpkg input.jpg
PYTHONPATH=build-py/python python3 -m openmeta.python.metatransfer --load-transfer-package-batch package.omtpkgRequirements:
scikit-build-coreinstalled in your Python environment.- A wheel builder:
pip(recommended) oruv(works even if your venv has nopip).
Build:
python3 -m pip wheel . -w dist --no-depsOr using uv:
uv --no-cache build --wheel --no-build-isolation -o dist -p "$(command -v python3)" .After installing the wheel, example modules are available as:
python3 -m openmeta.python.openmeta_stats file.jpg
python3 -m openmeta.python.metaread file.jpg
python3 -m openmeta.python.metatransfer file.jpgOr via CMake:
cmake -S . -B build-wheel -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DOPENMETA_BUILD_WHEEL=ON \
-DOPENMETA_PYTHON_EXECUTABLE=/path/to/venv/bin/python3
cmake --build build-wheel --target openmeta_wheelWhen OPENMETA_BUILD_WHEEL=ON, cmake --install also builds a wheel and copies
it into ${CMAKE_INSTALL_PREFIX}/share/openmeta/wheels (and also copies the
Python helper scripts metaread.py, metavalidate.py, metadump.py, metatransfer.py,
and openmeta_stats.py
into the same directory):
cmake --install build-wheel --prefix /tmp/openmeta-install
ls /tmp/openmeta-install/share/openmeta/wheelsIf you are building offline (or want strict control of the build environment),
install scikit-build-core into your Python environment and enable:
-DOPENMETA_WHEEL_NO_BUILD_ISOLATION=ON.
Interop adapter APIs for ASF integration targets:
openmeta/interop_export.h: shared traversal and naming styles (Canonical,XmpPortable,Oiio).openmeta/oiio_adapter.h: flat OIIO-style name/value export.openmeta/ocio_adapter.h: deterministic OCIO-style metadata tree export.openmeta/exr_adapter.h: EXR-native per-part attribute export.
Current Python binding entry points:
Document.export_names(style=..., include_makernotes=...)Document.oiio_attributes(...)Document.unsafe_oiio_attributes(...)Document.oiio_attributes_typed(...)Document.unsafe_oiio_attributes_typed(...)Document.ocio_metadata_tree(...)Document.unsafe_ocio_metadata_tree(...)Document.dump_xmp_sidecar(format=...)
Current C++ adapter entry points:
openmeta/oiio_adapter.h:- safe API:
collect_oiio_attributes_safe(..., InteropSafetyError*) - unsafe API:
collect_oiio_attributes(...) collect_oiio_attributes(..., const OiioAdapterRequest&)(stable flat request API)collect_oiio_attributes(..., const OiioAdapterOptions&)(advanced/legacy shape)- safe typed API:
collect_oiio_attributes_typed_safe(..., InteropSafetyError*) - unsafe typed API:
collect_oiio_attributes_typed(...) collect_oiio_attributes_typed(..., const OiioAdapterRequest&)(typed values)collect_oiio_attributes_typed(..., const OiioAdapterOptions&)(typed values)- typed payload model:
OiioTypedValue/OiioTypedAttribute - prepared transfer bridge:
collect_oiio_transfer_payload_views(...)returns one zero-copy payload list over aPreparedTransferBundlefor host/plugin write integrations - owned transfer bridge:
build_oiio_transfer_payload_batch(...)copies that same payload contract into one stable owned batch for caching or cross-layer handoff - persisted payload bridge:
collect_oiio_transfer_payload_views(const PreparedTransferPayloadBatch&, ...)andreplay_oiio_transfer_payload_batch(...)consume the same semantic payload contract afterserialize_prepared_transfer_payload_batch(...)/deserialize_prepared_transfer_payload_batch(...)
- safe API:
openmeta/exr_adapter.h:build_exr_attribute_batch(...)exports one owned EXR-native attribute batch fromMetaStore- the batch carries:
part_index,name,type_name,valuebytes, andis_opaque build_exr_attribute_part_spans(...)groups the batch into deterministic contiguous per-part spansbuild_exr_attribute_part_views(...)exposes zero-copy grouped per-part views over the same batchreplay_exr_attribute_batch(...)replays the grouped batch through explicit host callbacks- unlike the prepared JPEG/TIFF/JXL transfer path, this bridge is
store-based because typed
MetaValueentries must be re-encoded into EXR attribute bytes
Python typed behavior:
Document.oiio_attributes(...)is safe-by-default and raises on unsafe raw byte payloads; useDocument.unsafe_oiio_attributes(...)for legacy/raw fallback output.Document.oiio_attributes_typed(...)decodes text values to Pythonstrin safe mode and raises on unsafe/invalid text bytes.Document.unsafe_oiio_attributes_typed(...)returns raw text bytes for explicit unsafe workflows.Document.ocio_metadata_tree(...)is safe-by-default and raises on unsafe raw byte payloads; useDocument.unsafe_ocio_metadata_tree(...)for legacy/raw fallback output.openmeta/ocio_adapter.h:- safe API:
build_ocio_metadata_tree_safe(..., InteropSafetyError*) - unsafe API:
build_ocio_metadata_tree(...) build_ocio_metadata_tree(..., const OcioAdapterRequest&)(stable flat request API)build_ocio_metadata_tree(..., const OcioAdapterOptions&)(advanced/legacy shape)
- safe API:
Current C++ sidecar entry points:
openmeta/xmp_dump.h:dump_xmp_sidecar(..., const XmpSidecarRequest&)(stable flat request API)dump_xmp_sidecar(..., const XmpSidecarOptions&)(advanced/legacy shape)
Draft C++ transfer entry points (prepare/emit scaffold):
openmeta/metadata_transfer.h:PreparedTransferBundle(target-ready payload container)- backend emitter contracts:
JpegTransferEmitterTiffTransferEmitterJxlTransferEmitterWebpTransferEmitterExrTransferEmitter
prepare_metadata_for_target(..., PreparedTransferBundle*)currently prepares JPEG/TIFF transfer blocks plus the current bounded JXL/WebP/BMFF transfer set: EXIF APP1 (JPEG) / JXLExif/ WebPEXIF/ BMFF EXIF item, XMP (JPEG APP1 / TIFF tag 700 / JXLxmlbox / WebPXMPchunk / BMFF XMP item), ICC (JPEG APP2 / TIFF tag 34675 / JXL encoder ICC profile / WebPICCPchunk), IPTC (JPEG APP13 / TIFF tag 33723 or projected into JXL/WebP/BMFF XMP), bounded JUMBF/C2PA routes, with explicit warnings for unsupported/skipped entries.emit_prepared_bundle_jpeg(...)is implemented for route-based JPEG marker emission (jpeg:appN...,jpeg:com).emit_prepared_bundle_tiff(...)is implemented for route-based TIFF tag emission (tiff:ifd-exif-app1,tiff:tag-700-xmp,tiff:tag-34675-icc,tiff:tag-33723-iptc) and commit hook.- Current CLI TIFF rewrite path supports classic TIFF (little- and
big-endian) for ExifIFD materialization (
tiff:ifd-exif-app1). compile_prepared_bundle_jpeg(...)+emit_prepared_bundle_jpeg_compiled(...)provide route-compile + reusable emit plan for high-throughput "prepare once, emit many" use.compile_prepared_bundle_tiff(...)+emit_prepared_bundle_tiff_compiled(...)provide the same reusable route-compile emit plan for TIFF tag emission.apply_time_patches(...)applies fixed-width in-place updates overbundle.time_patch_map(for example EXIFDateTime*,SubSec*,OffsetTime*, GPS date/time slots) without full re-prepare.- TIFF edit path mirrors JPEG edit path:
plan_prepared_bundle_tiff_edit(...)apply_prepared_bundle_tiff_edit(...)(classic TIFF rewrite for prepared EXIF/XMP/ICC/IPTC updates).
- Writer/sink edit path is available for both targets:
TransferByteWriterSpanTransferByteWriterPreparedTransferExecutionPlanTimePatchViewwrite_prepared_bundle_jpeg(...)write_prepared_bundle_jpeg_compiled(...)write_prepared_bundle_jpeg_edit(...)write_prepared_bundle_tiff_edit(...)apply_time_patches_view(...)compile_prepared_transfer_execution(...)execute_prepared_transfer_compiled(...)write_prepared_transfer_compiled(...)emit_prepared_transfer_compiled(..., JpegTransferEmitter&)emit_prepared_transfer_compiled(..., TiffTransferEmitter&)ExecutePreparedTransferOptions::emit_output_writerExecutePreparedTransferOptions::edit_output_writerJPEG can stream either metadata-only emit bytes or edited output directly. TIFF edit output streams original input plus a planned metadata tail, avoiding a temporary full-file rewrite buffer.
prepare_metadata_for_target_file(...)provides the file-levelread/decode -> prepare bundlestep.execute_prepared_transfer(...)runs the sharedtime_patch -> compile -> emit -> optional editflow on an already prepared bundle.compile_prepared_transfer_execution(...)compiles a reusable execution plan that stores target-specific route mapping plus emit policy.build_prepared_transfer_adapter_view(...)flattens the same compiled route mapping into one target-neutral operation list for JPEG/TIFF/JXL/WebP/BMFF host integrations.emit_prepared_transfer_adapter_view(...)replays that compiled view into one generic host sink without route parsing.apply_time_patches_view(...)accepts non-owning patch spans for per-frame patching without owned update buffers.execute_prepared_transfer_compiled(...)runs the same sharedtime_patch -> emit -> optional editflow using a precompiled execution plan.write_prepared_transfer_compiled(...)is the narrow encoder-integration helper forprepare once -> compile once -> patch -> writeworkflows.SpanTransferByteWriteris the fixed-buffer adapter for encoder paths that want preallocated output memory and deterministic overflow reporting before any JPEG marker bytes are written.PreparedTransferPackagePlanis the shared final-output packaging layer for current JPEG/TIFF rewrite paths plus direct JPEG/JXL/WebP/BMFF emit packaging.TransferPackageChunkKind::SourceRangecopies bytes from the original input stream.TransferPackageChunkKind::PreparedTransferBlockserializes one prepared block directly for JPEG, JXL, WebP, or BMFF targets.TransferPackageChunkKind::PreparedJpegSegmentinjects one prepared JPEG marker segment from the bundle.TransferPackageChunkKind::InlineBytescarries deterministic generated bytes such as the patched TIFF IFD0 offset or appended TIFF tail.
PreparedTransferPackageBatchis the owned replay form of that package layer. It materializes each package chunk into stable bytes so host code can cache or hand off the final metadata package without retaining the original input stream or prepared bundle storage.serialize_prepared_transfer_package_batch(...)anddeserialize_prepared_transfer_package_batch(...)persist that owned batch for cross-process or cross-layer replay.collect_prepared_transfer_payload_views(...)andbuild_prepared_transfer_payload_batch(...)provide the matching target-neutral semantic surface one level earlier, directly over prepared bundles.serialize_prepared_transfer_payload_batch(...)anddeserialize_prepared_transfer_payload_batch(...)persist that semantic payload batch when a host wants cross-process handoff before final package materialization.collect_prepared_transfer_package_views(...)is the target-neutral semantic view above that persisted batch. It exposes semantic package chunks (Exif,Xmp,Icc,Iptc,Jumbf,C2pa, orUnknown) without pushing route parsing into host adapters.replay_prepared_transfer_package_batch(...)is the matching target-neutral callback replay path over the same persisted batch.collect_oiio_transfer_package_views(...)is the first host bridge above that persisted batch: it now maps the target-neutral semantic package view into OIIO-oriented names, so the host no longer depends on the original prepared bundle lifetime or route parsing.collect_oiio_transfer_payload_views(...)andbuild_oiio_transfer_payload_batch(...)now sit on top of the generic semantic payload layer rather than re-classifying prepared blocks inside the OIIO adapter.replay_oiio_transfer_package_batch(...)is the higher-level consume path above that same persisted batch, replaying semantic package chunks through explicit host callbacks in deterministic output order.PreparedTransferAdapterViewis the parallel adapter-facing surface for host integrations that want explicit per-block operations without route parsing.collect_oiio_transfer_payload_views(...)is the first thin host-facing bridge on top of that adapter view, exposing one zero-copy OIIO-oriented semantic payload list without adding new transfer core logic.build_oiio_transfer_payload_batch(...)is the owned form of that bridge when the host needs payload lifetime independent from the prepared bundle.build_exr_attribute_batch(...),build_exr_attribute_part_spans(...),build_exr_attribute_part_views(...), andreplay_exr_attribute_batch(...)are the EXR-native bridge for OpenEXR/OIIO header-attribute workflows. They stay outside thePreparedTransferBundlepath because EXR metadata is attribute-native, not block-native.build_prepared_transfer_emit_package(...),build_prepared_transfer_adapter_view(...),emit_prepared_transfer_adapter_view(...),build_prepared_bundle_jpeg_package(...),build_prepared_bundle_tiff_package(...), andwrite_prepared_transfer_package(...)expose that shared contract.
emit_prepared_transfer_compiled(..., TiffTransferEmitter&)is the intended TIFF hot path; TIFF does not expose a metadata-only byte-writer emit contract.execute_prepared_transfer_file(...)wraps the fullread/decode -> prepare -> executeflow and is now the main thin-wrapper entry point for CLI/Python tooling.
Python transfer entry point:
openmeta.transfer_probe(...)(safe):- calls the same file-level transfer execution API as the CLI, returning read/prepare/compile/emit summaries and prepared block routes/sizes;
- supports
time_patches={Field: "Value" | b"..."}with shared C++ patch logic insideexecute_prepared_transfer(...); - exposes
time_patch_*summary fields (time_patch_status_name,time_patch_patched_slots, ...); - if
include_payloads=True, returnsoverall_status=unsafe_datawitherror_code=unsafe_payloads_forbidden.
openmeta.unsafe_transfer_probe(...):- same probe contract, but allows
include_payloads=Trueand returns raw payload bytes (bytes) inblocks[i].payload. - intended for explicit raw/unsafe workflows only.
- same probe contract, but allows
Transfer probe contract hardening (stable machine fields):
overall_status,overall_status_nameerror_stage(none|api|file|prepare|emit)error_code,error_message- stage-specific stable code enums/strings:
- file:
PrepareTransferFileCode/file_code_name - prepare:
PrepareTransferCode/prepare_code_name - emit:
EmitTransferCode/emit_code_name
- file:
Current adapter/name-policy behavior:
ExportNamePolicy::ExifToolAliasapplies compatibility aliases for interop-name parity workflows.ExportNamePolicy::Specpreserves spec/native names.- OIIO adapter keeps numeric unknown names (for example
Exif_0x....) even when value formatting is empty, and keepsExif:MakerNotein spec mode. - When DNG context is detected (
DNGVersionpresent in the same IFD), DNG color/CCM tags are exported with dedicated adapter namespaces:dng:*(portable) andDNG:*(OIIO). - ICC entries are exported with adapter-friendly names:
icc:*(portable) andICC:*(OIIO), alongside canonicalicc:header:*/icc:tag:*naming.
Adapter-focused tests (public tree):
cmake --build build-tests --target openmeta_tests
./build-tests/openmeta_tests --gtest_filter='InteropExport.*:OiioAdapter.*:OcioAdapter.*'
./build-tests/openmeta_tests --gtest_filter='CrwCiffDecode.*'Notes:
CrwCiffDecodetests cover CRW/CIFF derived EXIF mapping for legacy Canon RAW.OiioAdaptertests cover stable handling of empty-value attributes needed by interop parity workflows.
Requirements:
doxygen(optional:graphviz)
Generate API docs:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOPENMETA_BUILD_DOCS=ON
cmake --build build --target openmeta_docsRequirements:
doxygen- Python packages listed in
docs/requirements.txt(Sphinx + Breathe;furois optional)
Install the Python deps into your active environment (example with uv):
uv pip install -r docs/requirements.txtBuild:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOPENMETA_BUILD_SPHINX_DOCS=ON
cmake --build build --target openmeta_docs_sphinxInstall:
cmake --install build --prefix /tmp/openmeta-install
ls /tmp/openmeta-install/share/doc/OpenMeta/html/index.htmlWhen both OPENMETA_BUILD_SPHINX_DOCS=ON and OPENMETA_BUILD_DOCS=ON, the
Doxygen HTML output is installed under share/doc/OpenMeta/doxygen/html.