Skip to content

Commit 1226e9d

Browse files
committed
Add XMP destination strip policies & persistence
Introduce explicit destination XMP handling and a bounded persistence helper. Adds enums XmpDestinationEmbeddedMode and XmpDestinationSidecarMode and new ExecutePreparedTransferFileOptions fields to control preserve/strip behavior for embedded XMP and sibling .xmp sidecars. propagate a strip_existing_xmp flag throughout JPEG/TIFF/PNG/WebP/JP2/JXL planning and emit paths so existing embedded XMP can be removed when sidecar-only writeback is selected for supported formats. Implement file helpers (path_exists, write_file_bytes, find_existing_xmp_sidecar_path), TIFF payload scrubbing, and related rewrite logic to remove XMP carriers as requested. Add persist_prepared_transfer_file_result and its options/result structs to let hosts persist edited output, write generated XMP sidecars, and optionally remove a stale destination sidecar without reimplementing CLI/python logic. execute_prepared_transfer_file now computes cleanup actions for destination sidecars and applies strip policies during planning/emit. Expose the new modes to the Python CLI and wrapper: new --xmp-destination-embedded and --xmp-destination-sidecar flags, wiring the Python wrapper to call transfer_file/unsafe_transfer_file when persisting output and to pass the new options through. Various minor messaging and validation updates accompany the new behavior.
1 parent 0ffcf8d commit 1226e9d

File tree

8 files changed

+2405
-326
lines changed

8 files changed

+2405
-326
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ In practice:
104104
embedded XMP only, sidecar-only writeback to a sibling `.xmp`, or dual
105105
embedded-plus-sidecar writeback when a generated XMP packet exists for the
106106
prepared transfer.
107+
- Sidecar-only writeback also has an explicit destination embedded-XMP policy:
108+
preserve existing embedded XMP by default, or strip it for
109+
`jpeg`, `tiff`, `png`, `webp`, `jp2`, and `jxl`.
110+
- Embedded-only writeback can also strip an existing sibling `.xmp`
111+
destination sidecar explicitly, so exports can move back to embedded-only
112+
XMP without leaving stale sidecar state behind.
113+
- C++ hosts now also have a bounded persistence helper for file-helper
114+
results, so edited output bytes, generated sidecars, and stale-sidecar
115+
cleanup can be applied without copying wrapper logic.
116+
- Python hosts also have matching `transfer_file(...)` and
117+
`unsafe_transfer_file(...)` bindings, and the public Python transfer wrapper
118+
now uses that same core-backed persistence path for real writes.
107119
- Prepared bundles record resolved policy decisions for MakerNote, JUMBF,
108120
C2PA, EXIF-to-XMP projection, and IPTC-to-XMP projection.
109121
- This is still not a full MWG-style sync engine. OpenMeta does not yet try to

docs/metadata_transfer_plan.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,18 @@ Current controls:
236236
- `EmbeddedOnly`
237237
- `SidecarOnly`
238238
- `EmbeddedAndSidecar`
239+
- `xmp_destination_embedded_mode` on the file-helper execution path:
240+
- `PreserveExisting`
241+
- `StripExisting`
239242
- CLI:
240243
- `--xmp-include-existing-sidecar`
241244
- `--xmp-existing-sidecar-precedence <sidecar_wins|source_wins>`
242245
- `--xmp-no-exif-projection`
243246
- `--xmp-no-iptc-projection`
244247
- `--xmp-conflict-policy <current|existing_wins|generated_wins>`
245248
- `--xmp-writeback <embedded|sidecar|embedded_and_sidecar>`
249+
- `--xmp-destination-embedded <preserve_existing|strip_existing>`
250+
- `--xmp-destination-sidecar <preserve_existing|strip_existing>`
246251

247252
Current behavior:
248253
- existing XMP can still be included independently
@@ -265,11 +270,27 @@ Current behavior:
265270
- the public `metatransfer` CLI and Python transfer wrapper can now persist
266271
that generated XMP as a sibling `.xmp` sidecar when sidecar or dual-write
267272
XMP writeback is selected
273+
- sidecar-only writeback now has an explicit destination embedded-XMP policy:
274+
- preserve existing embedded XMP by default
275+
- strip existing embedded XMP for `jpeg`, `tiff`, `png`, `webp`, `jp2`,
276+
and `jxl`
277+
- embedded-only writeback now has an explicit destination sidecar policy:
278+
- preserve an existing sibling `.xmp` by default
279+
- strip an existing sibling `.xmp` when explicitly requested
280+
- the C++ API now also has a bounded persistence helper for
281+
`execute_prepared_transfer_file(...)` results, so applications can write the
282+
edited file, write the generated `.xmp` sidecar, and remove a stale sibling
283+
`.xmp` without reimplementing wrapper-side file logic
284+
- the Python binding now exposes the same persistence path through
285+
`transfer_file(...)` and `unsafe_transfer_file(...)`, and the public Python
286+
wrapper uses that core helper instead of maintaining its own sidecar write
287+
and cleanup implementation
268288

269289
This is deliberately narrower than a full sync engine. It does not yet define:
270290
- full EXIF vs XMP precedence rules
271291
- MWG-style reconciliation
272-
- canonical destination embedded-vs-sidecar reconciliation policy
292+
- full destination embedded-vs-sidecar reconciliation policy beyond the
293+
current bounded carrier modes and strip rules
273294
- namespace-wide deduplication and normalization rules beyond the current
274295
generated-XMP path
275296

src/include/openmeta/metadata_transfer.h

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ struct PlanJpegEditOptions final {
452452
JpegEditMode mode = JpegEditMode::Auto;
453453
bool require_in_place = false;
454454
bool skip_empty_payloads = true;
455+
bool strip_existing_xmp = false;
455456
};
456457

457458
/// Planned JPEG edit summary (draft API).
@@ -460,6 +461,7 @@ struct JpegEditPlan final {
460461
JpegEditMode requested_mode = JpegEditMode::Auto;
461462
JpegEditMode selected_mode = JpegEditMode::MetadataRewrite;
462463
bool in_place_possible = false;
464+
bool strip_existing_xmp = false;
463465
uint32_t emitted_segments = 0;
464466
uint32_t replaced_segments = 0;
465467
uint32_t appended_segments = 0;
@@ -476,13 +478,15 @@ struct JpegEditPlan final {
476478
struct PlanTiffEditOptions final {
477479
/// If true, fail planning when the bundle has no TIFF-applicable updates.
478480
bool require_updates = true;
481+
bool strip_existing_xmp = false;
479482
};
480483

481484
/// Planned TIFF edit summary (draft API).
482485
struct TiffEditPlan final {
483486
TransferStatus status = TransferStatus::Ok;
484487
uint32_t tag_updates = 0;
485488
bool has_exif_ifd = false;
489+
bool strip_existing_xmp = false;
486490
uint64_t input_size = 0;
487491
uint64_t output_size = 0;
488492
std::string message;
@@ -1122,6 +1126,7 @@ struct ExecutePreparedTransferOptions final {
11221126
bool edit_requested = false;
11231127
bool edit_apply = false;
11241128
TransferByteWriter* edit_output_writer = nullptr;
1129+
bool strip_existing_xmp = false;
11251130
PlanJpegEditOptions jpeg_edit;
11261131
PlanTiffEditOptions tiff_edit;
11271132
};
@@ -1149,6 +1154,7 @@ struct ExecutePreparedTransferResult final {
11491154
std::vector<EmittedBmffPropertySummary> bmff_property_summary;
11501155
bool tiff_commit = false;
11511156
uint64_t emit_output_size = 0;
1157+
bool strip_existing_xmp = false;
11521158

11531159
bool edit_requested = false;
11541160
TransferStatus edit_plan_status = TransferStatus::Unsupported;
@@ -1168,13 +1174,30 @@ enum class XmpWritebackMode : uint8_t {
11681174
EmbeddedAndSidecar,
11691175
};
11701176

1177+
/// Destination embedded-XMP handling for file-helper transfer execution.
1178+
enum class XmpDestinationEmbeddedMode : uint8_t {
1179+
PreserveExisting,
1180+
StripExisting,
1181+
};
1182+
1183+
/// Destination sibling XMP sidecar handling for file-helper transfer
1184+
/// execution.
1185+
enum class XmpDestinationSidecarMode : uint8_t {
1186+
PreserveExisting,
1187+
StripExisting,
1188+
};
1189+
11711190
/// Options for \ref execute_prepared_transfer_file.
11721191
struct ExecutePreparedTransferFileOptions final {
11731192
PrepareTransferFileOptions prepare;
11741193
ExecutePreparedTransferOptions execute;
11751194
std::string edit_target_path;
11761195
std::string xmp_sidecar_base_path;
11771196
XmpWritebackMode xmp_writeback_mode = XmpWritebackMode::EmbeddedOnly;
1197+
XmpDestinationEmbeddedMode xmp_destination_embedded_mode
1198+
= XmpDestinationEmbeddedMode::PreserveExisting;
1199+
XmpDestinationSidecarMode xmp_destination_sidecar_mode
1200+
= XmpDestinationSidecarMode::PreserveExisting;
11781201
bool c2pa_stage_requested = false;
11791202
PreparedTransferC2paSignerInput c2pa_signer_input;
11801203
bool c2pa_signed_package_provided = false;
@@ -1190,8 +1213,57 @@ struct ExecutePreparedTransferFileResult final {
11901213
std::string xmp_sidecar_message;
11911214
std::string xmp_sidecar_path;
11921215
std::vector<std::byte> xmp_sidecar_output;
1216+
bool xmp_sidecar_cleanup_requested = false;
1217+
TransferStatus xmp_sidecar_cleanup_status = TransferStatus::Unsupported;
1218+
std::string xmp_sidecar_cleanup_message;
1219+
std::string xmp_sidecar_cleanup_path;
1220+
};
1221+
1222+
/// Options for \ref persist_prepared_transfer_file_result.
1223+
struct PersistPreparedTransferFileOptions final {
1224+
std::string output_path;
1225+
bool write_output = true;
1226+
bool overwrite_output = false;
1227+
uint64_t prewritten_output_bytes = 0;
1228+
bool overwrite_xmp_sidecar = false;
1229+
bool remove_destination_xmp_sidecar = true;
11931230
};
11941231

1232+
/// Result for \ref persist_prepared_transfer_file_result.
1233+
struct PersistPreparedTransferFileResult final {
1234+
TransferStatus status = TransferStatus::Unsupported;
1235+
std::string message;
1236+
1237+
TransferStatus output_status = TransferStatus::Unsupported;
1238+
std::string output_message;
1239+
std::string output_path;
1240+
uint64_t output_bytes = 0;
1241+
1242+
TransferStatus xmp_sidecar_status = TransferStatus::Unsupported;
1243+
std::string xmp_sidecar_message;
1244+
std::string xmp_sidecar_path;
1245+
uint64_t xmp_sidecar_bytes = 0;
1246+
1247+
TransferStatus xmp_sidecar_cleanup_status = TransferStatus::Unsupported;
1248+
std::string xmp_sidecar_cleanup_message;
1249+
std::string xmp_sidecar_cleanup_path;
1250+
bool xmp_sidecar_cleanup_removed = false;
1251+
};
1252+
1253+
/**
1254+
* \brief Persists edited output, generated XMP sidecar output, and any
1255+
* requested destination-sidecar cleanup from
1256+
* \ref execute_prepared_transfer_file.
1257+
*
1258+
* This is a bounded file helper for host applications that want the same
1259+
* output/sidecar behavior as the CLI or Python wrapper without reimplementing
1260+
* write and cleanup logic.
1261+
*/
1262+
PersistPreparedTransferFileResult
1263+
persist_prepared_transfer_file_result(
1264+
const ExecutePreparedTransferFileResult& prepared,
1265+
const PersistPreparedTransferFileOptions& options) noexcept;
1266+
11951267
/**
11961268
* \brief Materializes the final persisted package batch for one executed
11971269
* transfer state.

0 commit comments

Comments
 (0)