Metadata Translation ==================== ``openmeta/metadata_translation.h`` provides bounded explicit projection between metadata families. Current contracts translate edited XMP creation dates into native EXIF/IPTC date groups, exact technical XMP/TIFF properties into native EXIF fields, typed capture properties into native EXIF scalars, target-bound image geometry into native TIFF/EXIF groups, and exact descriptive, flat location, and editorial XMP properties into native IPTC-IIM datasets before transfer or writing. The APIs are experimental and versioned by ``kMetadataDateTranslationContractVersion == 2`` and ``kMetadataTechnicalTranslationContractVersion == 2`` and ``kMetadataCaptureTranslationContractVersion == 1`` and ``kMetadataGeometryTranslationContractVersion == 1`` and ``kMetadataDescriptiveTranslationContractVersion == 1`` and ``kMetadataLocationTranslationContractVersion == 1`` and ``kMetadataEditorialTranslationContractVersion == 1`` and ``kMetadataIptcTranslationContractVersion == 1`` and ``kMetadataGpsTranslationContractVersion == 1`` and ``kMetadataStructuredLocationTranslationContractVersion == 1`` and ``kMetadataGpsNavigationTranslationContractVersion == 1`` and ``kMetadataDevelopmentCorrectionTranslationContractVersion == 2`` and ``kMetadataLearningOptOutInTranslationContractVersion == 2`` and ``kMetadataProfileTranslationContractVersion == 1``. Development/correction and learning lifecycle (0.8.11) ------------------------------------------------------ The six existing development/correction fields A40D-A412 and LearningOptOutIn 9287 now use translation contract 2. Exact valid clean native matches gain Dirty while retaining values, wire hints/counts, origin and endian provenance. Dirty matches remain unchanged. Complete ReplaceExisting deletion retains typed native intent even when a key is absent; clean tombstones are promoted and dirty ones reused. Promotions and deletion intents consume operation limits, and new intents also consume entry limits. Conflict, source-mask and rollback rules remain. Selected sources still require explicit host exif_version 300 or 310; translation never infers or changes ExifVersion or processing history. These APIs do not validate a complete destination EXIF profile. All seven fields use selective TIFF/BigTIFF/DNG merge and removal in both safety modes, including restored snapshots. The shared native allowlist grows from 77 to 84. Existing/template DNG version retention, source-pixel filters, ABI 4, snapshot v1 and host-owned synchronization remain unchanged. LearningOptOutIn uses checked 16-bit word conversion from native provenance to canonical little endian and then to the target TIFF byte order. Unrelated UNDEFINED payloads remain opaque. Canonical encoding validates learning values before copying them. For native TIFF type-129 text, a counted payload may retain one explicit wire NUL. Validation and serialization both use the logical UTF-8 byte length plus one terminator. Ordinary authored text and the reader's normalized text omit that NUL from their payload. Embedded NULs and mismatched wire counts still fail. Workflow -------- See :doc:`capture_sync_milestone` for the seventeen capture APIs, 81 distinct native targets, shared-target composition and the portable round-trip fixes. Primary ExifIFD FNumber, FocalLength and DigitalZoomRatio emit exact fractions; FocalLength is in millimeters without the display suffix `` mm``. Invalid native types/counts and values leave valid existing XMP available. Regenerate older rounded packets from native EXIF when original precision is needed. Translation is a separate step. Creation, editing, transfer, and writing do not invoke it implicitly: 1. Read, create, or edit a finalized ``MetaStore``. 2. Call ``translate_xmp_creation_dates(...)``, ``translate_xmp_technical_metadata(...)``, ``translate_xmp_capture_metadata(...)``, ``translate_xmp_image_geometry(...)``, ``translate_xmp_descriptive_metadata(...)``, ``translate_xmp_location_metadata(...)``, ``translate_xmp_editorial_metadata(...)``, ``translate_xmp_iptc_metadata(...)``, ``translate_xmp_gps_metadata(...)``, ``translate_xmp_structured_location_metadata(...)``, ``translate_xmp_gps_navigation_metadata(...)``, ``translate_xmp_learning_opt_out_in_metadata(...)``, ``translate_xmp_profile_metadata(...)``, or the required combination with explicit mapping and conflict options. 3. Pass the returned finalized store to transfer preparation or a writer. This separation prevents a transfer from unexpectedly replacing native camera dates merely because decoded XMP is present. The default source mode is ``DirtyOnly``, so only caller-modified XMP values and tombstones are eligible. Date mappings ------------- .. list-table:: :header-rows: 1 :widths: 22 38 40 * - XMP source - Native destination - Precision requirements * - ``xmp:CreateDate`` - EXIF ``DateTimeDigitized``, ``OffsetTimeDigitized``, and ``SubSecTimeDigitized`` - Time is required. Timezone and up to nine fractional digits are preserved in companion tags. * - ``xmp:CreateDate`` - IPTC ``DigitalCreationDate`` and ``DigitalCreationTime`` - Date-only or whole seconds are accepted. Fractional seconds are rejected. * - ``photoshop:DateCreated`` - IPTC ``DateCreated`` and ``TimeCreated`` - Date-only or whole seconds are accepted. Fractional seconds are rejected. * - XMP ``exif:DateTimeOriginal`` - EXIF ``DateTimeOriginal``, ``OffsetTimeOriginal``, and ``SubSecTimeOriginal`` - Time is required. Timezone and up to nine fractional digits are preserved in companion tags. Accepted values use a full Gregorian ``YYYY-MM-DD`` date, optionally followed by ``T`` and ``hh:mm:ss``, up to nine fractional digits, and ``Z`` or a ``+/-HH:MM`` timezone. Invalid dates, malformed values, and conversions that would discard precision fail instead of being normalized or truncated. Lexical ``-00:00`` is preserved as a negative-zero offset. Each mapping can be disabled independently. For example, callers that need to retain fractional ``xmp:CreateDate`` can disable its IPTC projection while keeping exact EXIF projection. Technical EXIF mappings ----------------------- In 0.6.2, dirty tombstones for ``tiff:Make``, ``tiff:Model`` and ``xmp:CreatorTool`` translated with ``ReplaceExisting`` can remove the existing native IFD0 tags through TIFF/BigTIFF edit writers. Preparation preserves the explicit removal requests through snapshots. Source omission preserves the destination. In 0.7.0 all three timestamp owners control the base, offset and subsecond family. Missing companions produce explicit native dirty tombstones even when absent from the source store. Dirty owner deletion removes all three members; omission preserves values. Synthesized intents count against added entry and operation limits and are reused on repeat calls. TIFF edits merge unspecified destination entries for dirty timestamps, preserving other groups and native records. Date and technical calls remain separate transactions. See :doc:`migration_0_7` for contract version 2 and the unfrozen ABI-4 policy. .. list-table:: :header-rows: 1 :widths: 24 38 38 * - XMP source - Native EXIF destination - Requirements * - ``xmp:ModifyDate`` - IFD0 ``DateTime`` plus ExifIFD ``OffsetTime`` and ``SubSecTime`` - Time is required. Timezone and up to nine fractional digits are preserved in companion tags. * - ``tiff:Make`` - IFD0 ``Make`` - Non-empty 7-bit ASCII without embedded NUL bytes. * - ``tiff:Model`` - IFD0 ``Model`` - Non-empty 7-bit ASCII without embedded NUL bytes. * - ``xmp:CreatorTool`` - IFD0 ``Software`` - Non-empty 7-bit ASCII without embedded NUL bytes. Namespaces and property paths must match exactly. These mappings are intended for edited or newly created host metadata, not for copying source-bound camera processing data. Each singleton and the complete ``ModifyDate`` companion group reconcile independently, so a conflict in ``Make`` does not silently change ``Model``. Capture EXIF mappings --------------------- .. list-table:: :header-rows: 1 :widths: 34 38 28 * - XMP source - Native EXIF destination - Required native type * - ``exif:ExposureTime`` - ExifIFD ``ExposureTime`` - One unsigned ``RATIONAL``, greater than zero * - ``exif:FNumber`` - ExifIFD ``FNumber`` - One unsigned ``RATIONAL``, greater than zero * - ``exif:ISO``, ``exif:ISOSpeedRatings``, or ``exif:ISOSpeedRatings[1]`` - ExifIFD ``ISOSpeedRatings`` - One ``SHORT`` in ``1..65535`` * - ``exif:FocalLength`` - ExifIFD ``FocalLength`` - One unsigned ``RATIONAL``, greater than zero * - ``exif:ExposureCompensation`` or ``exif:ExposureBiasValue`` - ExifIFD ``ExposureBiasValue`` - One signed ``SRATIONAL`` Rational sources may be typed scalar XMP values or full text integers, decimals, scientific decimals, and ``numerator/denominator`` values. Focal length also accepts the OpenMeta portable `` mm`` suffix. Conversion uses integer arithmetic and reduces the exact source value before checking the 32-bit EXIF numerator and denominator limits. It never uses a floating-point approximation. For example, ``2.8`` becomes ``14/5`` and ``8e-3`` becomes ``1/125``. This exactness is intentionally strict. A bounded repeating decimal such as ``0.333333333333333`` does not fit native ``SRATIONAL`` exactly and returns ``ValueOutOfRange``; provide ``1/3`` or a typed signed rational when exact thirds are required. ISO rejects multi-value arrays, decimals, zero, and values above ``65535`` instead of selecting, truncating, or changing the native TIFF type. Portable and standard aliases target the same native singleton. If more than one eligible alias is present, the source is ambiguous and translation fails rather than selecting one. Capture settings writeback -------------------------- ``translate_xmp_capture_settings_metadata(...)`` and Python ``Document.translate_capture_settings_metadata(...)`` add twelve independent SHORT singleton mappings with ``MetadataCaptureSettingsTranslationOptions``, contract version 2. Exact unindexed paths use ``http://ns.adobe.com/exif/1.0/``. .. list-table:: :header-rows: 1 * - XMP path - ExifIFD tag - Accepted codes - Flag * - ExposureProgram - 0x8822 - 0..8 - exposure_program_to_exif * - MeteringMode - 0x9207 - 0..6, 255 - metering_mode_to_exif * - SensingMethod - 0xA217 - 1..5, 7, 8 - sensing_method_to_exif * - CustomRendered - 0xA401 - 0..1 - custom_rendered_to_exif * - ExposureMode - 0xA402 - 0..2 - exposure_mode_to_exif * - WhiteBalance - 0xA403 - 0..1 - white_balance_to_exif * - SceneCaptureType - 0xA406 - 0..3 - scene_capture_type_to_exif * - GainControl - 0xA407 - 0..4 - gain_control_to_exif * - Contrast - 0xA408 - 0..2 - contrast_to_exif * - Saturation - 0xA409 - 0..2 - saturation_to_exif * - Sharpness - 0xA40A - 0..2 - sharpness_to_exif * - SubjectDistanceRange - 0xA40C - 0..3 - subject_distance_range_to_exif Sources accept signed or unsigned scalar integers in the listed sets, unsigned decimal integer text, or exact existing OpenMeta enum labels. SceneCaptureType also accepts the portable label ``Night scene`` for code 3. Text must use Ascii, Utf8, or Unknown encoding. Case changes, whitespace, signs in text, decimals, fractions, floats, arrays, and unknown codes fail. ExposureProgram code 9/Bulb remains a read-only extension. No camera state or rendering behavior is inferred. Defaults are DirtyOnly/FailOnConflict with every field enabled. Eligible duplicate sources fail; clean sources are ignored in DirtyOnly, following the existing numeric capture contract. Native values must be one SHORT and equal to the selected code. PreserveExisting keeps existing fields, FailOnConflict protects non-equivalent values, and ReplaceExisting repairs types/duplicates. Dirty source tombstones remove the corresponding field under ReplaceExisting. All selected fields share one atomic transaction, including source/output aliasing and owned provenance. Existing numeric capture options remain unchanged. Limits are 12 added entries, 1024 operations, 128 source text bytes per selected active property, and 1536 total source text bytes. Limits may be lowered and never cause truncation. Preparation may allocate. EXIF versions are retained; the API does not create or upgrade version metadata. Capture coverage has no fixed all-tags denominator. Camera/lens/spectral text and direct APEX values have separate contracts below, as do focal-plane/subject arrays. These twelve settings do not close arbitrary EXIF writeback. Exact capture rational writeback -------------------------------- ``translate_xmp_capture_rational_metadata(...)`` and Python ``Document.translate_capture_rational_metadata(...)`` use ``MetadataCaptureRationalTranslationOptions``, contract version 2, for four independent unsigned RATIONAL singletons. Exact unindexed source paths use ``http://ns.adobe.com/exif/1.0/``. .. list-table:: :header-rows: 1 * - XMP path - ExifIFD tag - Value contract - Flag * - SubjectDistance - 0x9206 - Meters; zero unknown; wire numerator UINT32_MAX infinity - subject_distance_to_exif * - DigitalZoomRatio - 0xA404 - Nonnegative ratio; zero means digital zoom unused - digital_zoom_ratio_to_exif * - ExposureIndex - 0xA215 - Positive exposure index - exposure_index_to_exif * - FlashEnergy - 0xA20B - Nonnegative BCPS energy; no Flash-state inference - flash_energy_to_exif Sources accept scalar nonnegative integers, unsigned rationals, and the existing exact integer/decimal/scientific/fraction text grammar, including a leading plus. Signed rational and floating-point values, arrays, whitespace, unit suffixes, zero denominators, malformed numbers, and unsupported precision fail. Text must use Ascii, Utf8, or Unknown encoding. Checked uint64 parsing precedes reduction; the reduced numerator and denominator must fit uint32. No rounding occurs. SubjectDistance adds exact ``Unknown`` and ``Infinity`` labels. Integer/fraction text and typed integer/unsigned-rational inputs with numerator UINT32_MAX select infinity before reduction; their denominator must be positive and fit uint32. New unknown/infinity values use 0/1 and UINT32_MAX/1. Equivalent existing sentinel encodings retain their denominator. Decimal/scientific text denotes a finite number; if its reduced numerator would equal UINT32_MAX, translation fails to avoid encoding a finite distance as infinity. Other finite inputs that only reduce to that reserved numerator also fail. Native sentinel equivalence is checked before ordinary rational cross-multiplication. Defaults are DirtyOnly/FailOnConflict with all fields enabled. Missing sources retain native fields; eligible duplicates fail. PreserveExisting retains native values, FailOnConflict requires typed equivalence, and ReplaceExisting repairs values/types/duplicates or removes a dirty source tombstone's native field. All selected fields commit atomically, including source/output aliasing and owned provenance. Limits are four additions, 1024 operations, 128 source text bytes per property, and 512 total text bytes. Preparation may allocate. Accepted exact clean native values gain Dirty while retaining native components and wire provenance. Complete eligible deletion creates or reuses Dirty|Deleted native intent even when the native key was absent. PreserveExisting keeps an active owner without promotion; omitted or ineligible sources preserve the destination. Intent, promotions and duplicate removals consume the existing preflight budgets. Repeated calls reuse intent; failure preserves source and output. The host synchronizes shared objects. The canonical EXIF tags 0xA215 and 0xA20B are the only ExposureIndex/FlashEnergy targets; older TIFF/EP aliases remain untouched. No APEX conversion, unit conversion, version creation/upgrade, or geographic/flash-state inference occurs. Existing numeric and settings option layouts remain unchanged. In 0.5.5, primary native DigitalZoomRatio output retains exact fractions, including zero. Source spelling can still differ from equivalent generated values. Use ``xmp_include_existing=True`` and ``XmpConflictPolicy.ExistingWins`` for authoritative source spelling; choose ``CanonicalizeManaged`` when generated values must replace managed source aliases. These are explicit authority choices. Complete Flash writeback ------------------------ ``translate_xmp_flash_metadata(...)`` and Python ``Document.translate_flash_metadata(...)`` use ``MetadataFlashTranslationOptions``, contract version 1, to write one ExifIFD Flash (0x9209) SHORT. Select either an exact scalar ``exif:Flash`` or all five children in the EXIF namespace: - ``Flash/Fired``: bit 0, Boolean. - ``Flash/Return``: bits 1..2, codes 0, 2, or 3; code 1 is reserved. - ``Flash/Mode``: bits 3..4, codes 0..3. - ``Flash/Function``: bit 5, Boolean; True means no flash function. - ``Flash/RedEyeMode``: bit 6, Boolean. Children also accept the ``Flash/exif:Name`` spelling. Boolean values accept integer 0/1 or exact text ``True``, ``False``, ``0``, ``1``. Mode and Return accept nonnegative scalar integers or unsigned decimal integer text. Scalar Flash accepts those integer forms or exact OpenMeta Flash labels. Bits 7 and above, reserved Return code 1, floats, rationals, arrays, whitespace, signs in text, case changes, and unknown labels fail. Text encodings are Ascii, Utf8, or Unknown. All 96 combinations of defined bits are accepted; physical consistency is not inferred from other capture fields. Defaults are DirtyOnly/FailOnConflict. One dirty child selects the complete structure, including its clean companions. A partial structure fails without borrowing missing bits from native EXIF. Duplicate child aliases, scalar plus structured sources, qualifiers, nested children, and indexed forms fail. Clean unselected sources are retained. A dirty scalar tombstone or all five dirty child tombstones removes native Flash under ReplaceExisting; partial child removal fails. Missing sources retain native Flash. FlashEnergy is independent. Native equivalence requires one SHORT with the same bits. PreserveExisting keeps native fields, FailOnConflict protects non-equivalent values, and ReplaceExisting repairs types/duplicates. All operations commit atomically, including aliased source/output and owned provenance. Limits are one added entry, 1024 operations, 16 inspected Flash source properties, 128 text bytes per active property, and 640 total text bytes. Limits may be lowered; preparation may allocate. Existing capture option layouts remain unchanged. EXIF version fields are retained. For portable XMP readback, use ``include_existing_xmp=True`` and ``conflict_policy=XmpConflictPolicy.ExistingWins`` to retain a complete source structure or scalar spelling. Snapshot transfer uses ``xmp_include_existing`` and ``xmp_conflict_policy`` for the same choices. Native-only portable output continues to use the existing Flash projection. LightSource writeback --------------------- ``translate_xmp_light_source_metadata(...)`` and Python ``Document.translate_light_source_metadata(...)`` use ``MetadataLightSourceTranslationOptions``, contract version 1, to write one ExifIFD LightSource (0x9208) SHORT from exact ``exif:LightSource``. Accepted codes are ``0..4``, ``9..34``, and ``255``: 32 values from the existing OpenMeta name table. This includes unknown (0), other (255), warm white fluorescent (16), and the newer light-source/LED codes 25..34. Undefined codes fail. No source means no change; the API does not create a default Unknown. Sources accept nonnegative signed or unsigned scalar integers, unsigned decimal integer text, and exact unique OpenMeta labels. Explicit aliases ``Tungsten`` and ``Cloudy weather`` map to 3 and 10. Numeric ``1`` and ``25`` remain distinct, while text ``Daylight`` returns ``AmbiguousSource`` because both codes use that display label. Native values, conflict policies, EXIF versions, white balance, Flash, DNG calibration illuminants and color temperature never resolve this ambiguity. Callers must supply an explicit numeric code when the source label is ambiguous. Text must use Ascii, Utf8, or Unknown encoding. Signs in text, whitespace, case changes, floating-point/scientific notation, fractions, arrays, unsupported encodings and unknown labels fail. Eligible indexed, qualified or nested LightSource paths return ``UnsupportedSourceShape``. Foreign namespaces and other property names are ignored. Clean sources are ignored in default DirtyOnly mode; All selects active sources. Duplicate eligible sources fail. Native equivalence requires one SHORT with the selected code. Default FailOnConflict protects different values and wrong types. PreserveExisting retains native data; ReplaceExisting repairs types and duplicates. A dirty source tombstone removes native LightSource under ReplaceExisting. Tombstone contents are not parsed, so an ambiguous old label can still be explicitly removed. Clean tombstones are ignored. Source omission retains native metadata. Limits are one addition, 1024 operations, 128 text bytes per active property, and 128 total text bytes. Limits may be lowered; invalid settings fail. All changes share the existing atomic capture transaction, including aliased source/output and owned provenance. Preparation may allocate. Existing capture option layouts and EXIF version metadata are retained. This field operation does not claim full EXIF-version conformance or infer other capture metadata. Portable native-to-XMP output now emits numeric ``1`` and ``25`` for these two LightSource values, preserving their distinction. Other defined values retain the existing unique labels. Every defined native code survives portable dump, XMP decode and this reverse API. Existing XMP retained with ExistingWins keeps its original spelling, including ambiguous ``Daylight``; previously lost numeric identity cannot be recovered from that text alone. Human-readable native names and DNG calibration-illuminant display names remain unchanged. Target-bound image geometry --------------------------- ``translate_xmp_image_geometry(...)`` projects ``tiff:Orientation`` and complete XMP width/height pairs only when they agree with a caller-provided ``TransferTargetImageSpec``. Orientation becomes one IFD0 ``SHORT``. Stored dimensions become IFD0 ``ImageWidth``/``ImageLength`` and ExifIFD ``PixelXDimension``/``PixelYDimension`` ``LONG`` values. Width aliases are ``tiff:ImageWidth``, ``exif:ExifImageWidth``, and ``exif:PixelXDimension``. Height aliases are ``tiff:ImageLength``, portable ``tiff:ImageHeight``, ``exif:ExifImageHeight``, and ``exif:PixelYDimension``. Aliases may coexist only when all values agree. Dimensions describe the stored raster and are never swapped for orientation indices 5 through 8. Missing target facts, target mismatches, contradictory aliases, incomplete pairs, and mixed active/deleted pairs fail transactionally. Descriptive mappings -------------------- .. list-table:: :header-rows: 1 :widths: 34 46 20 * - XMP source - Native IPTC-IIM destination - Maximum encoded bytes * - ``dc:title[@xml:lang=x-default]`` - ``ObjectName`` (2:5) - 64 * - ``dc:description[@xml:lang=x-default]`` - ``Caption-Abstract`` (2:120) - 2000 * - ``dc:creator[n]`` - repeated ``By-line`` (2:80) - 32 per value * - ``dc:subject[n]`` - repeated ``Keywords`` (2:25) - 64 per value * - ``dc:rights[@xml:lang=x-default]`` - ``CopyrightNotice`` (2:116) - 128 * - ``photoshop:Credit`` - ``Credit`` (2:110) - 32 * - ``photoshop:Source`` - ``Source`` (2:115) - 32 The default-language and indexed paths must match exactly. Creator and keyword items retain numeric XMP index order. Duplicate singleton properties or duplicate active indexes are ambiguous and fail rather than selecting a value. IPTC-IIM limits are byte limits; text is never truncated. Non-ASCII values remain UTF-8. Translation emits IPTC ``CodedCharacterSet`` (1:90) with ``ESC % G`` when the marker is absent and every existing active IPTC value is ASCII or will be replaced by the same transaction. An incompatible charset marker or unrelated legacy high-bit data returns ``NativeEncodingConflict`` without modifying the output. Location mappings ----------------- ``translate_xmp_location_metadata(...)`` accepts ``MetadataLocationTranslationOptions`` and returns ``MetadataDescriptiveTranslationResult``. It uses the descriptive source modes, conflict policies, statuses, and mapping diagnostics. The existing descriptive API still selects only its original seven mappings. .. list-table:: :header-rows: 1 :widths: 40 45 15 * - Exact XMP source - Native IPTC-IIM destination - Maximum bytes * - ``photoshop:City`` - ``City`` (2:90) - 32 * - ``Iptc4xmpCore:Location`` - ``Sub-location`` (2:92) - 32 * - ``photoshop:State`` - ``Province-State`` (2:95) - 32 * - ``photoshop:Country`` - ``Country-PrimaryLocationName`` (2:101) - 64 * - ``Iptc4xmpCore:CountryCode`` - ``Country-PrimaryLocationCode`` (2:100) - 3 These are the flat legacy location mappings in the `IPTC Photo Metadata Standard `_. Each mapping is an independent singleton. Namespace URIs and property paths must match exactly; duplicate active sources are rejected even when equal. Empty values, invalid UTF-8/XML text, embedded NULs, and excess bytes fail without changing the output. Removal uses a dirty tombstone. Country codes require exactly two or three uppercase ASCII letters and are preserved byte for byte. Code membership, conversion between two and three letters, and agreement with the country name belong to the caller. Structured ``LocationCreated``/``LocationShown`` properties, indexed or qualified forms, and GPS are not aliases and are not selected by this API. The operation does not infer whether a flat location describes the camera or the depicted subject. UTF-8 charset promotion follows the descriptive policy above, including rejection of incompatible markers and unrelated legacy high-bit IPTC values. Limits are 1024 matched source properties, 4096 operations, 8 MiB of inspected text/charset-safety bytes, and six added entries (five datasets plus one charset marker). Options can lower these limits. New entries copy source provenance; updates preserve the existing native entry's provenance. Translation is a preparation operation that may allocate; it is not an allocation-free replay API. Structured location reconciliation ---------------------------------- ``translate_xmp_structured_location_metadata(...)`` accepts ``MetadataStructuredLocationTranslationOptions`` and returns the shared ``MetadataDescriptiveTranslationResult``. Contract version 1 selects one structured location and reconciles its five supported text fields into **both flat legacy XMP and native IPTC-IIM**. Existing flat-location and combined IPTC APIs keep their original mapping sets. The root namespace must be ``http://iptc.org/std/Iptc4xmpExt/2008-02-29/``. ``location_kind`` defaults to ``MetadataStructuredLocationKind::Shown``; ``Created`` requires an explicit choice. Shown and Created never fall back to each other. These properties distinguish the depicted place from the place where the camera was. Choosing Created authorizes copying that camera-location description into the legacy fields whose historical semantics are ambiguous. See the `IPTC Photo Metadata specification `_. .. list-table:: :header-rows: 1 * - Structured child - Flat XMP destination - Native IPTC - Maximum UTF-8 bytes * - ``City`` - ``photoshop:City`` - 2:90 - 32 * - ``Sublocation`` - ``Iptc4xmpCore:Location`` - 2:92 - 32 * - ``ProvinceState`` - ``photoshop:State`` - 2:95 - 32 * - ``CountryName`` - ``photoshop:Country`` - 2:101 - 64 * - ``CountryCode`` - ``Iptc4xmpCore:CountryCode`` - 2:100 - 3 Paths are ``LocationShown[n]/City``, ``LocationCreated[n]/City``, and the equivalent paths for the other four children. The existing scalar ``LocationCreated/City`` resource form is accepted as record index 1. Mixed scalar and indexed Created forms fail. Indexes must be positive decimal uint32 values without leading zeros. ``location_index == 0`` selects the sole represented record when present; an absent root kind is a no-op, and multiple records fail with AmbiguousLocation. A positive option selects that exact index and fails with LocationNotFound if absent. It never selects the first record implicitly or merges records. Any represented child, including an unsupported child, counts for record selection. Selection precedes DirtyOnly filtering. XML decoding uses one-based RDF order; authored stores can have sparse indexes. Canonical unqualified child names and ``Iptc4xmpExt:``-qualified equivalents are accepted. Duplicate field sources are ambiguous, including equal values, qualified/unqualified aliases, or mixed active and dirty-deleted entries. Other namespaces, nested Address children, language/index qualifiers, location names and IDs, WorldRegion, and structured GPS fields are retained without projection. Nonstandard scalar LocationShown and malformed indexed paths fail with UnsupportedSourceShape. The API operates on decoded paths and does not infer a missing RDF container type. Defaults are DirtyOnly and FailOnConflict. Only the selected structured leaf flags trigger a field in DirtyOnly mode; dirty flat/native destinations do not. All selects clean active leaves too. Each flat XMP/native IPTC pair is one conflict group: - PreserveExisting retains both destinations if either already exists. - FailOnConflict requires each existing destination to match and fills a missing counterpart. Mismatches and duplicate destinations fail atomically. - ReplaceExisting updates both destinations and removes duplicate entries. A dirty structured leaf tombstone removes both corresponding destinations. Missing structured fields preserve existing destination fields. Clean tombstones and whole-structure/array tombstones do not request field deletion. This is a field patch operation; it does not establish geographic consistency between supplied fields and retained fields. Text must be nonempty valid UTF-8/XML text. Byte limits never truncate. Country codes require two or three uppercase ASCII letters; registry membership and name/code agreement remain caller responsibilities. Limits are 1024 inspected structured properties across the chosen root kind, 4096 destination operations, 8 MiB of source text and charset-safety inspection, and 11 added entries: five flat XMP properties, five IPTC datasets, and a possible UTF-8 marker. Existing destination entries also count against the operation inspection limit. Each limit can be lowered. The shared IPTC charset policy rejects unsafe promotion of retained legacy high-bit values. Result entry counters include both destination families; group counters count five fields. ``NativeConflict`` covers either destination family. The source stays immutable. Failure leaves the output unchanged; output/source aliasing is supported. New entries own copied source provenance, while updates retain existing destination provenance. Preparation may allocate. .. code-block:: python translated = document.translate_structured_location_metadata( location_kind=openmeta.MetadataStructuredLocationKind.Shown, location_index=2, source_mode=openmeta.MetadataDescriptiveTranslationSourceMode.All, conflict_policy=openmeta.MetadataDescriptiveTranslationConflictPolicy.ReplaceExisting, ) The returned document is detached. Persist both XMP and IPTC from its snapshot to keep both destinations synchronized. For Python ``transfer_snapshot_file``, set ``xmp_include_existing=True``: that transfer API defaults to projecting native metadata only. This option retains the reconciled flat XMP and structured records. GPS translation is a separate explicit contract; structured location coordinates are not aliases for primary EXIF GPS. Structured location construction -------------------------------- ``translate_xmp_location_to_structured_metadata(...)`` and Python ``Document.translate_location_to_structured_metadata(...)`` copy the five flat legacy XMP location fields into one selected IPTC Extension record. ``MetadataLocationCreationTranslationOptions`` has contract version 1. .. list-table:: :header-rows: 1 * - Exact flat source - Selected structured child * - photoshop:City - City * - Iptc4xmpCore:Location - Sublocation * - photoshop:State - ProvinceState * - photoshop:Country - CountryName * - Iptc4xmpCore:CountryCode - CountryCode The caller must choose both ``location_kind`` (Shown or Created) and a positive ``location_index``. C++ defaults to Unspecified/zero and rejects those values; Python requires both arguments. Created permits index one only. Shown permits an existing record or append at the next index. Existing indexes must be dense from one, with a maximum of 1024. Sparse indexes, mixed scalar/indexed Created records, opaque root/record placeholders, and competing nested or language-qualified shapes for a selected field fail. Other namespaces and nested Address fields are not aliases. No Created/Shown selection or geographic information is inferred. New records use indexed ``LocationShown[n]`` or ``LocationCreated[1]`` paths with IPTC Extension children. Existing scalar Created records retain their resource shape. Portable XMP emits indexed locations as RDF Bags. The decoder omits redundant child prefixes within the same namespace; record order and field values are preserved. RDF indexes identify current order, not stable record IDs. City, sublocation, and state use the existing 32-byte flat-location limit; country uses 64 bytes. CountryCode requires two or three uppercase ASCII letters, without checking country membership. Other fields require nonempty valid UTF-8 text, with Ascii/Utf8/Unknown source encoding. Existing flat location text validation applies. There is no normalization or truncation. Defaults are DirtyOnly/FailOnConflict with five enabled flags. Missing source fields retain destinations. Duplicate selected sources fail. Each destination leaf is a conflict group; PreserveExisting keeps it, FailOnConflict requires equivalence, and ReplaceExisting replaces it and removes duplicate aliases. A dirty flat-field tombstone removes that selected structured leaf. Unrelated fields and records remain untouched. Removal of an entire non-last record fails to prevent renumbering another record during serialization. Removing the last record is allowed. All selected changes commit atomically, including aliased source/output calls. Preparation may allocate. Limits are five additions, 1024 operations, 1024 total inspected destination properties plus selected source candidates, and the existing descriptive total text budget. Native IPTC fields and charset markers are not modified or used as implicit sources. Native-only callers must explicitly prepare flat XMP first. To persist the constructed XMP, transfer requires ``xmp_include_existing=True`` and ``xmp_conflict_policy=openmeta.XmpConflictPolicy.ExistingWins``. Python example: .. code-block:: python created = document.translate_location_to_structured_metadata( openmeta.MetadataStructuredLocationKind.Shown, 1, source_mode=openmeta.MetadataDescriptiveTranslationSourceMode.All, ) Editorial mappings ------------------ ``translate_xmp_editorial_metadata(...)`` accepts ``MetadataEditorialTranslationOptions`` and returns ``MetadataDescriptiveTranslationResult``. Each mapping has an independent flag; ``DirtyOnly`` and ``FailOnConflict`` are the defaults. Descriptive and location calls retain their existing mapping sets. .. list-table:: :header-rows: 1 :widths: 40 45 15 * - Exact XMP source - Native IPTC-IIM destination - Maximum encoded bytes * - ``photoshop:Headline`` - ``Headline`` (2:105) - 256 * - ``photoshop:Instructions`` - ``SpecialInstructions`` (2:40) - 256 * - ``photoshop:TransmissionReference`` - ``OriginalTransmissionReference`` (2:103) - 32 The mappings and limits follow the IPTC Photo Metadata Standard 2025.1 (see the location specification link above). Sources must be exact singleton properties in the Photoshop namespace ``http://ns.adobe.com/photoshop/1.0/``. Title, description, rights usage terms, and other job identifiers are not aliases. Empty text, invalid UTF-8/XML, embedded NULs, duplicate active singletons, and excess encoded bytes fail transactionally. Values are never truncated. Dirty tombstones request removal under ``ReplaceExisting``. The shared IPTC transaction enforces charset safety, preserves unrelated native data, copies source provenance for new entries, and retains native provenance for updates. Limits are 1024 matched source properties, four added entries (three datasets plus one UTF-8 charset marker), 4096 operations, and 8 MiB of inspected text/charset-safety bytes. Options may lower these limits; disabling every mapping is invalid. This preparation operation may allocate. Python exposes the same operation as ``Document.translate_editorial_metadata``, with ``headline_to_iptc``, ``instructions_to_iptc``, and ``transmission_reference_to_iptc`` flags. It returns a detached document and raises ``ValueError`` with mapping diagnostics on failure. For clean metadata read from a file, explicitly select ``MetadataDescriptiveTranslationSourceMode.All``. Combined IPTC writeback ----------------------- ``translate_xmp_iptc_metadata(...)`` accepts ``MetadataIptcTranslationOptions`` and returns ``MetadataDescriptiveTranslationResult``. It selects the seven descriptive, five location, and three editorial groups above, plus the five workflow mappings below. All 20 text/priority groups share one transaction, including charset promotion and resource accounting. The existing subgroup APIs retain their original options and mappings. Date/time fields remain in ``translate_xmp_creation_dates(...)``; composing the two calls does not make them one transaction. .. list-table:: :header-rows: 1 :widths: 30 30 40 * - Exact Photoshop XMP source - Native IPTC-IIM destination - Contract * - ``AuthorsPosition`` - By-lineTitle (2:85) - Singleton text, at most 32 UTF-8 bytes * - ``CaptionWriter`` - Writer-Editor (2:122) - Singleton text, at most 32 UTF-8 bytes * - ``Category`` - Category (2:15) - One to three ASCII letters, case preserved * - ``SupplementalCategories[n]`` - Repeated SupplementalCategories (2:20) - Text, at most 32 UTF-8 bytes per value * - ``Urgency`` - Urgency (2:10) - One text digit from 1 to 8, or a signed/unsigned integer scalar in that range These contracts use the `Adobe Photoshop XMP namespace `__, `IPTC IIM 4.2 `__, and `IPTC Photo Metadata 2025.1 `__. Category, Supplemental Categories, and Urgency are legacy IIM fields. This API supports their explicit writeback; it does not convert them to modern taxonomy. Category does not consult a provider registry. The caller must supply a creator and associate AuthorsPosition with the first creator. Translation neither infers that association nor creates a missing creator. Supplemental Categories use exact indexed paths with positive numeric indexes. Gaps are accepted. Values are written in index order, and equal values at different indexes remain separate datasets. Duplicate active indexes fail. New repeated native entries receive placement ranks that preserve that order; source block and wire provenance remain owned by the output. Updates retain the existing native ranks. An unindexed scalar or empty Bag is not a removal request; remove the indexed entries with dirty tombstones. Urgency emits an ASCII digit, never a binary integer. Values ``0`` and ``9``, floats, rationals, arrays, and alternative textual spellings such as ``05``, ``+5``, or ``5.0`` are rejected. IPTC-to-portable-XMP projection now also carries native Urgency. All mappings default to enabled, with DirtyOnly/FailOnConflict policies. Each has an independent boolean flag. Disabling every mapping is invalid. Limits across the whole call are 1024 matched source properties, 1025 added entries (up to 1024 values plus one charset marker), 4096 operations, and 8 MiB of inspected text/charset-safety bytes. Callers may lower these limits. A failure in any selected group leaves the source and output unchanged. This also permits a non-ASCII replacement when another selected group removes the legacy IPTC bytes that would otherwise block UTF-8 promotion. Preparation may allocate; it is not an allocation-free replay operation. Python exposes ``Document.translate_iptc_metadata(...)`` with the same flags, policies, limits, detached output, and ValueError diagnostics. For example: .. code-block:: python translated = document.translate_iptc_metadata( source_mode=openmeta.MetadataDescriptiveTranslationSourceMode.All, conflict_policy=openmeta.MetadataDescriptiveTranslationConflictPolicy.ReplaceExisting, category_to_iptc=False, urgency_to_iptc=False, ) The combined API and existing date API cover the 24 active IPTC destination assignments in ExifTool's `xmp2iptc.args inventory `__ checked on 2026-09-09. This is a field inventory, not semantic or behavioral parity: the two commented taxonomy mappings, Photoshop IPTCDigest, arbitrary IPTC datasets, and ExifTool's conversion/overwrite conventions are excluded. Primary GPS writeback --------------------- ``translate_xmp_gps_metadata(...)`` accepts ``MetadataGpsTranslationOptions`` and returns ``MetadataGpsTranslationResult``. Contract version 1 covers three primary EXIF GPS groups, each with an independent flag. It does not select structured IPTC locations, destination coordinates, GPS time, navigation data, or nonstandard standalone XMP latitude/longitude reference properties. .. list-table:: Primary GPS mappings :header-rows: 1 :widths: 40 40 20 * - Exact EXIF XMP source - Native gpsifd fields - Flag * - ``GPSLatitude`` - LatitudeRef (1), Latitude (2) - ``latitude_to_exif`` * - ``GPSLongitude`` - LongitudeRef (3), Longitude (4) - ``longitude_to_exif`` * - ``GPSAltitude`` and ``GPSAltitudeRef`` - AltitudeRef (5), Altitude (6) - ``altitude_to_exif`` Coordinates accept unsigned integer degrees followed by decimal minutes, or integer minutes and decimal seconds: ``35,48.125N`` or ``139,34,55.25W``. Separators are commas; the final character must be uppercase N/S for latitude or E/W for longitude. Signs, whitespace, decimal degrees, exponent notation, and rational components are rejected. Latitude is bounded by 90 degrees and longitude by 180; minutes and seconds must be less than 60. At the maximum degree, all remaining components must be zero. Hemisphere is preserved at zero. The parser uses integer arithmetic and emits normalized integer degrees, integer minutes, and reduced rational seconds as RATIONAL[3], with an ASCII reference. Fractions must fit exact unsigned 32-bit numerator/denominator components; unsupported precision fails without rounding. Trailing decimal zeros do not consume precision. Parsing uses checked 64-bit intermediates; overflow also returns UnsupportedPrecision, even if further algebraic reduction could fit the native fields. Native equivalence compares typed components as rational numbers; it does not rearrange a different native DMS tuple into an equivalent whole angle. Use ReplaceExisting to canonicalize such tuples. Altitude uses a nonnegative unsigned rational, nonnegative integer scalar, or exact unsigned text (``123.45``, ``2469/20``, or ``0``). Floats, signed text, exponent notation, units, arrays, and zero denominators are rejected. A separate ``GPSAltitudeRef`` is mandatory. This contract uses the legacy Adobe XMP sea-level convention: text ``0``/``1``, or an integer scalar 0/1. It does not infer a missing reference from the sign or from existing native metadata. GPSVersionID is structural companion metadata. A missing version becomes BYTE[4] ``2.3.0.0`` when a selected active result needs it. An existing version must be one well-formed BYTE[4] entry; malformed or duplicate active versions fail. Existing version bytes are retained. Altitude supports ``2.0.0.0`` through ``2.4.0.0``: legacy versions use native sea-level references 0/1; version 2.4 uses 2/3 for the same meaning. Unknown versions fail altitude translation. No geoid, ellipsoid, horizontal datum, or unit conversion is performed, and native GPSVersionID is not upgraded. The XMP GPSVersionID property is not copied or used to infer an altitude convention. Callers with modern ellipsoidal-height XMP must not pass it under this legacy sea-level contract. Coordinate syntax and legacy XMP altitude semantics follow the `Adobe EXIF namespace `__ and `CIPA's EXIF/XMP mapping `__. The version-2.4 native reference distinction follows EXIF 3.0 section 4.6.7.1.6. Defaults are DirtyOnly and FailOnConflict. One dirty altitude member selects both active XMP members, including a clean companion. Missing, duplicate, or mixed active/deleted altitude members fail. A complete dirty altitude tombstone pair removes both native fields under ReplaceExisting; one coordinate tombstone removes its native value/reference pair. PreserveExisting preserves the existing native group as a unit when either field exists, even when the group is partial. FailOnConflict rejects mismatched or partial existing groups. ReplaceExisting updates fields and removes duplicates. If deletion removes the last GPS value, its version tag is also removed; unrelated GPS fields keep the version. The whole call is atomic. It leaves source and output unchanged on failure and copies provenance into output-owned storage. Calls may allocate during preparation. Limits are seven added entries including GPSVersionID, 1024 native operations, 128 text bytes per selected active source, and 512 total text bytes. Coordinates and altitude can be disabled independently; disabling every group is invalid. Existing translation APIs and their default mapping sets are unchanged. Python exposes the same C++ policy and diagnostics: .. code-block:: python translated = document.translate_gps_metadata( source_mode=openmeta.MetadataGpsTranslationSourceMode.All, conflict_policy=openmeta.MetadataGpsTranslationConflictPolicy.ReplaceExisting, ) Pass the detached result to transfer preparation to persist it. Portable XMP now writes all four GPSVersionID components. Its existing coordinate/altitude formatters can round native rational values; this reverse API cannot recover precision already lost before the XMP source was created. This contract covers primary position writeback, not all GPS metadata or complete ExifTool parity. GPS time and navigation writeback --------------------------------- ``translate_xmp_gps_navigation_metadata(...)`` uses independent ``MetadataGpsNavigationTranslationOptions`` and the shared GPS result, source mode, conflict policy, and diagnostic enums. Contract version 1 adds four atomic groups. The existing primary GPS API retains its options and mapping set. .. list-table:: :header-rows: 1 * - Exact EXIF XMP source - Native ``gpsifd`` fields - Flag * - ``GPSTimeStamp`` - TimeStamp (7) and DateStamp (29) - ``timestamp_to_exif`` * - ``GPSSpeedRef`` and ``GPSSpeed`` - SpeedRef (12) and Speed (13) - ``speed_to_exif`` * - ``GPSTrackRef`` and ``GPSTrack`` - TrackRef (14) and Track (15) - ``track_to_exif`` * - ``GPSImgDirectionRef`` and ``GPSImgDirection`` - ImgDirectionRef (16) and ImgDirection (17) - ``image_direction_to_exif`` Sources must use the exact ``http://ns.adobe.com/exif/1.0/`` namespace and unindexed, unqualified paths. XMP combines the native GPS date and time into one GPSTimeStamp property. Split GPSDateStamp/time, GPSDateTime aliases, capture-date fallback, receiver status/quality, destination GPS, and structured IPTC locations are outside this contract. Field identities and unit/reference codes follow the `Adobe EXIF namespace `_ and `CIPA EXIF/XMP mapping `_. GPSTimeStamp requires text ``YYYY-MM-DDTHH:MM:SS[.fraction]Z`` or the same complete date/time followed by ``+HH:MM`` or ``-HH:MM``. The timezone is mandatory. Numeric offsets through 23:59 normalize to UTC, including day/month/year rollover; both zero-offset signs mean UTC. Years must remain 0001 through 9999 after normalization. Gregorian calendar rules apply. Hours are 00-23, minutes 00-59, and seconds are less than 60. Leap seconds, partial dates/times, lowercase separators, whitespace, and inferred timezones are rejected. Output contains an ASCII ``YYYY:MM:DD`` date and an unsigned RATIONAL[3] time: integer UTC hours, integer UTC minutes, and reduced exact rational seconds. Fractions never round. Reduced numerator and denominator must each fit uint32; checked uint64 parser intermediates can also reject unsupported precision. Trailing decimal zeros do not consume precision. A supported fractional input can still fail when its whole-second numerator cannot fit uint32 after reduction. Speed and angles accept nonnegative integer scalars, unsigned rational scalars, or exact unsigned integer/decimal/fraction text, matching primary GPS altitude syntax. Floats, signed text, exponents, units inside numeric values, arrays, and zero denominators are rejected. Speed has no additional bound beyond the native rational capacity. Track and image direction are bounded by 359.99 inclusive, without rounding or modulo reduction. Reference properties are text. Speed accepts ``K``, ``M``, or ``N``, plus the exact existing OpenMeta portable aliases ``km/h``, ``mph``, and ``knots``. Track and image direction accept ``T`` or ``M``, plus ``True North`` and ``Magnetic North``. Output uses the compact ASCII letter code. Case, spelling, and whitespace are exact; no localized names or other synonyms are accepted. Units and north references are preserved without conversion or inference from native metadata. All flags default to enabled, with DirtyOnly/FailOnConflict. A dirty reference or numeric member selects its complete active XMP pair, including a clean companion. Missing, duplicate, or mixed active/deleted members fail. A dirty timestamp tombstone removes both date and time; removing a navigation pair requires both members to be dirty tombstones. Clean tombstones are ignored. Missing groups preserve native fields. Each native pair uses the primary GPS conflict rules: PreserveExisting keeps the whole group if either member exists; FailOnConflict requires a complete matching pair; ReplaceExisting repairs partial pairs and removes duplicates. Native rational equivalence is exact by component. Native text may include its one trailing wire NUL. Existing equivalent values retain their encoding and provenance. All selected groups share one atomic transaction. An active timestamp requires native GPSVersionID 2.2.0.0 through 2.4.0.0 under this versioned contract. Older and unknown versions fail without an upgrade. Speed/track/direction retain any well-formed BYTE[4] version. Missing versions become 2.3.0.0 when selected active output needs one. Malformed or duplicate active versions fail. Removing the last GPS value also removes its version; unrelated GPS fields retain it. Source XMP GPSVersionID is not copied. Limits are nine added entries including GPSVersionID, 1024 native operations, 128 text bytes per selected active property, and 896 total text bytes. Limits can be lowered. Failure leaves source and output unchanged, including when they alias. New entries own copied source provenance; preparation may allocate. .. code-block:: python translated = document.translate_gps_navigation_metadata( source_mode=openmeta.MetadataGpsTranslationSourceMode.All, conflict_policy=openmeta.MetadataGpsTranslationConflictPolicy.ReplaceExisting, ) Persist the detached result through a transfer snapshot with EXIF enabled. When retaining source XMP too, set ``xmp_include_existing=True`` and ``xmp_conflict_policy=openmeta.XmpConflictPolicy.ExistingWins`` in Python transfer helpers. Inclusion alone keeps the historical precedence of generated EXIF values, which can change reference spelling or timestamp formatting. ExistingWins preserves supplied XMP values over generated counterparts. The portable writer's native-only decimal formatting is not an exact arbitrary-rational round trip. Destination GPS writeback ------------------------- ``translate_xmp_gps_destination_metadata(...)`` uses independent ``MetadataGpsDestinationTranslationOptions`` and the shared GPS result, source mode, conflict policy, and diagnostic enums. Contract version 1 adds four atomic groups; the primary GPS and navigation APIs keep their existing scopes. .. list-table:: :header-rows: 1 * - Exact EXIF XMP source - Native ``gpsifd`` fields - Flag * - ``GPSDestLatitude`` - DestLatitudeRef (19) and DestLatitude (20) - ``latitude_to_exif`` * - ``GPSDestLongitude`` - DestLongitudeRef (21) and DestLongitude (22) - ``longitude_to_exif`` * - ``GPSDestBearingRef`` and ``GPSDestBearing`` - DestBearingRef (23) and DestBearing (24) - ``bearing_to_exif`` * - ``GPSDestDistanceRef`` and ``GPSDestDistance`` - DestDistanceRef (25) and DestDistance (26) - ``distance_to_exif`` Sources use exact unindexed, unqualified paths in ``http://ns.adobe.com/exif/1.0/``. Coordinates use the primary GPS text syntax: ``degrees,decimalMinutesN`` or ``degrees,minutes,decimalSecondsN``, with the appropriate N/S or E/W hemisphere. Bounds are 90/180 degrees and minutes or seconds below 60; components after a maximum degree must be zero. Output is ASCII hemisphere plus exact reduced unsigned RATIONAL[3], including the hemisphere of zero. Unsupported precision fails without rounding. Bearing and distance use the navigation exact nonnegative integer/rational scalar or unsigned integer/decimal/fraction text syntax. Bearing is in [0, 359.99], with ``T``/``M`` or exact ``True North``/``Magnetic North`` aliases. Distance accepts ``K``/``M``/``N`` for kilometers, miles, and nautical miles, plus exact ``Kilometers``, ``Miles``, and ``Nautical miles`` aliases. These distance units follow `CIPA DC-X010-2017, section 8.3 `_. Portable XMP now emits ``Nautical miles`` for destination unit N; it previously emitted the incorrect label ``Knots``. The reverse API accepts that historical capitalized spelling as N distance without converting the number. Lowercase ``knots``, ``km/h``, and ``mph`` are speed references and are rejected for distance. No bearing, distance, primary position, datum, north reference, or unit is inferred or converted. Numeric precision follows the primary GPS parser limits. Defaults are DirtyOnly/FailOnConflict with all four groups enabled. One dirty bearing/distance member selects the complete active pair, including its clean companion. Missing, duplicate, and mixed deleted/active members fail. A dirty coordinate tombstone removes both native coordinate fields; bearing/distance removal requires both XMP members to be dirty tombstones. Clean tombstones are ignored. Missing groups preserve their native values. Native pairs follow the primary GPS conflict policy. PreserveExisting retains a whole group when either native member exists. FailOnConflict requires a complete equivalent pair. ReplaceExisting repairs partial or malformed groups and removes duplicates. Equivalence compares typed rational components exactly; text may include its one wire NUL. A well-formed native BYTE[4] GPSVersionID is retained without interpreting or upgrading it. Missing versions default to 2.3.0.0 when active output needs one. Malformed or duplicate active versions fail. Removing the final GPS value removes its version; unrelated GPS retains it. Limits are nine added entries including the version, 1024 operations, 128 text bytes per selected active source, and 768 total text bytes. Limits may be lowered. All selected groups share one transaction; failures leave source and output unchanged, including aliasing. New entries own their provenance. Preparation may allocate. Python returns a detached document: .. code-block:: python destination = document.translate_gps_destination_metadata( source_mode=openmeta.MetadataGpsTranslationSourceMode.All, conflict_policy=openmeta.MetadataGpsTranslationConflictPolicy.ReplaceExisting, ) Persist the result through transfer preparation. To retain original XMP values and spelling, Python transfer requires both ``xmp_include_existing=True`` and ``xmp_conflict_policy=openmeta.XmpConflictPolicy.ExistingWins``. Native-only portable coordinate/rational formatting can be approximate; reverse translation cannot recover precision already lost from its input. Receiver status/quality, DOP, and geographic computation remain outside this contract. GPS receiver quality writeback ------------------------------ ``translate_xmp_gps_quality_metadata(...)`` and Python ``Document.translate_gps_quality_metadata(...)`` add five independent singleton mappings with ``MetadataGpsQualityTranslationOptions``, contract version 1. Exact unindexed paths use ``http://ns.adobe.com/exif/1.0/``. .. list-table:: :header-rows: 1 * - XMP source - Native tag/type - Flag * - ``GPSStatus`` - 9 / ASCII - ``status_to_exif`` * - ``GPSMeasureMode`` - 10 / ASCII - ``measure_mode_to_exif`` * - ``GPSDOP`` - 11 / RATIONAL - ``dop_to_exif`` * - ``GPSDifferential`` - 30 / SHORT - ``differential_to_exif`` * - ``GPSHPositioningError`` - 31 / RATIONAL - ``horizontal_error_to_exif`` Status accepts exact A/V or Measurement Active/Measurement Void text. Measure mode accepts nonnegative integer 2/3 or exact text ``2``/``3``. Differential accepts integer 0/1, exact text ``0``/``1``, or No Correction/Differential Corrected. No other enum spellings, decimal/fraction coercion, or unknown codes are accepted. DOP and horizontal error use the existing exact nonnegative rational parser; floats, signed text, exponents, zero denominators, and unsupported precision fail. DOP is retained without calculating an accuracy estimate. Horizontal error is in meters; no unit conversion or receiver-state inference occurs. The field identities follow `CIPA EXIF/XMP mapping `_. The v1 version contract accepts GPS 2.2 through 2.4 for differential correction and GPS 2.3 through 2.4 for horizontal error. Earlier or unknown versions fail when the corresponding active field is selected. Other quality fields retain any well-formed native BYTE[4] version. Versions are never upgraded. A missing native version defaults to 2.3.0.0; source XMP GPSVersionID is not copied. Defaults are DirtyOnly/FailOnConflict and five enabled flags. One dirty source tombstone removes its native singleton. Missing fields and clean tombstones retain native data. Duplicate sources fail. PreserveExisting retains existing native values; FailOnConflict requires typed equivalence; ReplaceExisting repairs malformed values and duplicates. Native ASCII permits one trailing wire NUL; rational equivalence is exact. All five selected fields share one atomic transaction. Removing the last GPS field also removes its version, while unrelated GPS retains it. New entries own their provenance, and failures preserve source/output even when they alias. Limits are six added entries including version, 1024 operations, 128 text bytes per selected active source, and 640 total text bytes. Limits can be lowered. Preparation may allocate. Existing primary, navigation, and destination APIs keep their scopes. Python returns a detached document for transfer preparation. Source XMP preservation still requires both ``xmp_include_existing=True`` and ``xmp_conflict_policy=openmeta.XmpConflictPolicy.ExistingWins`` during transfer. GPS text writeback ------------------ ``translate_xmp_gps_text_metadata(...)`` and Python ``Document.translate_gps_text_metadata(...)`` add four independent singleton mappings with ``MetadataGpsTextTranslationOptions``, contract version 1. Exact unindexed paths use ``http://ns.adobe.com/exif/1.0/``. .. list-table:: :header-rows: 1 * - XMP source - Native tag/type - Flag * - ``GPSSatellites`` - 8 / ASCII - ``satellites_to_exif`` * - ``GPSMapDatum`` - 18 / ASCII - ``map_datum_to_exif`` * - ``GPSProcessingMethod`` - 27 / UNDEFINED encoded text - ``processing_method_to_exif`` * - ``GPSAreaInformation`` - 28 / UNDEFINED encoded text - ``area_information_to_exif`` Satellites and datum require ASCII. Processing method and area accept valid UTF-8, including supplementary Unicode characters. Source Text values must use Ascii, Utf8, or Unknown encoding; Unknown is interpreted as UTF-8. Empty text is an active value, and spaces are retained exactly. NUL, C0/C1 controls, malformed UTF-8, and unsupported source types/encodings fail. There is no truncation, Unicode normalization, datum conversion, receiver-state inference, or parsing of satellite lists or processing-method names. Method and area are stored as UNDEFINED bytes with an eight-byte character-code identifier. ASCII content uses the ASCII prefix; non-ASCII content uses the UNICODE prefix followed by a UTF-16LE byte-order marker and exact UTF-16 payload. Supplementary characters use surrogate pairs. New encoded payloads omit the optional terminator; ordinary ASCII fields receive their wire terminator during serialization. Native equivalent text accepts the ASCII prefix or UNICODE with an explicit LE/BE byte-order marker and at most one trailing terminator. JIS, unknown identifiers, malformed text, and Unicode without a byte-order marker are non-equivalent. FailOnConflict protects these native values; PreserveExisting keeps them, while ReplaceExisting replaces them with canonical new output. Portable XMP recognizes the same supported encoded forms and emits decoded text. Unsupported forms are omitted from native-to-XMP projection without modifying the original native entries. The v1 method/area version contract accepts GPS 2.2 through 2.4; earlier or unknown versions fail when an active encoded field is selected. Satellites and datum retain any well-formed native BYTE[4] version. Missing versions default to 2.3.0.0, with no upgrade or source-XMP version copy. Defaults are DirtyOnly/FailOnConflict and four enabled flags. Selection, singleton conflicts, duplicate repair, dirty tombstone removal, version cleanup, owned provenance, and source/output aliasing follow the quality GPS contract. All selected fields commit atomically. Empty strings do not request deletion. Limits are five added entries including version, 1024 native operations, 4096 source text bytes per selected active property, and 16384 total text bytes. Encoded output is bounded by twice the source byte limit plus its prefix/BOM; encoding overhead does not consume the source-text budget. Limits may be lowered. Preparation may allocate. Python returns a detached document. Persist it through transfer preparation; retain source XMP using both ``xmp_include_existing=True`` and ``xmp_conflict_policy=openmeta.XmpConflictPolicy.ExistingWins``. The five GPS translation APIs now cover the repository's 32 standard native GPS tag IDs, including paired companions and automatically maintained GPSVersionID. This is bounded writeback coverage; it does not imply support for every possible encoding, version, or arbitrary metadata synchronization policy. Conflict and removal policy --------------------------- All translation APIs apply the following behaviors to each complete native group. Location and editorial translation reuse the descriptive conflict-policy enum. .. list-table:: :header-rows: 1 :widths: 24 76 * - Policy - Behavior * - ``PreserveExisting`` - Keep the complete native group when any member already exists. * - ``FailOnConflict`` - Require the group to be absent or already exactly equivalent. This is the default. * - ``ReplaceExisting`` - Replace the group, tombstone duplicates, and tombstone stale companion fields. A dirty deleted XMP source is propagated only with ``ReplaceExisting``; the corresponding native group is tombstoned. Duplicate eligible XMP source properties are rejected as ambiguous rather than choosing one occurrence. The operation is transactional. The source is immutable and the output store is replaced only after all selected mappings parse, reconcile, and finalize. New native entries retain the source XMP block and wire provenance, with any referenced wire-type name copied into output-owned storage. Date ``max_added_entries`` and ``max_operations`` may lower the public hard limits of 16 added entries and 1024 native operations. Technical translation has separate hard limits of 6 added entries, 1024 operations, 4096 bytes per text property, and 16 KiB total source text. Calls keep no global state and are safe when each concurrent call owns its output store. Capture translation separately permits at most 5 added entries, 1024 operations, 128 bytes per text property, and 640 total source text bytes. Its tag-specific conversion always emits scalar ``RATIONAL``, ``SHORT``, or ``SRATIONAL`` values, so generic writer type inference cannot select a different TIFF type. Geometry translation permits at most 5 added entries, 1024 operations, 32 bytes per text property, and 160 total source text bytes. One dirty dimension makes the complete active pair eligible. Dirty geometry tombstones may remove native fields under ``ReplaceExisting`` only when the target spec does not still declare the same fact. Descriptive translation separately bounds matched source properties, added entries, operations, and total text bytes. In ``DirtyOnly`` mode, one dirty member makes the complete repeated creator/keyword group eligible, so unchanged active members are retained while dirty tombstones can remove native values under ``ReplaceExisting``. C++ example ----------- .. code-block:: cpp #include "openmeta/metadata_transfer.h" #include "openmeta/metadata_translation.h" openmeta::MetadataDateTranslationOptions options; options.conflict_policy = openmeta::MetadataDateTranslationConflictPolicy::ReplaceExisting; openmeta::MetaStore translated; const openmeta::MetadataDateTranslationResult result = openmeta::translate_xmp_creation_dates(edited, options, &translated); if (result.status != openmeta::MetadataDateTranslationStatus::Ok) { // translated is unchanged. } openmeta::MetadataTechnicalTranslationOptions technical_options; technical_options.conflict_policy = openmeta::MetadataTechnicalTranslationConflictPolicy::ReplaceExisting; openmeta::MetaStore technical; const auto technical_result = openmeta::translate_xmp_technical_metadata( translated, technical_options, &technical); openmeta::MetadataCaptureTranslationOptions capture_options; capture_options.conflict_policy = openmeta::MetadataCaptureTranslationConflictPolicy::ReplaceExisting; openmeta::MetaStore capture; const auto capture_result = openmeta::translate_xmp_capture_metadata( technical, capture_options, &capture); openmeta::TransferTargetImageSpec target; target.has_dimensions = true; target.width = output_width; target.height = output_height; target.has_orientation = true; target.orientation = output_orientation; openmeta::MetaStore geometry; const auto geometry_result = openmeta::translate_xmp_image_geometry( capture, target, openmeta::MetadataGeometryTranslationOptions {}, &geometry); openmeta::MetadataDescriptiveTranslationOptions descriptive_options; descriptive_options.conflict_policy = openmeta::MetadataDescriptiveTranslationConflictPolicy::ReplaceExisting; openmeta::MetaStore descriptive; const auto descriptive_result = openmeta::translate_xmp_descriptive_metadata( geometry, descriptive_options, &descriptive); openmeta::MetadataLocationTranslationOptions location_options; location_options.conflict_policy = openmeta::MetadataDescriptiveTranslationConflictPolicy::ReplaceExisting; openmeta::MetaStore location; const auto location_result = openmeta::translate_xmp_location_metadata( descriptive, location_options, &location); Python ------ Python calls the same C++ transaction and returns a detached ``Document``: .. code-block:: python translated = edited.translate_creation_dates( conflict_policy=openmeta.MetadataDateTranslationConflictPolicy.ReplaceExisting, ) translated = translated.translate_technical_metadata( conflict_policy=openmeta.MetadataTechnicalTranslationConflictPolicy.ReplaceExisting, ) translated = translated.translate_capture_metadata( conflict_policy=openmeta.MetadataCaptureTranslationConflictPolicy.ReplaceExisting, ) translated = translated.translate_image_geometry(target_image_spec) translated = translated.translate_descriptive_metadata( conflict_policy=openmeta.MetadataDescriptiveTranslationConflictPolicy.ReplaceExisting, ) translated = translated.translate_location_metadata( conflict_policy=openmeta.MetadataDescriptiveTranslationConflictPolicy.ReplaceExisting, ) Invalid or lossy requests raise ``ValueError`` containing the C++ status, mapping, and source entry ID. The original ``Document`` is not mutated. Scope ----- This milestone is intentionally limited to exact creation-date, common technical and capture EXIF, target-bound orientation/stored dimensions, primary GPS coordinates/time/navigation, combined descriptive/editorial/workflow IPTC, flat IPTC Core locations, and selected structured-location reconciliation. It does not yet provide arbitrary EXIF/IPTC/XMP translation, broader target layout/storage projection, multilingual-alternative selection, timezone inference, numeric approximation or value repair, or automatic synchronization during transfer. Sensitivity companion writeback ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``translate_xmp_sensitivity_metadata(...)`` and Python ``Document.translate_sensitivity_metadata(...)`` use independent ``MetadataSensitivityTranslationOptions``, contract version 1. The seven ExifIFD fields form one transaction and one result group: .. list-table:: Sensitivity fields :header-rows: 1 * - exifEX property - Native tag/type * - PhotographicSensitivity - 0x8827 SHORT, 1..65535 * - SensitivityType - 0x8830 SHORT, 0..7 * - StandardOutputSensitivity - 0x8831 LONG, 1..4294967295 * - RecommendedExposureIndex - 0x8832 LONG, 1..4294967295 * - ISOSpeed - 0x8833 LONG, 1..4294967295 * - ISOSpeedLatitudeyyy - 0x8834 LONG, 1..4294967295 * - ISOSpeedLatitudezzz - 0x8835 LONG, 1..4294967295 Input values are unsigned scalar integers or decimal integer text with optional leading ``+``. Signed scalars, fractions, labels, zero sensitivity values, overflow, and malformed/indexed/qualified fields fail. This positive-value restriction is OpenMeta's bounded writeback policy. Exact namespaces are required. Legacy OpenMeta ``exif:ISO``, ``exif:ISOSpeedRatings``, and ``exif:ISOSpeedRatings[1]`` are base aliases; the six companion names in the older ``exif`` namespace are also accepted for historical portable packets. Duplicate properties or aliases fail even when equal. The existing ISO-only API retains its 1..65535 contract. An active group requires both base sensitivity and type. Type 0 means unknown; 1 selects SOS, 2 REI, 3 ISO speed, 4 SOS+REI, 5 SOS+ISO, 6 REI+ISO, and 7 all three. Any supplied selected parameters must equal the base value below 65535, or use base 65535 for values at or above that limit. Plural selected parameters must equal each other as full LONG values, including above the SHORT limit. Unselected parameters may differ. Optional selected parameters may be absent; a marker without its extended value remains a marker. No type or missing high-range value is inferred. Latitude values require both latitude fields and ISOSpeed. EXIF versions and unrelated exposure fields are retained. Defaults are DirtyOnly/FailOnConflict. One dirty recognized member selects the whole group, including clean active companions. All selects active sources and dirty tombstones. No selected source retains the native group. Once selected, missing or deleted optional sources specify absent target fields. A dirty base tombstone with no active group members requests complete removal; an orphan companion tombstone or partial required pair fails. Source XMP entries remain available for caller-controlled serialization. PreserveExisting retains the complete native group if any member exists. FailOnConflict requires the entire group to be absent or exactly equivalent, including presence, SHORT/LONG types and duplicate counts. ReplaceExisting updates the group and removes stale/duplicate native members. Validation runs before all policies, and failures preserve separate or aliased output. Limits are seven added entries, 1024 operations, 128 text bytes per property and 896 text bytes in total; callers may lower them. Preparation may allocate. Portable native output now uses ``exifEX`` for the six companions. A base with native SensitivityType uses ``exifEX:PhotographicSensitivity``; an ISO-only base retains ``exif:ISO``. These seven properties participate in the existing ``CanonicalizeManaged`` namespace policy. Complete generated groups round-trip through this API, including the full unsigned LONG range. Existing source XMP can be retained with ExistingWins; independently authored duplicate base aliases still require caller resolution. Snapshot persistence covers JPEG, Classic TIFF and BigTIFF creation, replacement and removal. The wire and companion rules follow the CIPA EXIF 2.32 English translation (section 4.6.6 and Annex G) and EXIF 2.31 XMP mapping tables. This bounded API is not a claim of complete current EXIF conformance or automatic reconciliation with every legacy ISO-array convention. References: `CIPA EXIF translation `_, `CIPA EXIF metadata for XMP `_. Camera, lens and spectral text writeback ---------------------------------------- ``translate_xmp_camera_text_metadata(...)`` and Python ``Document.translate_camera_text_metadata(...)`` use independent ``MetadataCameraTextTranslationOptions`` and the existing technical source-mode, conflict-policy, result and diagnostic enums. Contract version 1 adds six independent native targets in one transaction. .. list-table:: :header-rows: 1 * - Exact XMP property - Native ExifIFD tag - Option * - ``exif:SpectralSensitivity`` - ``0x8824`` ASCII - ``spectral_sensitivity_to_exif`` * - ``exifEX:CameraOwnerName`` - ``0xa430`` ASCII - ``camera_owner_name_to_exif`` * - ``exifEX:BodySerialNumber`` - ``0xa431`` ASCII - ``body_serial_number_to_exif`` * - ``exifEX:LensMake`` - ``0xa433`` ASCII - ``lens_make_to_exif`` * - ``exifEX:LensModel`` - ``0xa434`` ASCII - ``lens_model_to_exif`` * - ``exifEX:LensSerialNumber`` - ``0xa435`` ASCII - ``lens_serial_number_to_exif`` Here ``exif`` is ``http://ns.adobe.com/exif/1.0/`` and ``exifEX`` is ``http://cipa.jp/exif/1.0/``. The five identity fields also accept the same property names in the historical OpenMeta ``exif`` namespace. Prefix spelling is irrelevant; namespace URI and property path must match. ``aux:Lens``, ``aux:SerialNumber``, ``OwnerName``, generic ``Lens`` aliases and MakerNote identity inference are excluded. The tag types follow `Exif 2.32 `_; the canonical XMP identity mappings follow `CIPA metadata interchange guidance `_. An active value must be nonempty ``Text`` with ``Ascii`` or ``Utf8`` encoding, containing only printable ASCII bytes ``0x20..0x7e``. Leading/trailing spaces, punctuation and serial-number leading zeros are preserved. XMP decoding retains their boundary whitespace in description attributes, resource values and element text. Empty values, other value kinds or encodings fail with ``InvalidSourceValue``; NUL, controls, DEL and non-ASCII bytes fail with ``NonAsciiSource``. The bounded printable subset permits lossless portable XML round trips. No trimming, transliteration, Unicode conversion or ASTM spectral-curve grammar validation is performed. The latter remains the caller's responsibility; this API transports the supplied text. All six switches default to true. ``DirtyOnly`` selects each dirty property individually. ``All`` also selects active clean entries; deleted entries still require ``Dirty``. Missing or disabled properties retain native metadata. Eligible duplicates, including canonical/legacy alias pairs with equal values, fail with ``AmbiguousSource``. Indexed, qualified or nested forms of a selected property fail with ``UnsupportedSourceShape``. A selected dirty tombstone ignores its payload and removes only that native field under ``ReplaceExisting``. Each field has its own conflict decision and result group. ``FailOnConflict`` rejects a mismatch, ``PreserveExisting`` retains the conflicting field while other fields can translate, and ``ReplaceExisting`` updates the singleton and removes native duplicates. Native equivalence requires ``Text`` with ``Ascii`` or ``Utf8`` encoding and matching bytes after native terminal NUL bytes are removed; byte blobs, numbers and other encodings are conflicts. New values use native ASCII. All source validation, conflict decisions and resource checks finish before one commit. Failure leaves separate or aliased output unchanged. Source provenance and text ownership survive the source store's lifetime. Limits are 6 added entries, 1024 edit operations, 4096 bytes per property and 24576 total source-text bytes. Limits must be nonzero and at most those ceilings. At least one switch must be enabled. Existing technical and capture option layouts and existing enum values are unchanged. Portable native identity fields now use canonical ``exifEX`` names. That namespace is declared only when needed. Spectral sensitivity remains in ``exif``. ``CanonicalizeManaged`` recognizes all six fields. If callers retain legacy source XMP along with generated canonical identity properties, both aliases can remain under ordinary preservation policies; reverse translation rejects their ambiguity. Use ``CanonicalizeManaged`` when replacing those managed aliases. Shared tests cover all six fields, conflict and removal transactions, source shapes and budgets, portable XML and namespace policies, serialized snapshots through JPEG/Classic TIFF/BigTIFF, the Python wrapper and an installed shared library consumer. Related fields are implemented and reviewed together, with focused checks during development and one final platform matrix per stable batch. Lens specification and image identity use the separate contract below. APEX and focal-plane/subject arrays are covered by the batches below. Lens specification and image identity writeback ----------------------------------------------- ``translate_xmp_identity_metadata(...)`` and Python ``Document.translate_identity_metadata(...)`` use ``MetadataIdentityTranslationOptions``, contract version 2. Independent ``lens_specification_to_exif`` and ``image_unique_id_to_exif`` flags select two ExifIFD fields, with capture conflict policies and one transactional commit. * ``exifEX:LensSpecification`` (legacy ``exif`` accepted) maps to ``0xA432`` as four unsigned RATIONALs: minimum/maximum focal length, then minimum F-number at each endpoint. Positive focal lengths must be ordered. Apertures are positive or exactly ``0/0`` for unknown. One typed URational array or four dense ``[1]`` through ``[4]`` scalar properties are accepted. Numeric text converts exactly to unsigned 32-bit fractions; floating scalars, units, incomplete/mixed shapes and duplicate aliases fail. * ``exif:ImageUniqueID`` maps to ``0xA420`` as 32 ASCII hex characters plus the serializer's terminal NUL. Case and leading zeros are preserved. No trimming, case folding, UUID generation, inference or hyphen removal is performed. Under ``DirtyOnly``, a dirty lens member selects clean companions too. A lone root tombstone or four dirty indexed tombstones removes the lens field; partial member deletion fails. Image ID uses its own eligible scalar source/tombstone. Native equivalence requires the correct shape and value. Rational equivalence retains unreduced native fractions; ID equivalence allows one native terminal NUL. All validation, conflicts and limits precede changes, including aliased input/output. Preparation may allocate; the host synchronizes shared objects. Limits are 2 added entries, 1024 operations, 128 bytes per text member and 544 aggregate text bytes. Fixed four-element typed arrays do not consume text budget. Portable output emits a canonical ``exifEX`` RDF Seq with exact fraction text, including ``0/0``. Managed canonicalization recognizes its indexed shape. Generic validation accepts the exact unsigned lens aperture ``0/0`` slots, including validated authoring inputs; unrelated zero denominators remain errors. Malformed native lens/ID values are skipped. Decoding preserves ImageUniqueID boundary whitespace so malformed padded IDs remain detectable. This is a structural Exif 2.3 / CIPA XMP translation contract. It does not check UUID version, global uniqueness or capture-time identity retention policy and never upgrades ExifVersion. Hosts supply lens components in documented order: the store flattens RDF arrays and does not retain Seq/Bag container kind. See ``docs/translation.md`` for exact contracts, standards references and the focal-plane/subject batch below. Combined C++, Python, shared consumer and JPEG/Classic TIFF/BigTIFF snapshot checks cover both fields. Accepted exact clean natives gain Dirty while retaining lens fractions/unknown markers, ID case/NUL and wire provenance. Complete eligible deletion creates or reuses Dirty|Deleted intent even for absent keys. PreserveExisting retains an active owner without promotion; omitted or ineligible sources preserve the destination. Intent, promotions and duplicate removals consume existing budgets. Repeated calls reuse intent; failure retains source and output unchanged. APEX writeback (contract version 2) ----------------------------------- ``translate_xmp_apex_metadata`` and Python ``Document.translate_apex_metadata`` translate five independent scalar fields in one transaction. ``MetadataApexTranslationOptions`` uses the capture source modes, conflict policies, result and status types. .. list-table:: Exact exif-namespace APEX mappings :header-rows: 1 * - XMP source - ExifIFD target - Native type, count 1 * - ``ShutterSpeedValue`` - ``0x9201`` - SRATIONAL * - ``ApertureValue`` - ``0x9202`` - RATIONAL * - ``BrightnessValue`` - ``0x9203`` - SRATIONAL * - ``ExposureBiasValue`` or ``ExposureCompensation`` - ``0x9204`` - SRATIONAL * - ``MaxApertureValue`` - ``0x9205`` - RATIONAL The namespace must be ``http://ns.adobe.com/exif/1.0/``. All five independent ``*_value_to_exif`` switches default to true; at least one must remain enabled. The exact switch names are ``shutter_speed_value_to_exif``, ``aperture_value_to_exif``, ``brightness_value_to_exif``, ``exposure_bias_value_to_exif`` and ``max_aperture_value_to_exif``. Inputs are direct APEX values. No seconds/f-number conversion, exposure inference, cross-field consistency check or ExifVersion upgrade is performed. Scalar integers, the target rational type and ASCII/UTF-8/Unknown-encoding integer, decimal, scientific or fraction text are supported. Aperture fields accept nonnegative values, including zero; the other fields accept either sign. Denominators must be positive. Exact reduction uses bounded 64-bit intermediates and must fit 32-bit numerator/denominator components. Floating point, arrays, structured children, qualifiers, unit suffixes and invalid numeric text fail. Existing XMP decode whitespace normalization is unchanged. The usual brightness/bias range of -99.99 to 99.99 is not a hard limit. A brightness wire numerator of -1 denotes unknown before reduction. Typed ``SRational{-1,n}``, explicit fraction text ``-1/n`` or exact text ``Unknown`` creates or replaces the value with ``-1/1``; the positive denominator must fit INT32_MAX. Equivalent native unknown scalars retain their original encoding when gaining Dirty authority. Integer or single-number text inputs are finite: ``-1`` and ``-1.0`` write ``-2/2``; ``-0.5`` and ``-2/4`` write ``-2/4``. Finite reduced fractions with numerator -1 use ``-2/(2n)`` or fail with ValueOutOfRange if the doubled denominator cannot fit. Conflict comparison keeps unknown and finite values distinct. Missing sources retain native fields. Since 0.7.2, accepted dirty tombstones carry native removal intent even for absent keys; accepted exact clean native values gain Dirty while retaining scalar and wire provenance. Duplicate eligible aliases fail. Native equivalence requires the correct scalar type/count and rational value, with sentinel handling. Replacement repairs wrong types and duplicate entries. Parsing, conflicts and budgets finish before one commit, including aliased output. Preparation may allocate; the host synchronizes conflicting access to shared objects. Default/hard bounds are five added entries, 1024 operations, 128 text bytes per property and 640 total text bytes. Positive lower limits are supported. Exposure bias reuses the existing capture mapping and ``XmpExposureCompensation`` diagnostic. Option layouts and parsing stay the same; 0.7.2 extends native lifecycle authority in both bias entry points. In 0.5.3 generated portable XMP emits exact APEX fractions for all five tags. Native shutter ``6/1`` now emits ``6/1``; earlier output emitted seconds ``1/64``. Native aperture ``4/1`` now emits ``4/1``; earlier output emitted f-number text ``4.0``. Regenerate old packets from native EXIF before reverse translation; their units cannot be inferred reliably from the values alone. Brightness and the retained ``ExposureCompensation`` alias also emit exact fractions. Existing correctly typed scalar XMP APEX rationals retain exact wire fractions as well. Unknown brightness emits ``-1/1``. Wrong native types/counts and nonpositive denominators are omitted; large representable APEX values remain exact. Existing-XMP precedence remains unchanged; CanonicalizeManaged allows valid native replacements. Host FlatHost/Spec projections are unchanged. The structural definitions follow `CIPA DC-008-2012 `_ and XMP names follow `CIPA DC-X010-2017 `_. This is not a full conformance claim for all Exif tags or later revisions. The batch adds four native targets, bringing capture-related coverage to 41 targets across nine APIs at 0.5.3. The spatial batch below extends this coverage; downstream application acceptance is separate. Focal-plane and subject writeback (contract version 2) ------------------------------------------------------------ ``translate_xmp_capture_spatial_metadata``, ``MetadataCaptureSpatialTranslationOptions`` and Python ``Document.translate_capture_spatial_metadata`` provide three logical groups in one transaction. Sources use ``http://ns.adobe.com/exif/1.0/``. .. list-table:: Focal-plane and subject mappings :header-rows: 1 * - XMP property - ExifIFD target - Native shape - Group switch * - ``FocalPlaneXResolution`` - ``0xa20e`` - RATIONAL, count 1 - ``focal_plane_to_exif`` * - ``FocalPlaneYResolution`` - ``0xa20f`` - RATIONAL, count 1 - ``focal_plane_to_exif`` * - ``FocalPlaneResolutionUnit`` - ``0xa210`` - SHORT, count 1 - ``focal_plane_to_exif`` * - ``SubjectArea`` - ``0x9214`` - SHORT array, count 2, 3 or 4 - ``subject_area_to_exif`` * - ``SubjectLocation`` - ``0xa214`` - SHORT array, count 2 - ``subject_location_to_exif`` Focal-plane X/Y values are pixels per explicit unit for the recorded image. All three source fields must be present together. Positive scalar signed or unsigned integers, ``URational``, or ASCII/UTF-8 integer, decimal, scientific and fraction text are accepted for X/Y. Fractions reduce exactly using bounded 64-bit intermediates and must fit unsigned 32-bit components. Zero, negative values, zero denominators, floating point and approximation are rejected. The unit accepts integer 2 or 3, integer text with an optional leading plus, or the exact portable labels ``inches`` and ``cm``. Units 1, 4 and 5 and labels ``mm``/``um`` are outside this writeback contract. There is no default or conversion. SubjectArea holds X/Y for a point; a circle adds diameter; a rectangle adds width and height. SubjectLocation holds X/Y independently. Values are preserved as supplied relative to the upper-left origin before orientation processing. No rotation, crop, image-dimension check, positive-size rule or consistency relation between the two arrays is inferred. Every element may be 0..65535. Sources can be one root ``U16`` array or dense indexed members ``[1]`` through ``[N]``. Indexed members accept scalar signed/unsigned integers in range or ASCII/UTF-8 integer text, optionally prefixed with ``+``. They reject decimal/fraction text, float/rational values, nested arrays, qualifiers, duplicate or sparse indexes, leading-zero indexes and mixed root/indexed forms. SubjectArea requires 2..4 members; SubjectLocation requires exactly two. The store flattens RDF arrays, so callers must supply the documented order; Seq/Bag provenance is not retained. All three switches default to true, and at least one must remain enabled. DirtyOnly selects a group when any member is dirty; All also selects active clean sources. Selected groups include all active clean companions and dirty tombstones. Clean tombstones are ignored. Duplicate active/dirty sources fail, even when values agree. Disabled groups and unrelated namespaces are ignored. Every selected group must be wholly present or wholly deleted. An array root tombstone, or a complete dense deleted source array, removes the native array under ReplaceExisting. Focal-plane removal requires all three tombstones. Partial deletion fails instead of changing an array's meaning or leaving units behind. A missing group retains native values. PreserveExisting preserves the entire group if any native member exists; FailOnConflict requires all native members to match; ReplaceExisting repairs partial groups, wrong types and duplicates. Equality checks exact native type/count, rational value and ordered array contents. Results count up to three logical groups and five native entries. Since 0.8.4, accepted clean exact native fields gain Dirty authority through same-value updates retaining their original rational components, ordered arrays and wire provenance. Complete accepted deletion carries Dirty+Deleted intent for every owned native tag, even when it is absent from the source. Repeat calls reuse that intent. PreserveExisting retains the entire active native group without promotion. New markers, promotions and duplicate removals count against the existing limits before publication. Prepared TIFF/DNG bundles use the existing native ExifIFD merge and removal fields for all five spatial tags. Unselected destination entries, MakerNotes and media ranges remain preserved. The bounded ExifIFD allowlist now has 44 tags. Dirty native owners select merge mode. Live entries included by ordinary EXIF preparation, including clean entries, supply destination values. Destination entries absent from the prepared EXIF payload retain their existing values. Source snapshot version 1 retains the flags needed to regenerate these intents. Fresh emission and non-edit consumers reject removal intent they cannot express. Signatures, options and bundle layout remain unchanged; spatial contract version 2 identifies the extended lifecycle. Development ABI remains unfrozen 4. Parsing, conflicts and shared limits precede one commit, preserving separate and aliased output on failure. Success owns array storage and provenance. Default and hard bounds are five added entries, 1024 edit operations, 128 text bytes per property and 1152 total text bytes. Positive lower limits are allowed. Preparation may allocate; hosts synchronize conflicting shared-object access. In 0.5.4, native and existing typed XMP focal-plane rationals emit exact fractions. Earlier rounded decimal packets cannot recover the original exact fraction; regenerate them from native EXIF when exact recovery is required. Subject arrays emit ordered sequences and participate in managed replacement as arrays. Invalid native spatial types/counts/spans and nonpositive resolutions are omitted before claiming a property. Valid existing XMP remains available. Existing unit read labels, including mm/um, and default existing-XMP precedence remain. Host FlatHost/Spec and reader decode paths are unchanged. The field types and coordinate meanings follow `CIPA DC-008-2012, camera information tags `_; the XMP namespace and ordered-array mappings follow `CIPA DC-X010-2017 `_. Requiring explicit complete groups and positive resolutions is this bounded OpenMeta contract. This is not full Exif conformance or downstream acceptance. The spatial milestone brought coverage to 46 targets across ten APIs; 0.5.8 extends it to 65 targets across fifteen APIs. Additional capture and environment fields (0.5.6) ------------------------------------------------- ``translate_xmp_capture_additional_metadata`` adds FocalLengthIn35mmFilm A405 (SHORT count 1, 0 unknown), FileSource A300 (UNDEFINED count 1, codes 0..3), and SceneType A301 (UNDEFINED count 1, code 1). The explicit focal-length alias ``FocalLengthIn35mmFormat`` remains the portable spelling. XMP inputs accept integer scalars and decimal integer text without labels, units or inference. ``translate_xmp_environment_metadata`` accepts the CIPA namespace ``http://cipa.jp/exif/1.0/`` and explicit legacy EXIF namespace aliases. Duplicate eligible aliases and indexed/structured paths fail transactionally. Temperature 9400 (degrees Celsius), WaterDepth 9403 (metres), and CameraElevationAngle 9405 (degrees in [-180, 180)) use SRATIONAL count 1. Humidity 9401 (percent), Pressure 9402 (hPa), and Acceleration 9404 (mGal, 10^-5 m/s²) use RATIONAL count 1. No other physical ranges are imposed. Environment inputs accept matching typed rationals, integers and exact fraction/decimal/scientific text. Raw denominator 0xffffffff means unknown; signed fields represent it as -1. Recognition precedes reduction and preserves the numerator. Signed sentinel text accepts n/-1 or n/4294967295, unsigned text accepts n/4294967295, and ``Unknown`` uses numerator zero. Other denominators must be positive. Finite fractions that reduce to the reserved denominator fail. Portable output retains exact rational pairs. No float approximation or unit-suffix conversion is performed. Each call is bounded and transactional, including aliased output, dirty removals and native conflicts. Defaults are 3/6 added entries, 1024 primitive operations, 128 text bytes per property and 384/768 total text bytes. Python ``Document.translate_capture_additional_metadata`` and ``Document.translate_environment_metadata`` use the same policy. Multi-call publication and conflicting shared-object access remain host responsibilities. ``CanonicalizeManaged`` removes legacy environment aliases only when a valid native replacement is emitted; invalid native values preserve existing XMP. References: `CIPA Exif 2.32 `_ and `CIPA Exif metadata for XMP `_. In 0.8.6, both contracts are version 2. Accepted exact clean native values gain ``Dirty`` while retaining native fractions, unknown numerator/denominator bits, one-byte UNDEFINED codes and wire provenance. Native byte spans copy into the edit arena before publication. Complete eligible deletion creates or reuses ``Dirty|Deleted`` native intent even for absent keys. ``PreserveExisting`` keeps an active owner without promotion; omitted and ineligible sources preserve the destination. Existing preflight budgets include intent, promotion and duplicate removal. Repeated calls reuse intent and failure retains source/output unchanged. The shared ExifIFD merge/removal allowlist grows from 50 to 59 using existing prepared fields and snapshot v1. Signatures, options and bundle layout stay the same; development ABI 4 is unfrozen. Hosts synchronize shared objects. Image Encoding and Composite Capture (0.5.7) -------------------------------------------- ``translate_xmp_image_encoding_metadata`` / ``Document.translate_image_encoding_metadata`` cover Gamma, CompressedBitsPerPixel and ComponentsConfiguration. Rationals allow zero with a positive denominator and preserve exact fractions. Components are four codes in 0..6. Gamma uses CIPA ``exifEX`` with explicit legacy ``exif``; the other two use ``exif``. ``translate_xmp_composite_metadata`` / ``Document.translate_composite_metadata`` reconcile CompositeImage and its two companions as one bounded transaction. Codes are 0..3; companions require 2 or 3, and code 3 requires both. Source counts are total >=2 and used 0 (unavailable) or 2..total. The exposure payload has seven summary rationals, then SHORT m at offset 56. Summary 0/0 means unavailable. When m is zero the payload ends at 58 bytes. Otherwise SHORT n at 58 is positive and exactly m*n rational values follow at offset 60. The list contains at least two values, agrees with the total count, and has at most 4096 values. Finite seconds are nonnegative; totals and extrema are not inferred. Canonical XMP uses the full native names in ``http://cipa.jp/exif/1.0/`` and structured summary members with ordered Values. ExifTool short roots and legacy Adobe exif are explicit alternatives. Mixed aliases, sparse values and invalid group dependencies fail transactionally. Any selected member selects its complete source group; dirty tombstones can remove the list when m becomes zero. Native validation, typed editing and portable output share these contracts. New payloads are little-endian. Decoded big-endian bytes stay raw with ``EntryFlags::ValueBigEndian``; typed Set clears this flag for new little-endian bytes. Serialization and transfer convert the embedded fields. Snapshot v1 layout is unchanged; snapshots using the new flag require 0.5.7 or later. Direct EXIF serialization retains all six fields. Target transfer filters the three source encoding fields because they may not describe destination pixels. Preparation can allocate. Conflicting shared-object access remains host owned. See the detailed `field and binary-layout contracts `_. In 0.8.8, both translation contracts are version 2. Accepted exact clean native values gain ``Dirty`` without replacing their stored fractions, byte spans, byte-order flags or wire provenance. Complete eligible deletion creates or reuses typed ``Dirty|Deleted`` intent, including absent native fields. Under ``ReplaceExisting``, a selected valid composite group also records removal of omitted companions: source-only codes 0/1/2 can clear a destination's old group. ``PreserveExisting`` keeps an active native group without promotion; omitted and ineligible source properties leave the store unchanged. Promotion, deletion intent and duplicate removal consume the existing entry/operation budgets. Preflight failure leaves source and output unchanged, and repeated calls reuse intent. The shared ExifIFD merge/removal allowlist grows from 59 to 62 with A460-A462. Existing TIFF/BigTIFF/DNG preparation, snapshot v1 and vector/stream/package paths carry eligible composite edits. The three encoding fields remain excluded from target transfer, even when dirty or deleted: Gamma, compressed bits per pixel and component configuration describe source pixels. Direct canonical EXIF serialization retains them; callers own their correspondence to encoded pixels. Destination encoding metadata is preserved during composite merge. For an existing DNG, retain its original ``DNGVersion`` in the source metadata. DNG preparation synthesizes a version when the source lacks one; it cannot infer target context during preparation. No pixel transformation, RAW encoding or private-offset repair is performed. Append-based TIFF writing preserves retained image data; it does not promise whole-file identity after repeated edits. Public signatures, options and prepared-bundle/snapshot layouts remain unchanged; ABI 4 remains unfrozen and hosts synchronize shared objects. Structured Capture Data (0.5.8) =============================== ``translate_xmp_structured_capture_metadata(source, options, &output)`` adds four independent field groups in one transaction. Python exposes ``Document.translate_structured_capture_metadata(...)`` and returns a new document with the same policies and limits. Defaults select dirty sources and fail on native conflicts; preparation may allocate. The host synchronizes conflicting access to shared objects. * OECF (8828): Columns, Rows, Names and signed rational Values. * SpatialFrequencyResponse (A20C): Columns, Rows, Names and unsigned rational Values. * CFAPattern (A302): Columns, Rows and Values with color codes 0 through 6. * DeviceSettingDescription (A40B): Columns, Rows and ordered setting-string Values. The namespace is ``http://ns.adobe.com/exif/1.0/``. Dimensions are positive SHORT values. Tables require one NUL-free ASCII name per column and exactly Columns times Rows values. CFA requires the same number of color codes. Device dimensions describe the display; they do not determine the number of settings. At least one setting is required; individual strings may be empty. No dimensions, photographic relationships or CFA byte-order heuristics are inferred. Legacy SpatialFrequencyResponse 920C is outside this contract. Table values preserve explicit numerator/denominator pairs without reduction. OECF accepts signed 32-bit components, including negative denominators; SpatialFrequencyResponse accepts unsigned 32-bit components. Zero denominators are rejected. Device settings accept valid UTF-8 representable in XML 1.0. Embedded NULs, invalid Unicode and disallowed XML characters are rejected. Spaces, tabs, CR/LF, DEL and supplementary characters retain their values. The canonical spelling is ``Columns``. The published CIPA table spelling ``Columus`` is an explicit OECF/SFR input alias. Mixed aliases, qualifiers, sparse indices and root/child mixtures fail. Text lists use dense one-based paths such as ``OECF/Names[1]``. Numeric Values may instead be typed rational arrays or unsigned integer arrays for CFA. A root Bytes value is an alternative validated little-endian native payload. Any selected member selects the whole structure, including clean companions. All-deleted source members remove the field; partially populated active structures fail. PreserveExisting, FailOnConflict and ReplaceExisting act independently on the four fields, but any error rolls back the entire call. Default hard limits are 256 columns, 4096 values per field, 4096 bytes per text property, 1 MiB total source text per call, 1 MiB native payload per field, four added entries and 1024 edit operations. Options may lower these limits. They cannot increase them. Typed native editing validates the same wire layout and character rules; raw decoding retains malformed data for inspection. Portable output skips malformed native structures without suppressing valid existing XMP. Native UNDEFINED payloads use two SHORT dimensions. Tables then contain NUL-terminated ASCII names and rational pairs; CFA contains byte codes. Each device string has its own UTF-16 BOM and terminator. Newly authored strings use UTF-16LE; decoded strings may mix BOM byte orders. ``EntryFlags::ValueBigEndian`` records the order of native headers and table rational components without changing the raw bytes. EXIF serialization and transfer convert those numbers to the destination order while preserving string bytes. Typed replacement supplies a new little-endian payload and clears the provenance flag. Snapshot v1 has no layout change, but snapshots carrying big-endian values for these four tags require OpenMeta 0.5.8 or later. Older 0.5.7 readers only understand this flag for composite exposure data. Snapshots produced before 0.5.8 can lack byte-order provenance for these four fields. Reopen the original container with 0.5.8 when recovering those big-endian values; the old raw payload alone does not establish its byte order. In 0.8.9, the structured translation contract is version 2. Accepted exact clean native values gain ``Dirty`` while retaining their raw bytes, byte-order flags and wire provenance. Complete eligible deletion creates or reuses typed ``Dirty|Deleted`` native intent, including absent fields. ``PreserveExisting`` retains active native owners; omission and ineligible source mappings leave them unchanged. Existing entry, operation and payload budgets apply before publication. Partial deletion, malformed source or preflight failure leaves the source and output unchanged. Repeated calls reuse native intent. The shared TIFF ExifIFD merge/removal allowlist grows from 62 to 66 with these four tags. Native edits use the existing TIFF/BigTIFF/DNG, snapshot v1 and vector/stream/package paths. Byte-order conversion changes dimension and table rational words while preserving ASCII names and each UTF-16 string's BOM and bytes. Public signatures, options and storage layouts remain unchanged; ABI 4 remains unfrozen and hosts synchronize shared objects. Compatible-file transfer retains these four fields. Rendered-image transfer removes CFAPattern from both EXIF and XMP under the existing calibration policy. It does not transform image samples or infer a replacement pattern. This filtering applies to prepared source metadata. During a selective TIFF merge, a pre-existing destination CFAPattern remains when no removal intent selects it. An explicit dirty native CFAPattern deletion removes it in either transfer safety mode. Filtering alone does not create destination deletion intent. For compatible-file editing of an existing DNG, retain its original DNGVersion as source context, as described for the encoding/composite lifecycle above. Rendered-image filtering also removes this source version; DNG preparation then generates a version. To preserve an existing DNG container during these rendered-source metadata probes, prepare through the TIFF native edit route. This does not qualify rendered DNG pixels or DNG scaffold generation. The combined fixture now covers 65 distinct ExifIFD tags across fifteen APIs; target transfer retains 62 after its existing three encoding-field exclusions. The structured batch includes JPEG, classic TIFF and BigTIFF, both TIFF byte orders, snapshot persistence, source fallback, native-only and retained-XMP round trips, Unicode and transactional failures. This is a supported-profile inventory, not a claim of complete EXIF or arbitrary RDF editing. Layout references: CIPA DC-008-Translation-2023-E, Figures 16, 20, 21 and 23; CIPA DC-010-2024-E, corresponding EXIF/XMP structure mappings. Source PDFs and independent encoded-file qualification remain in OpenMeta-internal. EXIF Text and Version Metadata (0.5.9) ====================================== ``translate_xmp_exif_text_metadata`` and Python ``Document.translate_exif_text_metadata`` add one bounded transaction for the following properties. Exact canonical namespaces are required. .. list-table:: EXIF text mappings :header-rows: 1 * - XMP property - Native ExifIFD tag - Value contract * - exif:ExifVersion - 9000 - Four ASCII digits in UNDEFINED count 4; no NUL. * - exif:FlashpixVersion - A000 - ``0100`` in UNDEFINED count 4; no NUL. * - exif:UserComment - 9286 - Character-set prefix plus text; XMP scalar or ``[@xml:lang=x-default]``. * - exifEX:ImageTitle - A436 - ASCII or UTF-8 text; empty means unknown. * - exifEX:Photographer, ImageEditor - A437, A438 - ASCII or UTF-8 text; require explicit native IFD0 Artist. * - exifEX:CameraFirmware, RAWDevelopingSoftware, ImageEditingSoftware, MetadataEditingSoftware - A439–A43C - ASCII or UTF-8 text; require explicit native IFD0 Software. * - exifEX:CameraOwnerName, LensMake, LensModel - A430, A433, A434 - ASCII or UTF-8 text. These overlap the earlier camera-text API. Versions use four digits exactly. The API validates their syntax; accepting a future EXIF version number does not establish conformance with that future specification. FlashpixVersion is restricted to the defined ``0100`` value. Text accepts ASCII or UTF-8 ``MetaValue`` text with valid XML 1.0 characters. Whitespace, empty strings, carriage returns and supplementary Unicode characters are preserved. A leading BOM, embedded NUL, invalid UTF-8, UTF-16 source text or numeric source fails. Native type 2 and type 129 counts include one terminal NUL. Type 129 carries UTF-8 without a BOM. Plain ASCII output uses type 2. Decoded type 129 is retained on serialization, including ASCII-only payloads. The seven new tags and non-ASCII owner/lens text require an explicit effective ExifVersion of ``0300`` or later. The effective version is the native version after this call's selected conflict policy. The library does not upgrade it silently. Artist and Software must already exist as nonempty native text, with exactly one active entry. The caller supplies those values through authoring/editing; the translator does not infer them from photographer, editor or software details. General store validation checks field shapes and encodings. It is not a complete EXIF mandatory-tag/profile validator. These version and companion requirements belong to this translation contract. UserComment authoring uses ``ASCII\0\0\0`` for ASCII. Non-ASCII uses ``UNICODE\0`` followed by UTF-8 for EXIF 3, or UTF-16LE with a BOM for older or missing versions. UNDEFINED comments do not require a terminal NUL. Native projection recognizes explicit UTF-16 BOMs in either version family. Without a BOM, ``UNICODE`` follows the declared EXIF version: UTF-8 for EXIF 3 and the TIFF byte order for legacy UTF-16. The existing nonstandard ``UTF8`` marker is accepted for reading only. JIS and undefined character sets remain opaque; no guessed conversion or new charset dependency is introduced. Raw snapshots preserve the original bytes. An explicit semantic replacement preserves text rather than padding or the original character-set choice. Exact clean promotion in 0.8.10 retains both. Version changes cannot reinterpret retained comments or downgrade retained EXIF 3 text. Supply the comment source with ``ReplaceExisting``, or a dirty comment tombstone, when changing its encoding family. An explicit UTF-16 BOM keeps legacy comments readable independently of the version and TIFF byte order. The decoder records ``ValueBigEndian`` for native UserComment. Snapshot v1 layout and ABI 3 remain unchanged; BOM-less big-endian comment snapshots require a 0.5.9 reader. Older snapshots may lack provenance; reopen their original files with 0.5.9 to recover it. Portable output emits UserComment as one ``rdf:Alt`` with ``x-default``. Other language alternatives remain in XMP, including under ``CanonicalizeManaged``. The reverse API chooses only the scalar/default source. It rejects eligible scalar/default duplicates and unsupported structured/indexed shapes. No locale is selected automatically. Invalid native comments do not hide valid source XMP. The 0.8.10 native lifecycle uses EXIF text translation contract 2. Under FailOnConflict or ReplaceExisting, an exact clean valid native match gains Dirty while keeping raw bytes, encoding, endian flags and wire provenance. This includes comment prefixes, explicit BOMs and padding, and ASCII stored as type 129 under EXIF 3. Dirty exact matches remain unchanged; PreserveExisting retains active values. Explicit replacements use the authoring encodings above. A ReplaceExisting version-family change re-encodes a selected matching comment; a legacy downgrade also rewrites selected ASCII owner/lens type 129 as type 2. Unselected type 129 and EXIF 3-only tags still block a legacy downgrade. Artist/Software companion requirements apply to promotions too. Complete selected deletion preserves active native values under PreserveExisting, conflicts under FailOnConflict, and removes them under ReplaceExisting. Without an active value it promotes a clean tombstone, reuses a dirty tombstone, or adds an empty typed Dirty|Deleted marker: Bytes/U8 for versions and UserComment, Text for the other ten fields. Promotions and new markers consume an operation; new markers also consume an addition. Omitted or disabled sources retain native state, and all preflight failures leave output unchanged. All 13 fields now support native TIFF/BigTIFF/DNG selective merge/removal, including snapshot v1. The shared ExifIFD allowlist grows from 66 to 77; no public layout changes. Defaults follow the capture APIs: ``DirtyOnly`` and ``FailOnConflict``. ``PreserveExisting``, ``ReplaceExisting``, dirty tombstones, independent field switches, aliased source/output and complete failure rollback are supported. One call can add at most 13 entries, record 1,024 operations, read 65,536 text bytes per property and 1,048,576 total text bytes. Preparation may allocate; this is separate from prepared allocation-free patch application. Conflicting shared-object access remains the host's responsibility; no atomics or mutexes are added. Serializer type-129 support also covers the EXIF-permitted IFD0 text fields: ImageDescription, Make, Model, Software and Artist. Copyright remains outside this extension because its two-part NUL-separated form needs a separate contract. The older camera-text translator keeps its printable-ASCII contract. SpectralSensitivity, BodySerialNumber and LensSerialNumber remain outside the UTF-8 extension. The combined capture fixture covers **81 distinct ExifIFD tags across seventeen APIs**. Compatible-file transfer retains the six development/correction fields and still filters the three source-encoding fields for rendered-image output. IFD0 companion fields are separate from that inventory. Compatibility qualification found that the installed OIIO reader aborts with ``std::bad_alloc`` on type-129 JPEG metadata. An isolated diagnostic changed only those type codes and read successfully. This is an external reader limitation; valid EXIF 3 output keeps type 129. The installed ExifTool also interprets ``UNICODE`` as legacy UTF-16 regardless of ExifVersion. Direct TIFF byte checks qualify EXIF 3 text, ExifTool checks XMP, and FFmpeg verifies unchanged decoded pixels. Downstream application acceptance remains on hold. The wire rules follow CIPA EXIF 3.0 Table 4 and the type-129/tag definitions, and CIPA DC-010-2024 EXIF/XMP mappings. The original PDFs and the character-code table image remain in the private qualification evidence. EXIF 3.1 Development and Correction Data (0.5.11) =================================================== ``translate_xmp_development_correction_metadata`` and ``Document.translate_development_correction_metadata`` add one bounded transaction for A40D-A412. ``DevelopmentType`` is a SHORT with high and low bytes chosen from 1, 2, and 4. Its XMP resource uses the published ``DevelopmentCharacterstic`` and ``FactoryDefault`` children. A40E is UTF-8 TIFF type 129 even for ASCII text. A40F-A411 accept 0/1 and A412 accepts 0..3. The caller must provide ``exif_version`` 300 or 310 for a selected source. OpenMeta never changes or infers ``ExifVersion``. Root or indexed forms, partial resources, duplicate sources and invalid values fail atomically. Native validation, typed editing, portable XMP and TIFF serialization share the same rules. Compatible-file and rendered-image transfer retain present values; RAW and MakerNote processing history is not inferred. LearningOptOutIn and profile authoring are covered by the 0.5.12 sections below; complete-file profile validation remains separate. EXIF 3.1 LearningOptOutIn Data (0.5.12) ======================================= ``translate_xmp_learning_opt_out_in_metadata`` and its Python counterpart project one exact ``http://cipa.jp/exif/1.0/`` structure into ExifIFD tag ``0x9287`` (TIFF type 7). The source has ``NumberOfSets`` and either a typed ``Values`` array or dense scalar leaves ``Values[1]`` through ``[2*n]``. Usage/intention pairs require first usage zero, unique usage values 0..4 and intention values 0..2. Root values, sparse indexes, duplicate sources and partial deletion fail atomically. A dirty tombstone removes the native value only with ``ReplaceExisting``. The caller supplies ``exif_version`` 300 or 310 for selected sources. New payloads use canonical little-endian words; decoded big-endian values retain ``ValueBigEndian``. Defaults remain ``DirtyOnly`` and ``FailOnConflict`` and preparation may allocate while the commit is transactional. Profile and Authoring Data (0.5.12) =================================== ``translate_xmp_profile_metadata`` projects these exact properties in one bounded transaction: * ``tiff:ImageDescription`` -> IFD0 ``0x010E``; * ``tiff:Artist`` -> IFD0 ``0x013B``; * scalar ``tiff:Copyright`` -> IFD0 ``0x8298``; * ``exif:ColorSpace`` (1/65535 or ``sRGB``/``Uncalibrated``) -> ``0xA001``; * printable ASCII 8.3 ``exif:RelatedSoundFile`` -> ``0xA004``. Text encoding, legacy two-part Copyright conflict behavior, filename validation, limits, tombstones and host-owned synchronization are explicit. No ICC profile, image dimension, color agreement or audio-file association is inferred. Complete-file profile validation remains outside this contract. JP2 and JPH Qualification Boundary =================================== The existing JP2 writer path accepts boxed JPH brands, replaces selected Exif/XML carriers, preserves unknown UUID boxes and the codestream, and updates an existing ``jp2h/colr`` child without synthesizing ``jp2h``. Focused tests cover ordinary and extended boxes, terminal zero-length boxes, positional scans that skip codestream bytes, and both ``jp2`` and ``jph`` brands. Real-file ICC/read-back and OIIO/iRAW consumer acceptance remain external qualification gates. Capture and lens lifecycle (0.7.1) ---------------------------------- Capture, Flash, sensitivity and camera-text contracts are version 2. Accepted ExposureTime, FNumber, base ISO, Flash, FocalLength and lens make/model/serial owners create native Dirty+Deleted intent even when native keys are missing. Accepted clean exact values receive same-value Dirty updates that preserve bytes and wire provenance. Omission and conflict policies remain; intent and update costs are checked before transactional publication and repeat calls reuse intents. Hosts stage the separate calls before publishing an aggregate result. Full sensitivity owns ``8827`` and ``8830``--``8835`` as one validated group; optional absence becomes removal intent after complete validation. Basic ISO owns only ``8827`` and preserves companions; disable it when full sensitivity owns the source. Lens fields are independent. In 0.7.1, ExposureBiasValue, LensSpecification and camera-owner/body-serial paths retained their prior behavior. The 0.7.2 section below extends ExposureBiasValue. TIFF preparation merges unspecified destination ExifIFD records for these dirty edits. Bounded removal also accepts ``829A``, ``829D``, ``8827``, ``8830``--``8835``, ``9209``, ``920A`` and ``A433``--``A435``. MakerNote bytes and standard pointers are preserved as opaque records; private relocation or checksum repair is not provided. See :doc:`migration_0_7` for the unfrozen development ABI-4 policy. APEX and capture-settings lifecycle (0.7.2) ------------------------------------------- APEX and capture-settings contracts are version 2; general capture is version 3 because its ExposureCompensation mapping shares the APEX bias owner. Each of five APEX fields and twelve closed settings is independent. Accepted dirty owner deletion carries exact native Dirty+Deleted intent even when native keys are missing. Accepted clean exact values gain Dirty with original scalar and wire provenance. Omitted or ineligible owners preserve destination values. Existing conflict policies, exact APEX units, brightness unknown sentinel, accepted enum codes, aliases and source shapes remain. Added intent, clean-marker promotion and same-value authority updates consume preflight budgets. Repeat calls reuse intent; each translator publishes once or leaves output unchanged. Hosts stage separate calls before aggregate publication and disable one shared ExposureBias mapping when composing APEX and general capture. No inference is performed. TIFF/BigTIFF uses the existing removal list and merge flag for the added tags: ``8822``, ``9201``--``9205``, ``9207``, ``A217``, ``A401``--``A403``, ``A406``--``A40A`` and ``A40C``. The allowlist now has 39 tags. Unselected native records, opaque MakerNote bytes/original offsets, standard pointers and media remain preserved. Snapshot v1, bundle/API layouts, ABI 4, package-minor policy and unsupported-consumer rejection remain unchanged. GPS destination lifecycle (0.8.0) --------------------------------- All five GPS translators use contract version 2. Accepted dirty deletion carries native intent even for absent members; accepted exact clean members gain Dirty without changing native encoding. Omitted sources preserve the destination. Intent and authority updates consume existing transactional limits; repeated calls reuse them. Bounded TIFF/BigTIFF merges retain unselected destination GPS. Version cleanup depends on surviving destination values, and conflicting active versions fail before output. See :doc:`migration_0_8` for ownership and rebuilds.