Skip to content

Commit e5af717

Browse files
committed
Add transfer-source snapshot API
Introduce a reusable decoded-source snapshot flow and fileless transfer helpers. Public API additions include TransferSourceSnapshot, read_transfer_source_snapshot_file/bytes, build_transfer_source_snapshot, prepare_metadata_for_target_snapshot, execute_prepared_transfer_snapshot (including an overload that accepts target bytes) and execute_prepared_transfer_bundle, plus related option/result structs and stable read error enums. Implementations were added to copy/finalize MetaStore state, decode from bytes/files with proper resource policy handling, and map decode/map failures into transfer statuses. XMP sidecar/embedded controls were extended with XmpExistingDestinationSidecarState and new execution options. Documentation and Sphinx quick-start/host-integration docs were updated with a "Read Once / Save Later" section and Python examples; Python bindings and tests were adjusted accordingly. Note: current snapshots are decoded-store-backed (not raw passthrough).
1 parent e7f424c commit e5af717

12 files changed

Lines changed: 5820 additions & 1003 deletions

docs/development.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,23 @@ Python transfer entry point:
12691269
- same probe contract, but allows `include_payloads=True` and returns raw
12701270
payload bytes (`bytes`) in `blocks[i].payload`.
12711271
- intended for explicit raw/unsafe workflows only.
1272+
- Snapshot/fileless Python helpers:
1273+
- `openmeta.read_transfer_source_snapshot_file(...)` and
1274+
`openmeta.read_transfer_source_snapshot_bytes(...)` expose the reusable
1275+
decoded-source contract directly.
1276+
- `Document.build_transfer_source_snapshot()` and
1277+
`openmeta.build_transfer_source_snapshot(document)` mirror the C++
1278+
`MetaStore -> TransferSourceSnapshot` builder.
1279+
- `openmeta.transfer_snapshot_probe(...)` /
1280+
`openmeta.transfer_snapshot_file(...)` expose the core snapshot-based
1281+
execute/persist path, including host-owned `target_bytes`.
1282+
- `openmeta.unsafe_transfer_snapshot_probe(...)` /
1283+
`openmeta.unsafe_transfer_snapshot_file(...)` add optional edited-output
1284+
bytes for explicit unsafe workflows.
1285+
- the Python transfer wrappers now distinguish
1286+
`xmp_existing_sidecar_base_path` from `xmp_sidecar_base_path`, and they
1287+
also expose `xmp_existing_destination_embedded_path` plus
1288+
`xmp_existing_destination_sidecar_state` for pathless host flows.
12721289

12731290
Transfer probe contract hardening (stable machine fields):
12741291
- `overall_status`, `overall_status_name`

docs/host_integration.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,74 @@ openmeta::PersistPreparedTransferFileResult saved =
253253
This path is usually simpler than a custom adapter when the container already
254254
exists.
255255
256+
### Read Once, Save Later
257+
258+
If your host already decoded source metadata during the initial load, keep a
259+
decoded source snapshot and execute the later save without reopening the
260+
source file:
261+
262+
```cpp
263+
#include "openmeta/metadata_transfer.h"
264+
265+
const openmeta::ReadTransferSourceSnapshotFileResult snapshot =
266+
openmeta::read_transfer_source_snapshot_file("source.jpg");
267+
268+
openmeta::ExecutePreparedTransferSnapshotOptions options;
269+
options.prepare.target_format = openmeta::TransferTargetFormat::Tiff;
270+
options.edit_target_path = "target.tif";
271+
options.execute.edit_apply = true;
272+
273+
openmeta::ExecutePreparedTransferFileResult result =
274+
openmeta::execute_prepared_transfer_snapshot(
275+
snapshot.snapshot, options);
276+
```
277+
278+
Python mirrors that same host-facing snapshot flow:
279+
280+
```python
281+
from pathlib import Path
282+
283+
import openmeta
284+
285+
snapshot_info = openmeta.read_transfer_source_snapshot_file("source.jpg")
286+
snapshot = snapshot_info["snapshot"]
287+
288+
result = openmeta.transfer_snapshot_file(
289+
snapshot,
290+
target_format=openmeta.TransferTargetFormat.Tiff,
291+
edit_target_path="target.tif",
292+
target_bytes=Path("target.tif").read_bytes(),
293+
output_path="edited.tif",
294+
)
295+
```
296+
297+
Current source snapshots are decoded-store-backed. They are intended for the
298+
normal EXIF/XMP/ICC/IPTC transfer flow, not raw source-packet passthrough.
299+
For hosts that still own the bundle/execution split, the lower-level
300+
`prepare_metadata_for_target_snapshot(...)` entry point remains available.
301+
If the host already has a decoded `MetaStore`, build a reusable snapshot with
302+
`build_transfer_source_snapshot(store)`. If it already owns the source bytes in
303+
memory, use `read_transfer_source_snapshot_bytes(bytes)` instead of the
304+
file-path reader.
305+
In Python, a previously decoded `Document` can be turned into a reusable
306+
snapshot through `doc.build_transfer_source_snapshot()` or
307+
`openmeta.build_transfer_source_snapshot(doc)`.
308+
If it also owns the destination bytes in memory, call the overload
309+
`execute_prepared_transfer_snapshot(snapshot, target_bytes, options)`.
310+
If it already holds a prepared bundle, use
311+
`execute_prepared_transfer_bundle(bundle, target_bytes, options)` instead.
312+
Snapshot execution supports the same existing-sidecar merge and destination
313+
carrier-precedence controls as the file helper; when loading an existing
314+
sidecar it defaults to `edit_target_path` unless
315+
`xmp_existing_sidecar_base_path` is set explicitly.
316+
For embedded-only writeback with sidecar cleanup and no filesystem path, set
317+
`xmp_existing_destination_sidecar_state` explicitly so OpenMeta can return a
318+
cleanup decision without guessing a sidecar location.
319+
Python now exposes those same split path/state controls directly:
320+
`xmp_existing_sidecar_base_path`, `xmp_sidecar_base_path`,
321+
`xmp_existing_destination_embedded_path`, and
322+
`xmp_existing_destination_sidecar_state`.
323+
256324
## 7. Use The Optional Adobe DNG SDK Bridge
257325

258326
If OpenMeta was built with `OPENMETA_WITH_DNG_SDK_ADAPTER=ON`, you can use the

docs/metadata_transfer_plan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,11 @@ Current behavior:
418418
`transfer_file(...)` and `unsafe_transfer_file(...)`, and the public Python
419419
wrapper uses that core helper instead of maintaining its own sidecar write
420420
and cleanup implementation
421+
- the Python binding now also exposes the reusable decoded-source path through
422+
`read_transfer_source_snapshot_file(...)`,
423+
`read_transfer_source_snapshot_bytes(...)`,
424+
`Document.build_transfer_source_snapshot()`, and
425+
`transfer_snapshot_probe(...)` / `transfer_snapshot_file(...)`
421426
- public API regression coverage now asserts dual-write roundtrip and
422427
persistence behavior across `jpeg`, `tiff`, `dng`, `png`, `webp`, `jp2`,
423428
`jxl`, `heif`, `avif`, and `cr3`

docs/quick_start.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,71 @@ Use the same pattern for other file-backed targets:
221221
- `TransferTargetFormat::Jxl`
222222
- bounded BMFF targets such as `Heif`, `Avif`, and `Cr3`
223223

224+
### Read Once And Reuse Later
225+
226+
If your application already decoded source metadata earlier, keep a decoded
227+
source snapshot and execute the later save without reopening the source file:
228+
229+
```cpp
230+
#include "openmeta/metadata_transfer.h"
231+
232+
const openmeta::ReadTransferSourceSnapshotFileResult snapshot =
233+
openmeta::read_transfer_source_snapshot_file("source.jpg");
234+
235+
openmeta::ExecutePreparedTransferSnapshotOptions options;
236+
options.prepare.target_format = openmeta::TransferTargetFormat::Tiff;
237+
options.edit_target_path = "target.tif";
238+
options.execute.edit_apply = true;
239+
240+
const openmeta::ExecutePreparedTransferFileResult result =
241+
openmeta::execute_prepared_transfer_snapshot(
242+
snapshot.snapshot, options);
243+
```
244+
245+
Python exposes the same reusable snapshot flow for host code:
246+
247+
```python
248+
from pathlib import Path
249+
250+
import openmeta
251+
252+
snapshot_info = openmeta.read_transfer_source_snapshot_file("source.jpg")
253+
snapshot = snapshot_info["snapshot"]
254+
255+
result = openmeta.transfer_snapshot_probe(
256+
snapshot,
257+
target_format=openmeta.TransferTargetFormat.Tiff,
258+
edit_target_path="target.tif",
259+
target_bytes=Path("target.tif").read_bytes(),
260+
)
261+
```
262+
263+
Current source snapshots are decoded-store-backed. They are meant for the
264+
common EXIF/XMP/ICC/IPTC transfer workflow, not raw source-packet passthrough.
265+
If the host owns the final file write path, the lower-level
266+
`prepare_metadata_for_target_snapshot(...)` entry point remains available.
267+
If the host already has a decoded `MetaStore`, build a reusable snapshot with
268+
`build_transfer_source_snapshot(store)`. If it already owns the source bytes in
269+
memory, use `read_transfer_source_snapshot_bytes(bytes)` instead of the
270+
file-path reader.
271+
In Python, if the host already has `doc = openmeta.read(...)`, call
272+
`doc.build_transfer_source_snapshot()` or
273+
`openmeta.build_transfer_source_snapshot(doc)` instead of reopening the file.
274+
If it also owns the destination bytes in memory, call the overload
275+
`execute_prepared_transfer_snapshot(snapshot, target_bytes, options)`.
276+
If it already holds a prepared bundle, use
277+
`execute_prepared_transfer_bundle(bundle, target_bytes, options)` instead.
278+
Snapshot execution supports the same existing-sidecar merge and destination
279+
carrier-precedence controls as the file helper; when loading an existing
280+
sidecar it defaults to `edit_target_path` unless
281+
`xmp_existing_sidecar_base_path` is set explicitly.
282+
For embedded-only writeback with sidecar cleanup and no filesystem path, set
283+
`xmp_existing_destination_sidecar_state` explicitly so OpenMeta can return a
284+
cleanup decision without guessing a sidecar location.
285+
The Python snapshot helpers intentionally cover the core transfer/edit/persist
286+
path. The older `transfer_probe(...)` / `unsafe_transfer_probe(...)` entry
287+
points remain the broader artifact-dump/debug surface.
288+
224289
## 8. Prepare Metadata For Host-Owned Encoders
225290

226291
Some applications do not want a file helper. They already own the encoder or

docs/sphinx/host_integration.rst

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,75 @@ helper instead of building your own writer path.
215215
openmeta::PersistPreparedTransferFileResult saved =
216216
openmeta::persist_prepared_transfer_file_result(exec, persist);
217217
218+
Read once, save later
219+
~~~~~~~~~~~~~~~~~~~~~
220+
221+
If your host already decoded source metadata during the initial load, keep a
222+
decoded source snapshot and execute the later save without reopening the
223+
source file.
224+
225+
.. code-block:: cpp
226+
227+
#include "openmeta/metadata_transfer.h"
228+
229+
openmeta::ReadTransferSourceSnapshotFileResult snapshot =
230+
openmeta::read_transfer_source_snapshot_file("source.jpg");
231+
232+
openmeta::ExecutePreparedTransferSnapshotOptions options;
233+
options.prepare.target_format = openmeta::TransferTargetFormat::Tiff;
234+
options.edit_target_path = "target.tif";
235+
options.execute.edit_apply = true;
236+
237+
openmeta::ExecutePreparedTransferFileResult result =
238+
openmeta::execute_prepared_transfer_snapshot(
239+
snapshot.snapshot, options);
240+
241+
Python mirrors that same host-facing snapshot flow:
242+
243+
.. code-block:: python
244+
245+
from pathlib import Path
246+
247+
import openmeta
248+
249+
snapshot_info = openmeta.read_transfer_source_snapshot_file("source.jpg")
250+
snapshot = snapshot_info["snapshot"]
251+
252+
result = openmeta.transfer_snapshot_file(
253+
snapshot,
254+
target_format=openmeta.TransferTargetFormat.Tiff,
255+
edit_target_path="target.tif",
256+
target_bytes=Path("target.tif").read_bytes(),
257+
output_path="edited.tif",
258+
)
259+
260+
Current source snapshots are decoded-store-backed. They are intended for the
261+
normal EXIF/XMP/ICC/IPTC transfer flow, not raw source-packet passthrough.
262+
If the host still owns the bundle/execution split, the lower-level
263+
``prepare_metadata_for_target_snapshot(...)`` entry point remains available.
264+
If the host already has a decoded ``MetaStore``, build a reusable snapshot with
265+
``build_transfer_source_snapshot(store)``. If it already owns the source bytes
266+
in memory, use ``read_transfer_source_snapshot_bytes(bytes)`` instead of the
267+
file-path reader.
268+
In Python, a previously decoded ``Document`` can be turned into a reusable
269+
snapshot through ``doc.build_transfer_source_snapshot()`` or
270+
``openmeta.build_transfer_source_snapshot(doc)``.
271+
If it also owns the destination bytes in memory, call the overload
272+
``execute_prepared_transfer_snapshot(snapshot, target_bytes, options)``.
273+
If it already holds a prepared bundle, use
274+
``execute_prepared_transfer_bundle(bundle, target_bytes, options)`` instead.
275+
Snapshot execution supports the same existing-sidecar merge and destination
276+
carrier-precedence controls as the file helper; when loading an existing
277+
sidecar it defaults to ``edit_target_path`` unless
278+
``xmp_existing_sidecar_base_path`` is set explicitly.
279+
For embedded-only writeback with sidecar cleanup and no filesystem path, set
280+
``xmp_existing_destination_sidecar_state`` explicitly so OpenMeta can return a
281+
cleanup decision without guessing a sidecar location.
282+
Python now exposes those same split path/state controls directly:
283+
``xmp_existing_sidecar_base_path``, ``xmp_sidecar_base_path``,
284+
``xmp_existing_destination_embedded_path``, and
285+
``xmp_existing_destination_sidecar_state``.
286+
218287
Optional Adobe DNG SDK bridge
219288
-----------------------------
220289

docs/sphinx/quick_start.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,73 @@ Copy metadata into an existing target
176176
Use the same pattern for ``Tiff``, ``Dng``, ``Png``, ``Webp``, ``Jp2``,
177177
``Jxl``, and bounded BMFF targets such as ``Heif``, ``Avif``, and ``Cr3``.
178178

179+
Read once and reuse later
180+
~~~~~~~~~~~~~~~~~~~~~~~~~
181+
182+
If your application already decoded source metadata earlier, keep a decoded
183+
source snapshot and execute the later save without reopening the source file.
184+
185+
.. code-block:: cpp
186+
187+
#include "openmeta/metadata_transfer.h"
188+
189+
openmeta::ReadTransferSourceSnapshotFileResult snapshot =
190+
openmeta::read_transfer_source_snapshot_file("source.jpg");
191+
192+
openmeta::ExecutePreparedTransferSnapshotOptions options;
193+
options.prepare.target_format = openmeta::TransferTargetFormat::Tiff;
194+
options.edit_target_path = "target.tif";
195+
options.execute.edit_apply = true;
196+
197+
openmeta::ExecutePreparedTransferFileResult result =
198+
openmeta::execute_prepared_transfer_snapshot(
199+
snapshot.snapshot, options);
200+
201+
Python exposes the same reusable snapshot flow for host code:
202+
203+
.. code-block:: python
204+
205+
from pathlib import Path
206+
207+
import openmeta
208+
209+
snapshot_info = openmeta.read_transfer_source_snapshot_file("source.jpg")
210+
snapshot = snapshot_info["snapshot"]
211+
212+
result = openmeta.transfer_snapshot_probe(
213+
snapshot,
214+
target_format=openmeta.TransferTargetFormat.Tiff,
215+
edit_target_path="target.tif",
216+
target_bytes=Path("target.tif").read_bytes(),
217+
)
218+
219+
Current source snapshots are decoded-store-backed. They are intended for the
220+
common EXIF/XMP/ICC/IPTC transfer workflow, not raw source-packet passthrough.
221+
If the host still owns the bundle/execution split, the lower-level
222+
``prepare_metadata_for_target_snapshot(...)`` entry point remains available.
223+
If the host already has a decoded ``MetaStore``, build a reusable snapshot with
224+
``build_transfer_source_snapshot(store)``. If it already owns the source bytes
225+
in memory, use ``read_transfer_source_snapshot_bytes(bytes)`` instead of the
226+
file-path reader.
227+
In Python, if the host already has ``doc = openmeta.read(...)``, call
228+
``doc.build_transfer_source_snapshot()`` or
229+
``openmeta.build_transfer_source_snapshot(doc)`` instead of reopening the
230+
file.
231+
If it also owns the destination bytes in memory, call the overload
232+
``execute_prepared_transfer_snapshot(snapshot, target_bytes, options)``.
233+
If it already holds a prepared bundle, use
234+
``execute_prepared_transfer_bundle(bundle, target_bytes, options)`` instead.
235+
Snapshot execution supports the same existing-sidecar merge and destination
236+
carrier-precedence controls as the file helper; when loading an existing
237+
sidecar it defaults to ``edit_target_path`` unless
238+
``xmp_existing_sidecar_base_path`` is set explicitly.
239+
For embedded-only writeback with sidecar cleanup and no filesystem path, set
240+
``xmp_existing_destination_sidecar_state`` explicitly so OpenMeta can return a
241+
cleanup decision without guessing a sidecar location.
242+
The Python snapshot helpers intentionally cover the core transfer/edit/persist
243+
path. The older ``transfer_probe(...)`` / ``unsafe_transfer_probe(...)``
244+
entry points remain the broader artifact-dump/debug surface.
245+
179246
Next steps
180247
----------
181248

0 commit comments

Comments
 (0)