openmeta/metadata_editing.h provides a bounded transactional editing contract
for the same logical fields accepted by the high-level Creation API. It edits
canonical portable-XMP entries in a finalized MetaStore without requiring a
host to work with namespace paths or entry IDs.
The API is experimental and versioned by
kMetadataEditingContractVersion == 1.
#include "openmeta/metadata_editing.h"
#include <array>
const std::array operations = {
openmeta::make_metadata_edit_set(
openmeta::make_metadata_creation_text(
openmeta::MetadataCreationFieldKind::Title, "Edited title")),
openmeta::make_metadata_edit_add(
openmeta::make_metadata_creation_text(
openmeta::MetadataCreationFieldKind::Keyword, "approved")),
openmeta::make_metadata_edit_remove(
openmeta::MetadataCreationFieldKind::Creator, 0),
};
openmeta::MetadataEditingRequest request;
request.operations = operations;
openmeta::MetaStore edited;
const openmeta::MetadataEditingResult result =
openmeta::edit_metadata(source, request, &edited);source must be finalized. The output is replaced only after every operation
has passed validation and the entire edit has committed. On failure, edited
is unchanged and failed_operation_index identifies the rejected operation
when available.
| Operation | Behavior |
|---|---|
Add |
Creates an absent singleton or appends a creator/keyword value. Adding an existing singleton is an explicit conflict. |
Set |
Replaces one active value. Existing key, origin, block, wire provenance, and flags are preserved; Dirty is added. |
Remove |
Marks one active value as `Deleted |
RemoveAll |
Tombstones every active occurrence. This can repair malformed duplicate singleton fields before a new value is added in the same transaction. |
Operations observe earlier operations in request order. For repeated creators
and keywords, occurrence is a zero-based index into the current active
logical values. Removing an occurrence shifts later values for subsequent
operations. New repeated values receive the next unused canonical property
index; existing index gaps are not renumbered.
Singleton fields accept only occurrence zero. If a malformed store contains
multiple active copies, single-value Set and Remove return
AmbiguousTarget; use RemoveAll followed by Add when that repair is
intended. Missing targets are errors rather than silent no-ops.
Set changes only the value and dirty flag. Remove changes only entry flags.
Both therefore retain the original block and wire provenance.
Add emits a new dirty canonical portable-XMP entry. When the store already
contains an XMP entry with a valid block, the new entry uses that block and a
later deterministic order. A finalized empty store can also be edited; its new
entry has no source block because no original carrier exists. Portable XMP and
transfer preparation can serialize that entry normally.
Editing does not compact tombstones automatically. Call the lower-level
compact(...) helper only when losing deleted-entry identity is appropriate
for the host workflow.
Add and Set use exactly the Creation field mapping and value validation.
Text must be non-empty valid UTF-8 and XML 1.0 character data. Orientation,
rating, dimensions, color space, ISO, and rational values use the constraints
documented in creation.md.
The hard maxima are:
1024operations1 MiBof UTF-8 text perAddorSetoperation8 MiBof UTF-8 text per request
MetadataEditingRequest::limits may lower but not raise these bounds. The
implementation keeps no global state. Concurrent calls are safe when callers
use distinct output stores and do not mutate the finalized source.
Python operation objects own their text and pass the same request to C++:
import openmeta
K = openmeta.MetadataCreationFieldKind
edited = document.edit_metadata([
openmeta.metadata_edit_set(
openmeta.metadata_creation_text(K.Title, "Edited title")),
openmeta.metadata_edit_add(
openmeta.metadata_creation_text(K.Keyword, "approved")),
openmeta.metadata_edit_remove(K.Creator, 0),
])The method returns a detached edited Document; the original document is not
mutated. The result can be queried, dumped as XMP, or converted into a transfer
snapshot. Invalid requests raise ValueError with the C++ status and rejected
operation index.
The logical API edits the 24 fields listed in creation.md.
The exact-key API below covers supported typed EXIF/IPTC/XMP/custom entries.
Structural block editing, a full EXIF/IPTC/XMP synchronization engine and direct
in-place file patching remain separate concerns. Supported edited creation dates can
be projected explicitly into native EXIF/IPTC groups before persistence; see
translation.md. Lower-level MetaEdit remains available for
entry-ID-based host code, while transfer and writer APIs handle container
persistence.
edit_metadata_typed(...) adds an experimental v1 C++ contract for exact
EXIF/TIFF, IPTC-IIM and XMP keys, including private EXIF and custom XMP. It uses
MetadataAuthoringEntry and the borrowed MetaValueView helpers from generic
authoring. It copies all supplied keys and payloads. The logical-field API above
continues to provide its own field-specific aliases and repeated-list behavior.
#include "openmeta/metadata_editing.h"
openmeta::MetadataTypedEditingOperation operation;
operation.kind = openmeta::MetadataEditingOperationKind::Set;
operation.entry.key = openmeta::make_exif_tag_key_view("exififd", 0xa405);
operation.entry.value = openmeta::make_value_view_u16(50);
const auto result = openmeta::edit_metadata_typed(
source, std::span(&operation, 1), &source);The finalized base and output may alias. Every supplied Add/Set value and the
complete final candidate must pass structural and selected schema validation
before publication. A failed request leaves both stores unchanged and reports
an operation index or final candidate entry when available. Values overwritten
later in the request must still be valid. A malformed unrelated base entry can
therefore fail final validation; the caller can select schema policy through
MetadataTypedEditingOptions::validation.
| Operation | Exact-key behavior |
|---|---|
| Add | Default FailIfPresent rejects an active exact key. Explicit Append permits another occurrence, subject to final schema singleton checks. New entries are dirty and have no original source block. |
| Set | Default kMetadataTypedEditingUniqueOccurrence requires exactly one active match. A numeric occurrence selects the zero-based current active match. The value and wire hints are replaced; source entry identity, block and order remain. Omitted wire hints request inference, and obsolete wire type names are cleared. |
| Remove | Uses the same occurrence rules, or kMetadataEditingAllOccurrences for all active matches. Tombstones preserve identity and provenance. Missing targets fail. Value and wire hints are ignored. |
Operations observe earlier edits, including additions and shifted occurrences.
Removing all duplicate singleton entries followed by Add can repair a store in
one transaction. XMP path indices remain part of the exact key; deleting
subject[1] does not renumber subject[2]. This API performs no alias matching,
namespace migration, container restructuring or automatic translation.
Defaults bound the request to 4096 operations, 8 MiB of borrowed key/payload bytes, 200000 output entries, 64 MiB of output arena/value bytes and 4096 bytes per key component. Remove-all expansion counts against the operation budget. Output limits include retained tombstones and arena bytes; editing does not compact them. Existing lower store ceilings remain in force. Preparation may allocate. The host must synchronize conflicting access to shared objects; there are no library atomics or mutexes in this path.
The combined capture fixture now edits typed XMP, translates sixteen API groups, serializes/restores a transfer snapshot, and checks native and portable values through JPEG, classic TIFF and BigTIFF add/replace workflows. Exact-key authoring and editing remain C++ APIs; Python retains logical editing and mirrors the capture translation calls.
The 0.5.8 structured capture batch adds OECF, SpatialFrequencyResponse,
CFAPattern and DeviceSettingDescription through
translate_xmp_structured_capture_metadata and its Python counterpart.
The 0.5.8 combined inventory was 65 distinct native tags across fifteen APIs
(62 retained by compatible-file transfer). ABI 3 and host synchronization
remain unchanged; big-endian snapshots for the four new fields need a 0.5.8
reader. See Structured capture data.
The 0.5.9 text/version batch adds UserComment, both version fields and seven EXIF 3 text tags. The new EXIF text API also supports UTF-8 owner/lens fields. The combined capture inventory is 75 distinct ExifIFD tags across sixteen APIs; compatible-file transfer retains 72. Version and Artist/Software companion requirements are explicit. ABI 3, snapshot v1 layout and host synchronization responsibilities remain unchanged. BOM-less big-endian UserComment snapshots require a 0.5.9 reader. See the EXIF text/version translation contract for limits and the recorded OIIO/ExifTool reader limitations.
Typed editing and native validation use the same A40D–A412 contracts as
translate_xmp_development_correction_metadata: packed DevelopmentType,
published DevelopmentCharacterstic spelling, UTF-8 type-129 description and
bounded correction codes. The reverse translator requires an explicit host
EXIF version policy of 0300 or 0310 and commits the complete selected batch
atomically. Transfer retains present values, including rendered-image transfer;
it does not infer RAW or MakerNote processing history. See
the translation contract.
Typed editing and native validation now share the bounded LearningOptOutIn and
profile translation contracts. LearningOptOutIn requires a complete
exifEX usage/intention structure and an explicit EXIF 0300/0310 host policy.
Profile writeback covers ImageDescription, Artist, scalar Copyright, ColorSpace
and RelatedSoundFile; it does not infer ICC, image or audio authority or impose
complete-file companions. Failed validation, limits, conflicts and tombstones
remain transactional. See the translation contract.