Skip to content

Commit 5aaf1c8

Browse files
committed
Add writer target contract docs and test tool hooks
Introduce a new Writer Target Contract (docs/sphinx/writer_target_contract.rst and docs/writer_target_contract.md) that documents bounded preserve/replace rules and host-owned target image specs. Update multiple docs (quick_start, host_integration, development, metadata_transfer_plan, interop_api, xmp_sync_policy, and Sphinx indices) to reference the new contract and to document the target_image_spec and matching metatransfer/Python CLI --target-* flags and examples. Update CMake to add optional OPENMETA_OIIOTOOL_EXECUTABLE and OPENMETA_EXIFTOOL_EXECUTABLE cache vars (with find_program fallbacks) and wire those into test/gate targets; add a metatransfer image usability test target and integrate the executables into existing transfer_release and CLI/metatransfer smoke gate flows. Add the new tests/metatransfer_image_usability_test.cmake and other test updates to exercise external image usability checks.
1 parent ace0842 commit 5aaf1c8

25 files changed

Lines changed: 5064 additions & 279 deletions

CMakeLists.txt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ if(OPENMETA_PYTHON_EXECUTABLE)
6565
set(Python_EXECUTABLE "${OPENMETA_PYTHON_EXECUTABLE}" CACHE FILEPATH "Python interpreter" FORCE)
6666
endif()
6767

68+
set(OPENMETA_OIIOTOOL_EXECUTABLE "" CACHE FILEPATH "Optional oiiotool executable for external image usability checks")
69+
if(NOT OPENMETA_OIIOTOOL_EXECUTABLE)
70+
find_program(_openmeta_oiiotool_executable
71+
NAMES oiiotool
72+
HINTS ${CMAKE_PREFIX_PATH}
73+
PATH_SUFFIXES bin)
74+
if(_openmeta_oiiotool_executable)
75+
set(OPENMETA_OIIOTOOL_EXECUTABLE "${_openmeta_oiiotool_executable}"
76+
CACHE FILEPATH "Optional oiiotool executable for external image usability checks" FORCE)
77+
endif()
78+
endif()
79+
80+
set(OPENMETA_EXIFTOOL_EXECUTABLE "" CACHE FILEPATH "Optional exiftool executable for external metadata checks")
81+
if(NOT OPENMETA_EXIFTOOL_EXECUTABLE)
82+
find_program(_openmeta_exiftool_executable NAMES exiftool)
83+
if(_openmeta_exiftool_executable)
84+
set(OPENMETA_EXIFTOOL_EXECUTABLE "${_openmeta_exiftool_executable}"
85+
CACHE FILEPATH "Optional exiftool executable for external metadata checks" FORCE)
86+
endif()
87+
endif()
88+
6889
# Optional path to local dependency repos (used for the FuzzTest wrapper).
6990
# Expected layout:
7091
# <root>/fuzztest
@@ -402,6 +423,20 @@ if(OPENMETA_BUILD_TOOLS)
402423
COMMENT "Running metatransfer smoke gate"
403424
VERBATIM
404425
)
426+
427+
if(OPENMETA_OIIOTOOL_EXECUTABLE)
428+
add_custom_target(openmeta_gate_metatransfer_image_usability
429+
DEPENDS metatransfer
430+
COMMAND ${CMAKE_COMMAND}
431+
"-DMETATRANSFER_BIN=$<TARGET_FILE:metatransfer>"
432+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
433+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
434+
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_metatransfer_image_usability"
435+
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/metatransfer_image_usability_test.cmake"
436+
COMMENT "Running metatransfer external image usability gate"
437+
VERBATIM
438+
)
439+
endif()
405440
endif()
406441

407442
if(TARGET metaread AND TARGET metadump AND TARGET thumdump
@@ -662,6 +697,8 @@ if(OPENMETA_BUILD_TESTS)
662697
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_transfer_release_gate"
663698
"-DOPENMETA_PYTHON_EXECUTABLE=${Python_EXECUTABLE}"
664699
"-DOPENMETA_PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}/python"
700+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
701+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
665702
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/transfer_release_gate.cmake"
666703
COMMENT "Running transfer release gate"
667704
VERBATIM
@@ -674,6 +711,8 @@ if(OPENMETA_BUILD_TESTS)
674711
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_transfer_release_gate"
675712
"-DOPENMETA_PYTHON_EXECUTABLE=${Python_EXECUTABLE}"
676713
"-DOPENMETA_PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}/python"
714+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
715+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
677716
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/transfer_release_gate.cmake"
678717
)
679718
elseif(TARGET metatransfer)
@@ -683,6 +722,8 @@ if(OPENMETA_BUILD_TESTS)
683722
"-DOPENMETA_TESTS_BIN=$<TARGET_FILE:openmeta_tests>"
684723
"-DMETATRANSFER_BIN=$<TARGET_FILE:metatransfer>"
685724
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_transfer_release_gate"
725+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
726+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
686727
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/transfer_release_gate.cmake"
687728
COMMENT "Running transfer release gate"
688729
VERBATIM
@@ -693,6 +734,8 @@ if(OPENMETA_BUILD_TESTS)
693734
"-DOPENMETA_TESTS_BIN=$<TARGET_FILE:openmeta_tests>"
694735
"-DMETATRANSFER_BIN=$<TARGET_FILE:metatransfer>"
695736
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_transfer_release_gate"
737+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
738+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
696739
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/transfer_release_gate.cmake"
697740
)
698741
endif()
@@ -729,6 +772,18 @@ if(OPENMETA_BUILD_TESTS)
729772
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_cli_metatransfer_smoke"
730773
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/cli_metatransfer_smoke_test.cmake"
731774
)
775+
776+
if(OPENMETA_OIIOTOOL_EXECUTABLE)
777+
add_test(
778+
NAME openmeta_cli_metatransfer_image_usability
779+
COMMAND ${CMAKE_COMMAND}
780+
"-DMETATRANSFER_BIN=$<TARGET_FILE:metatransfer>"
781+
"-DOIIOTOOL_BIN=${OPENMETA_OIIOTOOL_EXECUTABLE}"
782+
"-DEXIFTOOL_BIN=${OPENMETA_EXIFTOOL_EXECUTABLE}"
783+
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/_metatransfer_image_usability"
784+
-P "${CMAKE_CURRENT_SOURCE_DIR}/tests/metatransfer_image_usability_test.cmake"
785+
)
786+
endif()
732787
endif()
733788

734789
if(TARGET metaread AND TARGET metadump AND TARGET thumdump

docs/development.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ Portable sidecar note:
175175
--source-meta source.jpg \
176176
--target-tiff target.tif \
177177
-o injected.tif
178+
179+
#Inject target-owned image facts when source and output pixels differ
180+
./build/metatransfer \
181+
--target-jpeg target.jpg \
182+
--target-width 320 --target-height 240 \
183+
--target-samples-per-pixel 3 --target-bits-per-sample 8 \
184+
--target-photometric 2 --target-exif-color-space 1 \
185+
-o injected.jpg source.jpg
178186
```
179187

180188
`metatransfer` is a thin CLI wrapper over the public transfer APIs. It uses
@@ -189,6 +197,12 @@ exposes
189197
used, the CLI passes a `TransferByteWriter` sink into the shared execution
190198
path so edited output can stream directly to disk instead of always
191199
materializing a full output buffer.
200+
The `--target-width`, `--target-height`, `--target-orientation`,
201+
`--target-samples-per-pixel`, `--target-bits-per-sample`,
202+
`--target-sample-format`, `--target-photometric`,
203+
`--target-planar-configuration`, `--target-compression`, and
204+
`--target-exif-color-space` flags populate
205+
`PrepareTransferRequest::target_image_spec`.
192206
Current v1 behavior is:
193207

194208
- JPEG edit output is streamed directly from the shared core path.
@@ -1287,6 +1301,12 @@ Python transfer entry point:
12871301
`xmp_existing_sidecar_base_path` from `xmp_sidecar_base_path`, and they
12881302
also expose `xmp_existing_destination_embedded_path` plus
12891303
`xmp_existing_destination_sidecar_state` for pathless host flows.
1304+
- `openmeta.python.metatransfer` remains a thin command-line wrapper: its
1305+
`--xmp-writeback`, `--xmp-destination-embedded`,
1306+
`--xmp-destination-sidecar`, `--output`, and `--force` flags map directly
1307+
onto the C++ file-helper options and persistence flags. It reports the
1308+
sidecar and cleanup paths returned by the C++ result instead of deriving a
1309+
separate Python-side contract.
12901310

12911311
Transfer probe contract hardening (stable machine fields):
12921312
- `overall_status`, `overall_status_name`

docs/host_integration.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ For deterministic host compatibility baselines, see
1818
[compatibility_dump.md](compatibility_dump.md).
1919
For generated XMP merge and writeback precedence, see
2020
[xmp_sync_policy.md](xmp_sync_policy.md).
21+
For per-target writer preserve/replace guarantees, see
22+
[writer_target_contract.md](writer_target_contract.md).
2123

2224
## Pick The Integration Path
2325

@@ -328,6 +330,20 @@ Python now exposes those same split path/state controls directly:
328330
`xmp_existing_destination_embedded_path`, and
329331
`xmp_existing_destination_sidecar_state`.
330332

333+
The CLI and Python command-line wrapper do not implement their own transfer
334+
semantics. They map flags onto the same file-helper contract:
335+
- `--output` is the sidecar base for `sidecar` and `embedded_and_sidecar`
336+
writeback, so the generated sidecar is `output-stem.xmp`.
337+
- `--xmp-writeback sidecar` suppresses generated embedded XMP.
338+
- `--xmp-writeback embedded_and_sidecar` writes generated XMP to both the
339+
edited output and the generated sidecar.
340+
- embedded-only writeback preserves an existing destination sidecar unless
341+
`--xmp-destination-sidecar strip_existing` is selected.
342+
- sidecar-only writeback preserves existing destination embedded XMP unless
343+
`--xmp-destination-embedded strip_existing` is selected.
344+
- `--force` maps to the C++ persistence overwrite flags for the primary
345+
output and generated sidecar.
346+
331347
## 7. Query Runtime Capabilities
332348

333349
Hosts can ask OpenMeta what the current build supports before wiring format
@@ -395,6 +411,66 @@ openmeta::apply_prepared_dng_sdk_metadata(
395411
This bridge is for applications that already use the Adobe DNG SDK. OpenMeta
396412
still does not encode pixels or invent raw-image structure.
397413

414+
### Host-Owned Image Specs
415+
416+
If a transfer target is produced from a different image buffer than the source,
417+
the host writer owns the target image facts: dimensions, channel count, sample
418+
type, compression, orientation, colorspace, ICC profile, and TIFF strip/tile
419+
storage. OpenMeta does not infer those values from copied metadata. During
420+
prepared transfer it filters source EXIF/XMP image-layout fields so stale source
421+
properties are not written into a different output image.
422+
423+
Host code that encodes pixels should keep those fields from the target
424+
container or inject values derived from the actual output buffer. Enable source
425+
ICC transfer only when the host has verified that the profile matches the target
426+
pixel buffer; otherwise preserve or write the target profile.
427+
428+
```cpp
429+
openmeta::PrepareTransferRequest request;
430+
request.target_format = openmeta::TransferTargetFormat::Jpeg;
431+
432+
request.target_image_spec.has_dimensions = true;
433+
request.target_image_spec.width = encoded_width;
434+
request.target_image_spec.height = encoded_height;
435+
436+
request.target_image_spec.has_samples_per_pixel = true;
437+
request.target_image_spec.samples_per_pixel = 3;
438+
request.target_image_spec.bits_per_sample_count = 1;
439+
request.target_image_spec.bits_per_sample[0] = 8;
440+
request.target_image_spec.has_photometric_interpretation = true;
441+
request.target_image_spec.photometric_interpretation = 2; // RGB
442+
request.target_image_spec.has_exif_color_space = true;
443+
request.target_image_spec.exif_color_space = 1; // sRGB
444+
```
445+
446+
Python exposes the same structure as `openmeta.TransferTargetImageSpec` and the
447+
command-line wrappers pass it through without a separate policy layer:
448+
449+
```python
450+
spec = openmeta.TransferTargetImageSpec()
451+
spec.has_dimensions = True
452+
spec.width = encoded_width
453+
spec.height = encoded_height
454+
spec.has_samples_per_pixel = True
455+
spec.samples_per_pixel = 3
456+
spec.bits_per_sample = [8]
457+
spec.has_photometric_interpretation = True
458+
spec.photometric_interpretation = 2
459+
spec.has_exif_color_space = True
460+
spec.exif_color_space = 1
461+
```
462+
463+
For smoke testing the file-helper path, `metatransfer` and
464+
`python -m openmeta.python.metatransfer` expose equivalent flags:
465+
466+
```bash
467+
metatransfer --target-jpeg target.jpg -o output.jpg \
468+
--target-width 320 --target-height 240 \
469+
--target-samples-per-pixel 3 --target-bits-per-sample 8 \
470+
--target-photometric 2 --target-exif-color-space 1 \
471+
source.jpg
472+
```
473+
398474
## 9. Build `MetaStore` Yourself
399475

400476
If your application creates metadata directly, build the store first and then

docs/metadata_backend_matrix.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Date: March 5, 2026
77
Define one OpenMeta write/transfer contract that can target multiple container
88
backends without per-backend metadata logic duplication.
99

10+
For the public per-target preserve/replace guarantees, see
11+
[writer_target_contract.md](writer_target_contract.md).
12+
1013
## Capability Matrix
1114

1215
| Backend | Native metadata write primitives | Best use in OpenMeta |
@@ -143,6 +146,10 @@ backends without per-backend metadata logic duplication.
143146
now performs bounded metadata-only edit by appending one OpenMeta-authored
144147
metadata-only top-level `meta` box and replacing any prior OpenMeta-authored
145148
metadata-only `meta` box from the same bounded contract.
149+
- Embedded-XMP strip mode removes only OpenMeta-authored metadata `meta` boxes.
150+
Foreign BMFF XMP item graphs are preserved; recognized foreign XMP `mime`
151+
items, including `iinf` version 0/1/2 tables, make strip mode fail
152+
explicitly instead of silently claiming removal.
146153
- The same bounded BMFF edit contract now also participates in the core /
147154
file-helper C2PA signer path:
148155
- sign-request derivation

0 commit comments

Comments
 (0)