Skip to content

Commit a0e373f

Browse files
committed
Add XMP sidecar handling and conflict policy
Introduce explicit XMP conflict and sidecar handling for transfer preparation and execution. Key additions: - New enums: XmpConflictPolicy, XmpExistingSidecarMode, XmpExistingSidecarPrecedence, XmpWritebackMode. - Prepare/execute API extensions: PrepareTransferFileOptions/Result and ExecutePreparedTransferFileOptions/Result now carry sidecar paths, status, messages and generated_xmp_sidecar bytes; PrepareTransferRequest gains xmp_conflict_policy. - Support to discover, load and decode existing sibling .xmp sidecars (load_existing_xmp_sidecar_bytes, decode_existing_xmp_sidecar_into_store) and to merge them into the MetaStore when requested, with configurable precedence vs source-embedded XMP. - Portable XMP generation updated to honor conflict policy: claim/owner map replaces the simple key set and emits properties in ordered passes (EXIF/Existing/IPTC) according to CurrentBehavior/ExistingWins/GeneratedWins. - Prepared bundle now captures generated portable XMP bytes and execute_prepared_transfer_file can emit sidecar output and strip embedded XMP blocks when sidecar writeback is requested. - Python CLI, wrapper and module bindings extended: new arguments (--xmp-include-existing-sidecar, --xmp-existing-sidecar-precedence, --xmp-conflict-policy, --xmp-writeback), corresponding OpenMeta enums exposed to Python, and logic to write generated .xmp sidecars alongside edited outputs. - Documentation updated (README and docs/metadata_transfer_plan.md) describing the new knobs and behaviors. These changes make XMP generation, merging and writeback behavior explicit and configurable without implementing a full sync engine.
1 parent a6cdc79 commit a0e373f

11 files changed

Lines changed: 1683 additions & 138 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ In practice:
9292
- Writer-side sync behavior is now partially explicit instead of implicit:
9393
generated XMP can independently keep or suppress EXIF-derived and
9494
IPTC-derived projection during transfer preparation.
95+
- Generated portable XMP also has an explicit conflict policy for existing
96+
decoded XMP versus generated EXIF/IPTC mappings:
97+
current behavior, `existing_wins`, or `generated_wins`.
98+
- Transfer preparation can also fold an existing sibling `.xmp` sidecar from
99+
the destination path into generated portable XMP when that bounded mode is
100+
requested, with explicit `sidecar_wins` or `source_wins` precedence against
101+
source-embedded existing XMP.
102+
- File-helper execution, `metatransfer`, and the Python transfer wrapper now
103+
share a bounded XMP carrier choice:
104+
embedded XMP only, or sidecar-only writeback to a sibling `.xmp` when a
105+
generated XMP packet exists for the prepared transfer.
95106
- Prepared bundles record resolved policy decisions for MakerNote, JUMBF,
96107
C2PA, EXIF-to-XMP projection, and IPTC-to-XMP projection.
97108
- This is still not a full MWG-style sync engine. OpenMeta does not yet try to

docs/metadata_transfer_plan.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ For the XMP projection subjects, the current public knobs are intentionally
212212
simple:
213213
- EXIF-derived properties can be mirrored into generated XMP or suppressed
214214
- IPTC-derived properties can be mirrored into generated XMP or suppressed
215+
- generated portable XMP can choose how existing decoded XMP conflicts with
216+
generated EXIF/IPTC mappings
215217

216218
This gives callers stable write-side control over the most important projection
217219
behavior without forcing them to reverse-engineer the transfer output.
@@ -223,16 +225,43 @@ OpenMeta now has a bounded public sync-policy layer for generated XMP.
223225
Current controls:
224226
- `xmp_project_exif`
225227
- `xmp_project_iptc`
228+
- `xmp_conflict_policy`
229+
- `xmp_existing_sidecar_mode` on the file-read/prepare path:
230+
- `Ignore`
231+
- `MergeIfPresent`
232+
- `xmp_existing_sidecar_precedence` on the file-read/prepare path:
233+
- `SidecarWins`
234+
- `SourceWins`
235+
- `xmp_writeback_mode` on the file-helper execution path:
236+
- `EmbeddedOnly`
237+
- `SidecarOnly`
226238
- CLI:
239+
- `--xmp-include-existing-sidecar`
240+
- `--xmp-existing-sidecar-precedence <sidecar_wins|source_wins>`
227241
- `--xmp-no-exif-projection`
228242
- `--xmp-no-iptc-projection`
243+
- `--xmp-conflict-policy <current|existing_wins|generated_wins>`
244+
- `--xmp-writeback <embedded|sidecar>`
229245

230246
Current behavior:
231247
- existing XMP can still be included independently
232248
- EXIF payload emission stays independent from EXIF-to-XMP projection
233249
- IPTC native carrier emission stays independent from IPTC-to-XMP projection
250+
- portable generated XMP can keep the historical mixed order, prefer existing
251+
decoded XMP, or prefer generated EXIF/IPTC mappings when the same portable
252+
property would collide
253+
- existing sibling `.xmp` sidecars from the destination path can be merged
254+
into generated portable XMP before transfer packaging when explicitly
255+
requested
256+
- that sidecar merge path now has explicit precedence against source-embedded
257+
existing XMP instead of relying on implicit decode order
234258
- some targets without a native IPTC carrier can still use XMP as the bounded
235259
fallback carrier when IPTC projection is enabled
260+
- file-helper export can now strip prepared embedded XMP blocks and return
261+
canonical sidecar output guidance instead
262+
- the public `metatransfer` CLI and Python transfer wrapper can now persist
263+
that generated XMP as a sibling `.xmp` sidecar when sidecar writeback is
264+
selected
236265

237266
This is deliberately narrower than a full sync engine. It does not yet define:
238267
- full EXIF vs XMP precedence rules
@@ -274,7 +303,7 @@ the first public projection controls now exist.
274303

275304
Missing pieces include:
276305
- conflict resolution rules
277-
- sidecar vs embedded policy
306+
- broader sidecar vs embedded policy beyond the current bounded writeback mode
278307
- canonical writeback policy
279308
- broader namespace reconciliation behavior
280309

src/include/openmeta/metadata_transfer.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "openmeta/meta_store.h"
44
#include "openmeta/resource_policy.h"
55
#include "openmeta/simple_meta.h"
6+
#include "openmeta/xmp_dump.h"
67

78
#include <array>
89
#include <cstddef>
@@ -393,6 +394,7 @@ struct PreparedTransferBundle final {
393394
std::vector<PreparedTransferPolicyDecision> policy_decisions;
394395
std::vector<PreparedTransferBlock> blocks;
395396
std::vector<TimePatchSlot> time_patch_map;
397+
std::vector<std::byte> generated_xmp_sidecar;
396398
};
397399

398400
/// Options for explicit raw JUMBF append into a prepared JPEG bundle.
@@ -413,6 +415,8 @@ struct PrepareTransferRequest final {
413415
bool xmp_project_exif = true;
414416
bool xmp_project_iptc = true;
415417
bool xmp_include_existing = true;
418+
XmpConflictPolicy xmp_conflict_policy
419+
= XmpConflictPolicy::CurrentBehavior;
416420
bool xmp_exiftool_gpsdatetime_alias = false;
417421
};
418422

@@ -896,12 +900,30 @@ class TransferAdapterSink {
896900
= 0;
897901
};
898902

903+
/// Existing sibling XMP sidecar handling for transfer preparation.
904+
enum class XmpExistingSidecarMode : uint8_t {
905+
Ignore,
906+
MergeIfPresent,
907+
};
908+
909+
/// Conflict precedence between a merged destination-side `.xmp` and
910+
/// source-embedded existing XMP.
911+
enum class XmpExistingSidecarPrecedence : uint8_t {
912+
SidecarWins,
913+
SourceWins,
914+
};
915+
899916
/// File-read + decode options for \ref prepare_metadata_for_target_file.
900917
struct PrepareTransferFileOptions final {
901918
bool include_pointer_tags = true;
902919
bool decode_makernote = false;
903920
bool decode_embedded_containers = true;
904921
bool decompress = true;
922+
std::string xmp_existing_sidecar_base_path;
923+
XmpExistingSidecarMode xmp_existing_sidecar_mode
924+
= XmpExistingSidecarMode::Ignore;
925+
XmpExistingSidecarPrecedence xmp_existing_sidecar_precedence
926+
= XmpExistingSidecarPrecedence::SidecarWins;
905927

906928
OpenMetaResourcePolicy policy;
907929
PrepareTransferRequest prepare;
@@ -913,6 +935,10 @@ struct PrepareTransferFileResult final {
913935
PrepareTransferFileCode code = PrepareTransferFileCode::None;
914936
uint64_t file_size = 0;
915937
uint32_t entry_count = 0;
938+
bool xmp_existing_sidecar_loaded = false;
939+
TransferStatus xmp_existing_sidecar_status = TransferStatus::Unsupported;
940+
std::string xmp_existing_sidecar_message;
941+
std::string xmp_existing_sidecar_path;
916942

917943
SimpleMetaResult read;
918944
PrepareTransferResult prepare;
@@ -1135,11 +1161,19 @@ struct ExecutePreparedTransferResult final {
11351161
std::vector<std::byte> edited_output;
11361162
};
11371163

1164+
/// XMP carrier preference for file-helper transfer execution.
1165+
enum class XmpWritebackMode : uint8_t {
1166+
EmbeddedOnly,
1167+
SidecarOnly,
1168+
};
1169+
11381170
/// Options for \ref execute_prepared_transfer_file.
11391171
struct ExecutePreparedTransferFileOptions final {
11401172
PrepareTransferFileOptions prepare;
11411173
ExecutePreparedTransferOptions execute;
11421174
std::string edit_target_path;
1175+
std::string xmp_sidecar_base_path;
1176+
XmpWritebackMode xmp_writeback_mode = XmpWritebackMode::EmbeddedOnly;
11431177
bool c2pa_stage_requested = false;
11441178
PreparedTransferC2paSignerInput c2pa_signer_input;
11451179
bool c2pa_signed_package_provided = false;
@@ -1150,6 +1184,11 @@ struct ExecutePreparedTransferFileOptions final {
11501184
struct ExecutePreparedTransferFileResult final {
11511185
PrepareTransferFileResult prepared;
11521186
ExecutePreparedTransferResult execute;
1187+
bool xmp_sidecar_requested = false;
1188+
TransferStatus xmp_sidecar_status = TransferStatus::Unsupported;
1189+
std::string xmp_sidecar_message;
1190+
std::string xmp_sidecar_path;
1191+
std::vector<std::byte> xmp_sidecar_output;
11531192
};
11541193

11551194
/**

src/include/openmeta/xmp_dump.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@
1414

1515
namespace openmeta {
1616

17+
/// Conflict policy for existing XMP versus generated portable XMP properties.
18+
///
19+
/// This currently applies to portable XMP generation only.
20+
enum class XmpConflictPolicy : uint8_t {
21+
/// Preserve the historical OpenMeta order:
22+
/// EXIF-derived properties first, then existing XMP, then IPTC-derived
23+
/// properties.
24+
CurrentBehavior,
25+
/// Existing decoded XMP properties win over generated EXIF/IPTC mappings.
26+
ExistingWins,
27+
/// Generated EXIF/IPTC mappings win over existing decoded XMP properties.
28+
GeneratedWins,
29+
};
30+
1731
/// XMP dump result status.
1832
enum class XmpDumpStatus : uint8_t {
1933
Ok,
@@ -52,6 +66,9 @@ struct XmpPortableOptions final {
5266
///
5367
/// \note Currently only simple `property_path` values are emitted (no `/` nesting).
5468
bool include_existing_xmp = false;
69+
/// Conflict policy between existing decoded XMP and generated portable
70+
/// EXIF/IPTC mappings.
71+
XmpConflictPolicy conflict_policy = XmpConflictPolicy::CurrentBehavior;
5572
/// Emit `exif:GPSDateTime` instead of `exif:GPSTimeStamp` for GPS time.
5673
///
5774
/// Default keeps standard portable naming. This compatibility mode is
@@ -86,6 +103,8 @@ struct XmpSidecarRequest final {
86103
bool include_exif = true;
87104
bool include_iptc = true;
88105
bool include_existing_xmp = false;
106+
XmpConflictPolicy portable_conflict_policy
107+
= XmpConflictPolicy::CurrentBehavior;
89108
bool portable_exiftool_gpsdatetime_alias = false;
90109

91110
/// Lossless mode options (applied when format == Lossless).

0 commit comments

Comments
 (0)