Skip to content

Commit 5783aad

Browse files
committed
Merge existing destination embedded XMP support
Add support to include and merge existing embedded XMP from the edit target into generated transfer XMP with explicit precedence controls. Introduces XmpExistingDestinationEmbeddedMode and XmpExistingDestinationEmbeddedPrecedence enums and extends Prepare/Execute options and results to carry the requested merge and status information. Implements decode_existing_destination_embedded_xmp_into_store and helpers (value copying, append) to read, validate and merge XMP entries into the transfer MetaStore, and threads the feature through prepare/execute flows. Adds CLI and Python flags/arguments, updates the Python bindings and metatransfer tool to accept precedence and inclusion flags, and updates docs/README and tests to reflect the new behavior. Also includes TIFF/IFD pointer-preservation logic to preserve standard pointer tags (e.g. ExifIFD->InteropIFD) when replacing front subsets.
1 parent f15837c commit 5783aad

File tree

10 files changed

+1563
-11
lines changed

10 files changed

+1563
-11
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ In practice:
9696
chain rewrite (`ifd1`, `ifd2`, and preserved downstream tails), and bounded
9797
TIFF/DNG-style SubIFD rewrite with preserved downstream auxiliary tails
9898
and preserved trailing existing children when only the front subset is
99-
replaced.
99+
replaced. Replaced `ExifIFD` blocks can also preserve an existing target
100+
`InteropIFD` when the source does not supply its own interop child.
100101
- For DNG-like TIFF sources, the current bounded merge policy is:
101102
replace the source-supplied front preview-page/aux front structures, preserve
102103
existing target page tails and trailing auxiliary children.
@@ -118,11 +119,18 @@ In practice:
118119
the destination path into generated portable XMP when that bounded mode is
119120
requested, with explicit `sidecar_wins` or `source_wins` precedence against
120121
source-embedded existing XMP.
122+
- Transfer preparation and file-helper execution can also fold existing
123+
embedded XMP from the destination file into generated portable XMP when
124+
that bounded mode is requested, with explicit `destination_wins` or
125+
`source_wins` precedence against source-embedded existing XMP.
121126
- File-helper execution, `metatransfer`, and the Python transfer wrapper now
122127
share a bounded XMP carrier choice:
123128
embedded XMP only, sidecar-only writeback to a sibling `.xmp`, or dual
124129
embedded-plus-sidecar writeback when a generated XMP packet exists for the
125130
prepared transfer.
131+
- `metatransfer` and the Python transfer wrapper also expose the bounded
132+
destination-embedded merge controls directly instead of hiding them behind
133+
lower-level bindings.
126134
- Sidecar-only writeback also has an explicit destination embedded-XMP policy:
127135
preserve existing embedded XMP by default, or strip it for
128136
`jpeg`, `tiff`, `png`, `webp`, `jp2`, and `jxl`.

docs/metadata_transfer_plan.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ The first public write-side sync controls are also in place:
4747
| Target | Status | Current shape | Main limits |
4848
| --- | --- | --- | --- |
4949
| JPEG | First-class | Prepared bundle, compiled emit, byte-writer emit, edit planning/apply, file helper, bounded JUMBF/C2PA staging | Not a full arbitrary metadata editor yet |
50-
| TIFF | First-class | Prepared bundle, compiled emit, classic-TIFF and BigTIFF edit planning/apply, bounded `ifd1` chain rewrite with preserved downstream page tails, bounded TIFF/DNG-style SubIFD rewrite with preserved downstream auxiliary tails and preserved trailing existing children when only the front subset is replaced, file helper, streaming edit path | Broader TIFF/DNG rewrite coverage is still narrower than JPEG |
50+
| TIFF | First-class | Prepared bundle, compiled emit, classic-TIFF and BigTIFF edit planning/apply, bounded preview-page chain rewrite (`ifd1`, `ifd2`, and preserved downstream tails), bounded TIFF/DNG-style SubIFD rewrite with preserved downstream auxiliary tails and preserved trailing existing children when only the front subset is replaced, bounded `ExifIFD -> InteropIFD` preservation when a replaced ExifIFD omits its own interop child, file helper, streaming edit path | Broader TIFF/DNG rewrite coverage is still narrower than JPEG |
5151
| PNG | Bounded but real | Prepared bundle, compiled emit, bounded chunk rewrite/edit, file-helper roundtrip | Not a general PNG chunk editor |
5252
| WebP | Bounded but real | Prepared bundle, compiled emit, bounded chunk rewrite/edit, file-helper roundtrip | Not a general WebP chunk editor |
5353
| JP2 | Bounded but real | Prepared bundle, compiled emit, bounded box rewrite/edit, file-helper roundtrip | `jp2h` synthesis is still out of scope |
@@ -87,9 +87,10 @@ These support the public transfer flow:
8787
OpenMeta now has explicit end-to-end read-backed transfer tests for:
8888
- source JPEG -> JPEG edit/apply -> read-back
8989
- source JPEG -> TIFF edit/apply -> read-back
90-
- source JPEG -> TIFF edit/apply with `ifd1` -> read-back
91-
- source TIFF/BigTIFF with existing multi-page `ifd1 -> next` chain ->
92-
replace `ifd1` -> preserve downstream tail
90+
- source JPEG -> TIFF edit/apply with bounded preview-page chain ->
91+
read-back
92+
- source TIFF/BigTIFF with existing multi-page preview chain ->
93+
replace the front preview pages and preserve downstream tails
9394
- source DNG-like TIFF with `subifd0` + `ifd1` -> TIFF edit/apply -> read-back
9495
- source DNG-like TIFF with `subifd0` + `ifd1` -> BigTIFF edit/apply -> read-back
9596
- source TIFF/BigTIFF with existing `subifd0 -> next` auxiliary chain ->
@@ -135,6 +136,8 @@ Implemented:
135136
existing downstream auxiliary tail when `subifdN` is replaced
136137
- bounded front-subset `SubIFD` replacement that preserves trailing existing
137138
children from the target file
139+
- bounded `ExifIFD` replacement that preserves an existing target
140+
`InteropIFD` when the source replacement omits its own interop child
138141
- bounded DNG-style merge policy in the file-helper path:
139142
source-supplied preview/aux front structures replace the target front
140143
structures, while existing target page tails and trailing auxiliary
@@ -265,6 +268,14 @@ Current controls:
265268
- `xmp_existing_sidecar_precedence` on the file-read/prepare path:
266269
- `SidecarWins`
267270
- `SourceWins`
271+
- `xmp_existing_destination_embedded_mode` on the file-read/prepare and
272+
file-helper execution paths:
273+
- `Ignore`
274+
- `MergeIfPresent`
275+
- `xmp_existing_destination_embedded_precedence` on the file-read/prepare and
276+
file-helper execution paths:
277+
- `DestinationWins`
278+
- `SourceWins`
268279
- `xmp_writeback_mode` on the file-helper execution path:
269280
- `EmbeddedOnly`
270281
- `SidecarOnly`
@@ -275,6 +286,8 @@ Current controls:
275286
- CLI:
276287
- `--xmp-include-existing-sidecar`
277288
- `--xmp-existing-sidecar-precedence <sidecar_wins|source_wins>`
289+
- `--xmp-include-existing-destination-embedded`
290+
- `--xmp-existing-destination-embedded-precedence <destination_wins|source_wins>`
278291
- `--xmp-no-exif-projection`
279292
- `--xmp-no-iptc-projection`
280293
- `--xmp-conflict-policy <current|existing_wins|generated_wins>`
@@ -294,6 +307,12 @@ Current behavior:
294307
requested
295308
- that sidecar merge path now has explicit precedence against source-embedded
296309
existing XMP instead of relying on implicit decode order
310+
- existing embedded XMP from the destination file can also be merged into
311+
generated portable XMP on the file-read/prepare path and on the file-helper
312+
path when explicitly requested
313+
- that destination-embedded merge path has its own explicit precedence
314+
against source-embedded existing XMP instead of relying on implicit decode
315+
order
297316
- some targets without a native IPTC carrier can still use XMP as the bounded
298317
fallback carrier when IPTC projection is enabled
299318
- file-helper export can now strip prepared embedded XMP blocks and return
@@ -303,6 +322,8 @@ Current behavior:
303322
- the public `metatransfer` CLI and Python transfer wrapper can now persist
304323
that generated XMP as a sibling `.xmp` sidecar when sidecar or dual-write
305324
XMP writeback is selected
325+
- the public `metatransfer` CLI and Python transfer wrapper now also expose
326+
the bounded destination-embedded merge and precedence controls directly
306327
- sidecar-only writeback now has an explicit destination embedded-XMP policy:
307328
- preserve existing embedded XMP by default
308329
- strip existing embedded XMP for `jpeg`, `tiff`, `png`, `webp`, `jp2`,
@@ -323,7 +344,7 @@ This is deliberately narrower than a full sync engine. It does not yet define:
323344
- full EXIF vs XMP precedence rules
324345
- MWG-style reconciliation
325346
- full destination embedded-vs-sidecar reconciliation policy beyond the
326-
current bounded carrier modes and strip rules
347+
current bounded merge, precedence, carrier-mode, and strip rules
327348
- namespace-wide deduplication and normalization rules beyond the current
328349
generated-XMP path
329350

src/include/openmeta/metadata_transfer.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,17 +917,38 @@ enum class XmpExistingSidecarPrecedence : uint8_t {
917917
SourceWins,
918918
};
919919

920+
/// Existing destination embedded-XMP handling for file-helper transfer
921+
/// execution.
922+
enum class XmpExistingDestinationEmbeddedMode : uint8_t {
923+
Ignore,
924+
MergeIfPresent,
925+
};
926+
927+
/// Conflict precedence between merged destination embedded XMP and
928+
/// source-embedded existing XMP.
929+
enum class XmpExistingDestinationEmbeddedPrecedence : uint8_t {
930+
DestinationWins,
931+
SourceWins,
932+
};
933+
920934
/// File-read + decode options for \ref prepare_metadata_for_target_file.
921935
struct PrepareTransferFileOptions final {
922936
bool include_pointer_tags = true;
923937
bool decode_makernote = false;
924938
bool decode_embedded_containers = true;
925939
bool decompress = true;
926940
std::string xmp_existing_sidecar_base_path;
941+
std::string xmp_existing_destination_embedded_path;
927942
XmpExistingSidecarMode xmp_existing_sidecar_mode
928943
= XmpExistingSidecarMode::Ignore;
929944
XmpExistingSidecarPrecedence xmp_existing_sidecar_precedence
930945
= XmpExistingSidecarPrecedence::SidecarWins;
946+
XmpExistingDestinationEmbeddedMode
947+
xmp_existing_destination_embedded_mode
948+
= XmpExistingDestinationEmbeddedMode::Ignore;
949+
XmpExistingDestinationEmbeddedPrecedence
950+
xmp_existing_destination_embedded_precedence
951+
= XmpExistingDestinationEmbeddedPrecedence::DestinationWins;
931952

932953
OpenMetaResourcePolicy policy;
933954
PrepareTransferRequest prepare;
@@ -943,6 +964,11 @@ struct PrepareTransferFileResult final {
943964
TransferStatus xmp_existing_sidecar_status = TransferStatus::Unsupported;
944965
std::string xmp_existing_sidecar_message;
945966
std::string xmp_existing_sidecar_path;
967+
bool xmp_existing_destination_embedded_loaded = false;
968+
TransferStatus xmp_existing_destination_embedded_status
969+
= TransferStatus::Unsupported;
970+
std::string xmp_existing_destination_embedded_message;
971+
std::string xmp_existing_destination_embedded_path;
946972

947973
SimpleMetaResult read;
948974
PrepareTransferResult prepare;
@@ -1193,6 +1219,12 @@ struct ExecutePreparedTransferFileOptions final {
11931219
ExecutePreparedTransferOptions execute;
11941220
std::string edit_target_path;
11951221
std::string xmp_sidecar_base_path;
1222+
XmpExistingDestinationEmbeddedMode
1223+
xmp_existing_destination_embedded_mode
1224+
= XmpExistingDestinationEmbeddedMode::Ignore;
1225+
XmpExistingDestinationEmbeddedPrecedence
1226+
xmp_existing_destination_embedded_precedence
1227+
= XmpExistingDestinationEmbeddedPrecedence::DestinationWins;
11961228
XmpWritebackMode xmp_writeback_mode = XmpWritebackMode::EmbeddedOnly;
11971229
XmpDestinationEmbeddedMode xmp_destination_embedded_mode
11981230
= XmpDestinationEmbeddedMode::PreserveExisting;
@@ -1208,6 +1240,11 @@ struct ExecutePreparedTransferFileOptions final {
12081240
struct ExecutePreparedTransferFileResult final {
12091241
PrepareTransferFileResult prepared;
12101242
ExecutePreparedTransferResult execute;
1243+
bool xmp_existing_destination_embedded_loaded = false;
1244+
TransferStatus xmp_existing_destination_embedded_status
1245+
= TransferStatus::Unsupported;
1246+
std::string xmp_existing_destination_embedded_message;
1247+
std::string xmp_existing_destination_embedded_path;
12111248
bool xmp_sidecar_requested = false;
12121249
TransferStatus xmp_sidecar_status = TransferStatus::Unsupported;
12131250
std::string xmp_sidecar_message;

0 commit comments

Comments
 (0)