Skip to content

Commit c0dcbb2

Browse files
committed
Add DngTargetMode and DNG target path checks
Introduce a public DngTargetMode enum (ExistingTarget, TemplateTarget, MinimalFreshScaffold) and plumb it through the metadata transfer codepath, Python bindings, and CLI. Default remains MinimalFreshScaffold for metadata-only flows; ExistingTarget and TemplateTarget now require an explicit edit_target_path during execute, and prepare bundles record the chosen mode. The dng_sdk_adapter maps MinimalFreshScaffold to ExistingTarget for adapter compatibility. Added CLI flag (--dng-target-mode), Python parameter, unit tests, and updated README/docs to describe the public DNG transfer contract.
1 parent 5c64917 commit c0dcbb2

9 files changed

Lines changed: 286 additions & 8 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,14 @@ In practice:
114114
tails, preserved trailing existing auxiliary children, and bounded
115115
`ExifIFD -> InteropIFD` preservation. When a non-DNG source is merged into
116116
an existing DNG target, the target's core DNG tags and preview/raw
117-
structure are preserved under that same bounded contract.
117+
structure are preserved under that same bounded contract. The public DNG
118+
transfer contract is now explicit:
119+
- `ExistingTarget`
120+
- `TemplateTarget`
121+
- `MinimalFreshScaffold`
122+
Existing/template modes require a target path in the file-helper flow;
123+
minimal fresh scaffold keeps the metadata-only DNG prepare path available
124+
without claiming a full standalone DNG writer.
118125
- When built with `OPENMETA_WITH_DNG_SDK_ADAPTER=ON` and a `dng_sdk`
119126
package is available, OpenMeta also exposes
120127
[dng_sdk_adapter.h](src/include/openmeta/dng_sdk_adapter.h) as an optional

docs/metadata_transfer_plan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ Implemented:
158158
- EXIF, XMP, ICC, and IPTC transfer through the TIFF-family backend
159159
- read-backed file-helper roundtrip for JPEG-source and DNG-like-source input
160160
- minimal `DNGVersion` synthesis when the source metadata lacks it
161+
- explicit public target modes:
162+
- `ExistingTarget`
163+
- `TemplateTarget`
164+
- `MinimalFreshScaffold`
161165
- bounded preview-page chain rewrite/merge
162166
- bounded raw-image `SubIFD` rewrite/merge
163167
- preservation of existing target DNG core tags when a non-DNG source is
@@ -171,6 +175,9 @@ Implemented:
171175
Current limits:
172176
- still a bounded DNG policy layer, not a full DNG-specific rewrite engine
173177
- broader arbitrary nested-IFD graph rewrite is still out of scope
178+
- in the file-helper path, `ExistingTarget` and `TemplateTarget` now require
179+
an explicit target path; only `MinimalFreshScaffold` keeps the metadata-only
180+
prepare/emit path available without a backing DNG container
174181

175182
Optional host bridge:
176183
- When OpenMeta is built with `OPENMETA_WITH_DNG_SDK_ADAPTER=ON` and a

src/include/openmeta/metadata_transfer.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ enum class TransferTargetFormat : uint8_t {
4444
Dng,
4545
};
4646

47+
/// Public DNG target contract for metadata-only transfer workflows.
48+
enum class DngTargetMode : uint8_t {
49+
ExistingTarget,
50+
TemplateTarget,
51+
MinimalFreshScaffold,
52+
};
53+
4754
/// Prepared payload block category.
4855
enum class TransferBlockKind : uint8_t {
4956
Exif,
@@ -392,6 +399,7 @@ struct PreparedTransferBlock final {
392399
struct PreparedTransferBundle final {
393400
uint32_t contract_version = kMetadataTransferContractVersion;
394401
TransferTargetFormat target_format = TransferTargetFormat::Jpeg;
402+
DngTargetMode dng_target_mode = DngTargetMode::MinimalFreshScaffold;
395403
TransferProfile profile;
396404
PreparedTransferC2paRewriteRequirements c2pa_rewrite;
397405
std::vector<PreparedTransferPolicyDecision> policy_decisions;
@@ -409,6 +417,7 @@ struct AppendPreparedJpegJumbfOptions final {
409417
/// Request options for preparation.
410418
struct PrepareTransferRequest final {
411419
TransferTargetFormat target_format = TransferTargetFormat::Jpeg;
420+
DngTargetMode dng_target_mode = DngTargetMode::MinimalFreshScaffold;
412421
TransferProfile profile;
413422
bool include_exif_app1 = true;
414423
bool include_xmp_app1 = true;

src/openmeta/dng_sdk_adapter.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,10 @@ update_dng_sdk_stream_metadata_from_file(
388388
ApplyDngSdkMetadataFileResult out;
389389
PrepareTransferFileOptions prepare = options.prepare;
390390
prepare.prepare.target_format = TransferTargetFormat::Dng;
391+
if (prepare.prepare.dng_target_mode
392+
== DngTargetMode::MinimalFreshScaffold) {
393+
prepare.prepare.dng_target_mode = DngTargetMode::ExistingTarget;
394+
}
391395
out.prepared = prepare_metadata_for_target_file(path, prepare);
392396
if (out.prepared.file_status != TransferFileStatus::Ok) {
393397
out.adapter.status = DngSdkAdapterStatus::InvalidArgument;

src/openmeta/metadata_transfer.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9421,6 +9421,26 @@ namespace {
94219421
return TransferStatus::Malformed;
94229422
}
94239423

9424+
static bool
9425+
dng_target_mode_requires_existing_target(DngTargetMode mode) noexcept
9426+
{
9427+
return mode == DngTargetMode::ExistingTarget
9428+
|| mode == DngTargetMode::TemplateTarget;
9429+
}
9430+
9431+
static const char*
9432+
dng_target_mode_requires_path_message(DngTargetMode mode) noexcept
9433+
{
9434+
switch (mode) {
9435+
case DngTargetMode::ExistingTarget:
9436+
return "dng existing_target mode requires edit_target_path";
9437+
case DngTargetMode::TemplateTarget:
9438+
return "dng template_target mode requires edit_target_path";
9439+
case DngTargetMode::MinimalFreshScaffold: break;
9440+
}
9441+
return "dng target mode requires edit_target_path";
9442+
}
9443+
94249444
} // namespace
94259445

94269446
static PrepareTransferResult
@@ -9440,6 +9460,7 @@ prepare_metadata_for_target_impl(const MetaStore& store,
94409460

94419461
PreparedTransferBundle bundle;
94429462
bundle.target_format = request.target_format;
9463+
bundle.dng_target_mode = request.dng_target_mode;
94439464
bundle.profile = request.profile;
94449465

94459466
if (request.target_format != TransferTargetFormat::Jpeg
@@ -23609,6 +23630,30 @@ execute_prepared_transfer_file(
2360923630
return out;
2361023631
}
2361123632

23633+
if (out.prepared.bundle.target_format == TransferTargetFormat::Dng
23634+
&& dng_target_mode_requires_existing_target(
23635+
out.prepared.bundle.dng_target_mode)
23636+
&& options.edit_target_path.empty()) {
23637+
const char* const message = dng_target_mode_requires_path_message(
23638+
out.prepared.bundle.dng_target_mode);
23639+
out.execute.compile = skipped_emit_result(
23640+
"skipped emit due to missing required DNG target path");
23641+
out.execute.emit = out.execute.compile;
23642+
out.execute.edit_requested = true;
23643+
out.execute.edit_plan_status = TransferStatus::InvalidArgument;
23644+
out.execute.edit_plan_message = message;
23645+
out.execute.edit_apply.status = TransferStatus::InvalidArgument;
23646+
out.execute.edit_apply.code = EmitTransferCode::InvalidArgument;
23647+
out.execute.edit_apply.errors = 1U;
23648+
out.execute.edit_apply.message = message;
23649+
if (out.xmp_sidecar_requested) {
23650+
out.xmp_sidecar_status = TransferStatus::Unsupported;
23651+
out.xmp_sidecar_message = message;
23652+
out.xmp_sidecar_output.clear();
23653+
}
23654+
return out;
23655+
}
23656+
2361223657
if (strip_destination_sidecar) {
2361323658
const char* sidecar_base_path = nullptr;
2361423659
if (!options.xmp_sidecar_base_path.empty()) {

src/python/openmeta/python/metatransfer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def update_dng_sdk_file(
267267
path: str | os.PathLike[str],
268268
target_path: str | os.PathLike[str],
269269
*,
270+
dng_target_mode: object = openmeta.DngTargetMode.MinimalFreshScaffold,
270271
format: object = openmeta.XmpSidecarFormat.Portable,
271272
include_pointer_tags: bool = True,
272273
decode_makernote: bool = False,
@@ -294,6 +295,7 @@ def update_dng_sdk_file(
294295
return openmeta.update_dng_sdk_file_from_file(
295296
os.fspath(path),
296297
os.fspath(target_path),
298+
dng_target_mode=dng_target_mode,
297299
format=format,
298300
include_pointer_tags=include_pointer_tags,
299301
decode_makernote=decode_makernote,
@@ -377,6 +379,7 @@ def main(argv: list[str]) -> int:
377379
ap.add_argument("--target-jpeg", type=str, default="", help="target JPEG stream for edit/apply")
378380
ap.add_argument("--target-tiff", type=str, default="", help="target TIFF stream for edit/apply")
379381
ap.add_argument("--target-dng", type=str, default="", help="target DNG stream for edit/apply")
382+
ap.add_argument("--dng-target-mode", choices=["existing_target", "template_target", "minimal_fresh_scaffold"], default="minimal_fresh_scaffold", help="public DNG transfer contract for --target-dng")
380383
ap.add_argument("--source-meta", type=str, default="", help="source metadata file for edit/apply against a separate target file")
381384
ap.add_argument("--jpeg-c2pa-signed", type=str, default="", help="externally signed logical C2PA payload for JPEG, JXL, or bounded BMFF staging")
382385
ap.add_argument("--c2pa-manifest-output", type=str, default="", help="external manifest-builder output bytes for signed C2PA staging")
@@ -967,6 +970,11 @@ def main(argv: list[str]) -> int:
967970
xmp_existing_destination_carrier_precedence = (
968971
openmeta.XmpExistingDestinationCarrierPrecedence.EmbeddedWins
969972
)
973+
dng_target_mode = openmeta.DngTargetMode.MinimalFreshScaffold
974+
if args.dng_target_mode == "existing_target":
975+
dng_target_mode = openmeta.DngTargetMode.ExistingTarget
976+
elif args.dng_target_mode == "template_target":
977+
dng_target_mode = openmeta.DngTargetMode.TemplateTarget
970978

971979
for path in input_paths:
972980
source_path = args.source_meta if args.source_meta else path
@@ -989,6 +997,7 @@ def main(argv: list[str]) -> int:
989997
)
990998
common_kwargs = dict(
991999
target_format=target_format,
1000+
dng_target_mode=dng_target_mode,
9921001
format=sidecar_format,
9931002
include_pointer_tags=True,
9941003
decode_makernote=bool(args.makernotes),

src/python/src/openmeta_module.cc

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,8 @@ namespace {
16431643

16441644
static nb::dict update_dng_sdk_file_from_file_to_python(
16451645
const std::string& source_path, const std::string& target_path,
1646-
XmpSidecarFormat format, bool include_pointer_tags,
1646+
DngTargetMode dng_target_mode, XmpSidecarFormat format,
1647+
bool include_pointer_tags,
16471648
bool decode_makernote, bool decode_embedded_containers,
16481649
bool decompress, bool include_exif_app1, bool include_xmp_app1,
16491650
bool include_icc_app2, bool include_iptc_app13,
@@ -1662,6 +1663,7 @@ namespace {
16621663
= decode_embedded_containers;
16631664
options.prepare.decompress = decompress;
16641665
options.prepare.prepare.target_format = TransferTargetFormat::Dng;
1666+
options.prepare.prepare.dng_target_mode = dng_target_mode;
16651667
options.prepare.prepare.xmp_portable
16661668
= (format == XmpSidecarFormat::Portable);
16671669
options.prepare.prepare.include_exif_app1 = include_exif_app1;
@@ -1790,6 +1792,7 @@ namespace {
17901792

17911793
static nb::dict transfer_probe_to_python(
17921794
const std::string& path, TransferTargetFormat target_format,
1795+
DngTargetMode dng_target_mode,
17931796
XmpSidecarFormat format, bool include_pointer_tags,
17941797
bool decode_makernote, bool decode_embedded_containers, bool decompress,
17951798
bool include_exif_app1, bool include_xmp_app1, bool include_icc_app2,
@@ -1841,6 +1844,7 @@ namespace {
18411844
prepare_options.decode_embedded_containers = decode_embedded_containers;
18421845
prepare_options.decompress = decompress;
18431846
prepare_options.prepare.target_format = target_format;
1847+
prepare_options.prepare.dng_target_mode = dng_target_mode;
18441848
prepare_options.prepare.xmp_portable = (format
18451849
== XmpSidecarFormat::Portable);
18461850
prepare_options.prepare.include_exif_app1 = include_exif_app1;
@@ -4012,6 +4016,12 @@ NB_MODULE(_openmeta, m)
40124016
.value("EmbeddedWins",
40134017
XmpExistingDestinationCarrierPrecedence::EmbeddedWins);
40144018

4019+
nb::enum_<DngTargetMode>(m, "DngTargetMode")
4020+
.value("ExistingTarget", DngTargetMode::ExistingTarget)
4021+
.value("TemplateTarget", DngTargetMode::TemplateTarget)
4022+
.value("MinimalFreshScaffold",
4023+
DngTargetMode::MinimalFreshScaffold);
4024+
40154025
nb::enum_<TransferTargetFormat>(m, "TransferTargetFormat")
40164026
.value("Jpeg", TransferTargetFormat::Jpeg)
40174027
.value("Tiff", TransferTargetFormat::Tiff)
@@ -5077,6 +5087,7 @@ NB_MODULE(_openmeta, m)
50775087
m.def(
50785088
"transfer_probe",
50795089
[](const std::string& path, TransferTargetFormat target_format,
5090+
DngTargetMode dng_target_mode,
50805091
XmpSidecarFormat format, bool include_pointer_tags,
50815092
bool decode_makernote, bool decode_embedded_containers,
50825093
bool decompress, bool include_exif_app1, bool include_xmp_app1,
@@ -5115,7 +5126,8 @@ NB_MODULE(_openmeta, m)
51155126
XmpDestinationEmbeddedMode xmp_destination_embedded_mode,
51165127
XmpDestinationSidecarMode xmp_destination_sidecar_mode) {
51175128
return transfer_probe_to_python(
5118-
path, target_format, format, include_pointer_tags,
5129+
path, target_format, dng_target_mode, format,
5130+
include_pointer_tags,
51195131
decode_makernote, decode_embedded_containers, decompress,
51205132
include_exif_app1, include_xmp_app1, include_icc_app2,
51215133
include_iptc_app13, xmp_include_existing,
@@ -5144,6 +5156,7 @@ NB_MODULE(_openmeta, m)
51445156
true);
51455157
},
51465158
"path"_a, "target_format"_a = TransferTargetFormat::Jpeg,
5159+
"dng_target_mode"_a = DngTargetMode::MinimalFreshScaffold,
51475160
"format"_a = XmpSidecarFormat::Portable,
51485161
"include_pointer_tags"_a = true, "decode_makernote"_a = false,
51495162
"decode_embedded_containers"_a = true, "decompress"_a = true,
@@ -5193,6 +5206,7 @@ NB_MODULE(_openmeta, m)
51935206
m.def(
51945207
"unsafe_transfer_probe",
51955208
[](const std::string& path, TransferTargetFormat target_format,
5209+
DngTargetMode dng_target_mode,
51965210
XmpSidecarFormat format, bool include_pointer_tags,
51975211
bool decode_makernote, bool decode_embedded_containers,
51985212
bool decompress, bool include_exif_app1, bool include_xmp_app1,
@@ -5231,7 +5245,8 @@ NB_MODULE(_openmeta, m)
52315245
XmpDestinationEmbeddedMode xmp_destination_embedded_mode,
52325246
XmpDestinationSidecarMode xmp_destination_sidecar_mode) {
52335247
return transfer_probe_to_python(
5234-
path, target_format, format, include_pointer_tags,
5248+
path, target_format, dng_target_mode, format,
5249+
include_pointer_tags,
52355250
decode_makernote, decode_embedded_containers, decompress,
52365251
include_exif_app1, include_xmp_app1, include_icc_app2,
52375252
include_iptc_app13, xmp_include_existing,
@@ -5260,6 +5275,7 @@ NB_MODULE(_openmeta, m)
52605275
true);
52615276
},
52625277
"path"_a, "target_format"_a = TransferTargetFormat::Jpeg,
5278+
"dng_target_mode"_a = DngTargetMode::MinimalFreshScaffold,
52635279
"format"_a = XmpSidecarFormat::Portable,
52645280
"include_pointer_tags"_a = true, "decode_makernote"_a = false,
52655281
"decode_embedded_containers"_a = true, "decompress"_a = true,
@@ -5309,6 +5325,7 @@ NB_MODULE(_openmeta, m)
53095325
m.def(
53105326
"transfer_file",
53115327
[](const std::string& path, TransferTargetFormat target_format,
5328+
DngTargetMode dng_target_mode,
53125329
XmpSidecarFormat format, bool include_pointer_tags,
53135330
bool decode_makernote, bool decode_embedded_containers,
53145331
bool decompress, bool include_exif_app1, bool include_xmp_app1,
@@ -5350,7 +5367,8 @@ NB_MODULE(_openmeta, m)
53505367
bool overwrite_xmp_sidecar,
53515368
bool remove_destination_xmp_sidecar) {
53525369
return transfer_probe_to_python(
5353-
path, target_format, format, include_pointer_tags,
5370+
path, target_format, dng_target_mode, format,
5371+
include_pointer_tags,
53545372
decode_makernote, decode_embedded_containers, decompress,
53555373
include_exif_app1, include_xmp_app1, include_icc_app2,
53565374
include_iptc_app13, xmp_include_existing,
@@ -5381,6 +5399,7 @@ NB_MODULE(_openmeta, m)
53815399
remove_destination_xmp_sidecar);
53825400
},
53835401
"path"_a, "target_format"_a = TransferTargetFormat::Jpeg,
5402+
"dng_target_mode"_a = DngTargetMode::MinimalFreshScaffold,
53845403
"format"_a = XmpSidecarFormat::Portable,
53855404
"include_pointer_tags"_a = true, "decode_makernote"_a = false,
53865405
"decode_embedded_containers"_a = true, "decompress"_a = true,
@@ -5433,6 +5452,7 @@ NB_MODULE(_openmeta, m)
54335452
m.def(
54345453
"unsafe_transfer_file",
54355454
[](const std::string& path, TransferTargetFormat target_format,
5455+
DngTargetMode dng_target_mode,
54365456
XmpSidecarFormat format, bool include_pointer_tags,
54375457
bool decode_makernote, bool decode_embedded_containers,
54385458
bool decompress, bool include_exif_app1, bool include_xmp_app1,
@@ -5474,7 +5494,8 @@ NB_MODULE(_openmeta, m)
54745494
bool overwrite_xmp_sidecar,
54755495
bool remove_destination_xmp_sidecar) {
54765496
return transfer_probe_to_python(
5477-
path, target_format, format, include_pointer_tags,
5497+
path, target_format, dng_target_mode, format,
5498+
include_pointer_tags,
54785499
decode_makernote, decode_embedded_containers, decompress,
54795500
include_exif_app1, include_xmp_app1, include_icc_app2,
54805501
include_iptc_app13, xmp_include_existing,
@@ -5505,6 +5526,7 @@ NB_MODULE(_openmeta, m)
55055526
remove_destination_xmp_sidecar);
55065527
},
55075528
"path"_a, "target_format"_a = TransferTargetFormat::Jpeg,
5529+
"dng_target_mode"_a = DngTargetMode::MinimalFreshScaffold,
55085530
"format"_a = XmpSidecarFormat::Portable,
55095531
"include_pointer_tags"_a = true, "decode_makernote"_a = false,
55105532
"decode_embedded_containers"_a = true, "decompress"_a = true,
@@ -5599,7 +5621,8 @@ NB_MODULE(_openmeta, m)
55995621
m.def(
56005622
"update_dng_sdk_file_from_file",
56015623
[](const std::string& source_path, const std::string& target_path,
5602-
XmpSidecarFormat format, bool include_pointer_tags,
5624+
DngTargetMode dng_target_mode, XmpSidecarFormat format,
5625+
bool include_pointer_tags,
56035626
bool decode_makernote, bool decode_embedded_containers,
56045627
bool decompress, bool include_exif_app1, bool include_xmp_app1,
56055628
bool include_icc_app2, bool include_iptc_app13,
@@ -5612,7 +5635,8 @@ NB_MODULE(_openmeta, m)
56125635
bool apply_iptc, bool synchronize_metadata,
56135636
bool cleanup_for_update) {
56145637
return update_dng_sdk_file_from_file_to_python(
5615-
source_path, target_path, format, include_pointer_tags,
5638+
source_path, target_path, dng_target_mode, format,
5639+
include_pointer_tags,
56165640
decode_makernote, decode_embedded_containers, decompress,
56175641
include_exif_app1, include_xmp_app1, include_icc_app2,
56185642
include_iptc_app13, xmp_include_existing,
@@ -5623,6 +5647,7 @@ NB_MODULE(_openmeta, m)
56235647
cleanup_for_update);
56245648
},
56255649
"source_path"_a, "target_path"_a,
5650+
"dng_target_mode"_a = DngTargetMode::MinimalFreshScaffold,
56265651
"format"_a = XmpSidecarFormat::Portable,
56275652
"include_pointer_tags"_a = true, "decode_makernote"_a = false,
56285653
"decode_embedded_containers"_a = true, "decompress"_a = true,

0 commit comments

Comments
 (0)