Skip to content

Commit a6cdc79

Browse files
committed
Add bounded EXR emit and XMP projection controls
Bounded first-class EXR metadata target and writer-side XMP projection controls. Adds EXR-specific types (PreparedExrEmitOp/Plan, EmittedExrAttributeSummary, ExrPreparedAttributeView), parsing/serialization helpers, unique EXR attribute naming, and OIIO attribute collection to prepare flattened string header attributes as prepared transfer blocks. Implements EXR emit/compile/compiled-emit paths and hot-path execution helpers, adapter op kind and adapter view support, and recording emitters/summaries. Add request flags (xmp_project_exif, xmp_project_iptc), XMP-sidecar IPTC inclusion, policy decision recording for EXIF/IPTC->XMP projection, and corresponding CLI/python flags (--xmp-no-exif-projection, --xmp-no-iptc-projection, --target-exr). Update docs/README to reflect EXR status and write-side sync controls and adjust XMP dump to honor IPTC projection option.
1 parent a95cde3 commit a6cdc79

11 files changed

Lines changed: 1583 additions & 57 deletions

File tree

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,22 @@ Current target status:
8080
| JP2 | Bounded but real |
8181
| JXL | Bounded but real |
8282
| HEIF / AVIF / CR3 | Bounded but real |
83-
| EXR | Bridge-only today |
83+
| EXR | Bounded but real |
8484

8585
In practice:
8686
- JPEG and TIFF are the strongest transfer targets today.
87-
- PNG, WebP, JP2, JXL, and bounded BMFF targets have real emit/edit paths.
88-
- EXR currently has an attribute bridge for host integrations, not a full
89-
first-class transfer path.
87+
- PNG, WebP, JP2, JXL, bounded BMFF, and EXR all have real first-class
88+
transfer entry points.
89+
- EXR is still narrower than the container-edit targets: it emits safe string
90+
header attributes through the transfer core, but it does not rewrite full
91+
EXR files yet.
92+
- Writer-side sync behavior is now partially explicit instead of implicit:
93+
generated XMP can independently keep or suppress EXIF-derived and
94+
IPTC-derived projection during transfer preparation.
95+
- Prepared bundles record resolved policy decisions for MakerNote, JUMBF,
96+
C2PA, EXIF-to-XMP projection, and IPTC-to-XMP projection.
97+
- This is still not a full MWG-style sync engine. OpenMeta does not yet try to
98+
solve all EXIF/IPTC/XMP conflict resolution or canonical writeback policy.
9099

91100
For transfer details, see
92101
[docs/metadata_transfer_plan.md](docs/metadata_transfer_plan.md).

docs/metadata_transfer_plan.md

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ Source-side readiness is already strong:
3434

3535
The main remaining work is now on the target side.
3636

37+
The first public write-side sync controls are also in place:
38+
- generated XMP can explicitly suppress EXIF-derived projection
39+
- generated XMP can explicitly suppress IPTC-derived projection
40+
- prepared bundles record those resolved projection decisions alongside the
41+
existing preservation policies
42+
3743
## Target Status Matrix
3844

3945
| Target | Status | Current shape | Main limits |
@@ -45,7 +51,7 @@ The main remaining work is now on the target side.
4551
| JP2 | Bounded but real | Prepared bundle, compiled emit, bounded box rewrite/edit, file-helper roundtrip | `jp2h` synthesis is still out of scope |
4652
| JXL | Bounded but real | Prepared bundle, compiled emit, bounded box rewrite/edit, file-helper roundtrip | Still narrower than JPEG/TIFF |
4753
| HEIF / AVIF / CR3 | Bounded but real | Prepared bundle, compiled emit, bounded BMFF item/property edit, file-helper roundtrip | Not broad BMFF writer parity |
48-
| EXR | Bridge-only today | EXR-native attribute export and replay for host integrations | No first-class transfer target path yet |
54+
| EXR | Bounded but real | Prepared bundle, compiled emit, direct backend attribute emit, CLI/Python transfer surface | No file rewrite/edit path yet; current transfer payload is safe string attributes only |
4955

5056
## What Is Already Implemented
5157

@@ -71,7 +77,8 @@ These support the public transfer flow:
7177
- prepared payload and package batch persistence
7278
- adapter views for host integrations
7379
- explicit time-patch support for fixed-width EXIF date/time fields
74-
- transfer-policy decisions for MakerNote, JUMBF, and C2PA
80+
- transfer-policy decisions for MakerNote, JUMBF, C2PA, EXIF-to-XMP
81+
projection, and IPTC-to-XMP projection
7582

7683
### Current File-Helper Regression Coverage
7784

@@ -163,21 +170,34 @@ Implemented as a bounded BMFF target family:
163170

164171
### EXR
165172

166-
Implemented today as an integration bridge, not a first-class transfer target:
173+
Implemented today as a bounded first-class target:
174+
- `prepare_metadata_for_target(...)`
175+
- `prepare_metadata_for_target_file(...)`
176+
- `compile_prepared_transfer_execution(...)`
177+
- `emit_prepared_bundle_exr(...)`
178+
- `emit_prepared_transfer_compiled(...)`
179+
- public CLI/Python `--target-exr` transfer surface
180+
181+
It still keeps the older integration bridge:
167182
- `build_exr_attribute_batch(...)`
168183
- `build_exr_attribute_part_spans(...)`
169184
- `build_exr_attribute_part_views(...)`
170185
- `replay_exr_attribute_batch(...)`
171186

172-
This is useful for OpenEXR / OIIO-style hosts, but it is not yet a true
173-
`prepare -> compile -> emit/edit` path in the same sense as JPEG or TIFF.
187+
Current EXR transfer scope is intentionally conservative:
188+
- safe flattened `string` header attributes
189+
- backend emission through `ExrTransferEmitter`
190+
- no general file-based EXR metadata rewrite/edit path yet
191+
- no typed EXR attribute synthesis beyond the current safe string projection
174192

175193
## Transfer Policies
176194

177-
The public transfer contract already models three policy subjects:
195+
The public transfer contract now models five policy subjects:
178196
- MakerNote
179197
- JUMBF
180198
- C2PA
199+
- XMP EXIF projection
200+
- XMP IPTC projection
181201

182202
Each uses explicit `TransferPolicyAction` values:
183203
- `Keep`
@@ -188,6 +208,39 @@ Each uses explicit `TransferPolicyAction` values:
188208
Prepared bundles also record the resolved policy decisions and reasons so
189209
callers do not have to infer behavior from warning text alone.
190210

211+
For the XMP projection subjects, the current public knobs are intentionally
212+
simple:
213+
- EXIF-derived properties can be mirrored into generated XMP or suppressed
214+
- IPTC-derived properties can be mirrored into generated XMP or suppressed
215+
216+
This gives callers stable write-side control over the most important projection
217+
behavior without forcing them to reverse-engineer the transfer output.
218+
219+
## Write-Side Sync Controls
220+
221+
OpenMeta now has a bounded public sync-policy layer for generated XMP.
222+
223+
Current controls:
224+
- `xmp_project_exif`
225+
- `xmp_project_iptc`
226+
- CLI:
227+
- `--xmp-no-exif-projection`
228+
- `--xmp-no-iptc-projection`
229+
230+
Current behavior:
231+
- existing XMP can still be included independently
232+
- EXIF payload emission stays independent from EXIF-to-XMP projection
233+
- IPTC native carrier emission stays independent from IPTC-to-XMP projection
234+
- some targets without a native IPTC carrier can still use XMP as the bounded
235+
fallback carrier when IPTC projection is enabled
236+
237+
This is deliberately narrower than a full sync engine. It does not yet define:
238+
- full EXIF vs XMP precedence rules
239+
- MWG-style reconciliation
240+
- canonical sidecar vs embedded writeback policy
241+
- namespace-wide deduplication and normalization rules beyond the current
242+
generated-XMP path
243+
191244
## Time Patch Plan
192245

193246
Time patching is intentionally narrow and fixed-width.
@@ -214,9 +267,10 @@ OpenMeta still does not present one fully mature, general-purpose metadata
214267
editor across all formats. The current transfer core is real, but still more
215268
bounded than ExifTool or Exiv2.
216269

217-
### 2. EXIF / IPTC / XMP sync policy
270+
### 2. Broader EXIF / IPTC / XMP sync policy
218271

219-
This remains one of the biggest product gaps for writer adoption.
272+
This remains one of the biggest product gaps for writer adoption, even though
273+
the first public projection controls now exist.
220274

221275
Missing pieces include:
222276
- conflict resolution rules
@@ -229,11 +283,12 @@ Missing pieces include:
229283
Read parity is strong, but broad rewrite guarantees for vendor metadata are not
230284
yet at the level of mature editing tools.
231285

232-
### 4. EXR direction
286+
### 4. EXR depth
233287

234-
The architectural question is now explicit:
235-
- keep EXR as an attribute bridge only, or
236-
- promote it to a first-class transfer target
288+
The architectural question is now how far to deepen the current bounded EXR
289+
target:
290+
- keep EXR as a backend-emitter target plus bridge helpers, or
291+
- add a broader EXR file rewrite/edit path
237292

238293
## Recommended Next Priorities
239294

@@ -246,7 +301,7 @@ The architectural question is now explicit:
246301
- JP2
247302
- JXL
248303
- bounded BMFF
249-
3. Decide the EXR direction explicitly.
304+
3. Decide how deep EXR should go beyond the current bounded target.
250305
4. Add more transfer-focused roundtrip and compare gates where they improve
251306
confidence for adopters.
252307
5. Add an explicit EXIF / IPTC / XMP sync policy.

src/include/openmeta/metadata_transfer.h

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ enum class TransferPolicySubject : uint8_t {
113113
MakerNote,
114114
Jumbf,
115115
C2pa,
116+
XmpExifProjection,
117+
XmpIptcProjection,
116118
};
117119

118120
/// Requested/effective action for a metadata family during transfer.
@@ -408,6 +410,8 @@ struct PrepareTransferRequest final {
408410
bool include_icc_app2 = true;
409411
bool include_iptc_app13 = true;
410412
bool xmp_portable = true;
413+
bool xmp_project_exif = true;
414+
bool xmp_project_iptc = true;
411415
bool xmp_include_existing = true;
412416
bool xmp_exiftool_gpsdatetime_alias = false;
413417
};
@@ -731,6 +735,17 @@ struct PreparedJp2EmitPlan final {
731735
std::vector<PreparedJp2EmitOp> ops;
732736
};
733737

738+
/// One precompiled EXR emit operation (prepared block -> EXR attribute).
739+
struct PreparedExrEmitOp final {
740+
uint32_t block_index = 0;
741+
};
742+
743+
/// Reusable precompiled EXR emit plan for a prepared transfer bundle.
744+
struct PreparedExrEmitPlan final {
745+
uint32_t contract_version = kMetadataTransferContractVersion;
746+
std::vector<PreparedExrEmitOp> ops;
747+
};
748+
734749
/// Kind of precompiled ISO-BMFF metadata emit operation.
735750
enum class PreparedBmffEmitKind : uint8_t {
736751
Item,
@@ -770,6 +785,7 @@ struct PreparedTransferExecutionPlan final {
770785
PreparedWebpEmitPlan webp_emit;
771786
PreparedPngEmitPlan png_emit;
772787
PreparedJp2EmitPlan jp2_emit;
788+
PreparedExrEmitPlan exr_emit;
773789
PreparedBmffEmitPlan bmff_emit;
774790
};
775791

@@ -781,9 +797,10 @@ enum class TransferAdapterOpKind : uint8_t {
781797
JxlIccProfile,
782798
WebpChunk,
783799
PngChunk,
800+
Jp2Box,
801+
ExrAttribute,
784802
BmffItem,
785803
BmffProperty,
786-
Jp2Box,
787804
};
788805

789806
/// One compiled adapter-facing operation derived from a prepared bundle.
@@ -980,6 +997,14 @@ struct EmittedJp2BoxSummary final {
980997
uint64_t bytes = 0;
981998
};
982999

1000+
/// One emitted EXR attribute summary entry.
1001+
struct EmittedExrAttributeSummary final {
1002+
std::string name;
1003+
std::string type_name;
1004+
uint32_t count = 0;
1005+
uint64_t bytes = 0;
1006+
};
1007+
9831008
/// One emitted ISO-BMFF metadata item summary entry.
9841009
struct EmittedBmffItemSummary final {
9851010
uint32_t item_type = 0U;
@@ -1093,6 +1118,7 @@ struct ExecutePreparedTransferResult final {
10931118
std::vector<EmittedWebpChunkSummary> webp_chunk_summary;
10941119
std::vector<EmittedPngChunkSummary> png_chunk_summary;
10951120
std::vector<EmittedJp2BoxSummary> jp2_box_summary;
1121+
std::vector<EmittedExrAttributeSummary> exr_attribute_summary;
10961122
std::vector<EmittedBmffItemSummary> bmff_item_summary;
10971123
std::vector<EmittedBmffPropertySummary> bmff_property_summary;
10981124
bool tiff_commit = false;
@@ -1250,6 +1276,14 @@ struct ExrPreparedAttribute final {
12501276
bool is_opaque = false;
12511277
};
12521278

1279+
/// Zero-copy EXR attribute view for prepared transfer emission.
1280+
struct ExrPreparedAttributeView final {
1281+
std::string_view name;
1282+
std::string_view type_name;
1283+
std::span<const std::byte> value;
1284+
bool is_opaque = false;
1285+
};
1286+
12531287
/**
12541288
* \brief Backend contract for OpenEXR header attribute emission.
12551289
*/
@@ -1259,6 +1293,16 @@ class ExrTransferEmitter {
12591293
virtual TransferStatus
12601294
set_attribute(const ExrPreparedAttribute& attr) noexcept
12611295
= 0;
1296+
virtual TransferStatus
1297+
set_attribute_view(const ExrPreparedAttributeView& attr) noexcept
1298+
{
1299+
ExrPreparedAttribute owned;
1300+
owned.name.assign(attr.name.data(), attr.name.size());
1301+
owned.type_name.assign(attr.type_name.data(), attr.type_name.size());
1302+
owned.value.assign(attr.value.begin(), attr.value.end());
1303+
owned.is_opaque = attr.is_opaque;
1304+
return set_attribute(owned);
1305+
}
12621306
};
12631307

12641308
/// \brief Draft bundle preparation entry point.
@@ -1373,6 +1417,21 @@ emit_prepared_bundle_jp2(const PreparedTransferBundle& bundle,
13731417
const EmitTransferOptions& options
13741418
= EmitTransferOptions {}) noexcept;
13751419

1420+
/**
1421+
* \brief Emit prepared metadata blocks into an EXR backend.
1422+
*
1423+
* Route mapping:
1424+
* - `exr:attribute-string` -> EXR `string` header attribute
1425+
*
1426+
* This first-class EXR target is intentionally bounded to safe flattened
1427+
* string attributes. It does not rewrite complete EXR files.
1428+
*/
1429+
EmitTransferResult
1430+
emit_prepared_bundle_exr(const PreparedTransferBundle& bundle,
1431+
ExrTransferEmitter& emitter,
1432+
const EmitTransferOptions& options
1433+
= EmitTransferOptions {}) noexcept;
1434+
13761435
/**
13771436
* \brief Emit prepared metadata blocks into an ISO-BMFF metadata backend.
13781437
*
@@ -1488,6 +1547,15 @@ compile_prepared_bundle_jp2(const PreparedTransferBundle& bundle,
14881547
const EmitTransferOptions& options
14891548
= EmitTransferOptions {}) noexcept;
14901549

1550+
/**
1551+
* \brief Compile a reusable EXR emit plan from a prepared bundle.
1552+
*/
1553+
EmitTransferResult
1554+
compile_prepared_bundle_exr(const PreparedTransferBundle& bundle,
1555+
PreparedExrEmitPlan* out_plan,
1556+
const EmitTransferOptions& options
1557+
= EmitTransferOptions {}) noexcept;
1558+
14911559
/**
14921560
* \brief Compile a reusable ISO-BMFF metadata emit plan from a prepared
14931561
* bundle.
@@ -1528,6 +1596,16 @@ emit_prepared_bundle_jp2_compiled(const PreparedTransferBundle& bundle,
15281596
const EmitTransferOptions& options
15291597
= EmitTransferOptions {}) noexcept;
15301598

1599+
/**
1600+
* \brief Emit a prepared bundle using a precompiled EXR emit plan.
1601+
*/
1602+
EmitTransferResult
1603+
emit_prepared_bundle_exr_compiled(const PreparedTransferBundle& bundle,
1604+
const PreparedExrEmitPlan& plan,
1605+
ExrTransferEmitter& emitter,
1606+
const EmitTransferOptions& options
1607+
= EmitTransferOptions {}) noexcept;
1608+
15311609
/**
15321610
* \brief Emit a prepared bundle using a precompiled ISO-BMFF metadata emit
15331611
* plan.
@@ -1900,6 +1978,18 @@ emit_prepared_transfer_compiled(
19001978
const ApplyTimePatchOptions& time_patch = ApplyTimePatchOptions {},
19011979
uint32_t emit_repeat = 1U) noexcept;
19021980

1981+
/**
1982+
* \brief Hot-path helper: apply non-owning time patches and emit through an
1983+
* EXR backend.
1984+
*/
1985+
ExecutePreparedTransferResult
1986+
emit_prepared_transfer_compiled(
1987+
PreparedTransferBundle* bundle, const PreparedTransferExecutionPlan& plan,
1988+
ExrTransferEmitter& emitter,
1989+
std::span<const TimePatchView> time_patches = {},
1990+
const ApplyTimePatchOptions& time_patch = ApplyTimePatchOptions {},
1991+
uint32_t emit_repeat = 1U) noexcept;
1992+
19031993
/**
19041994
* \brief Hot-path helper: apply non-owning time patches and emit through an
19051995
* ISO-BMFF metadata backend.

src/include/openmeta/xmp_dump.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ struct XmpPortableOptions final {
4646
XmpDumpLimits limits;
4747
/// Include TIFF/EXIF/GPS derived properties.
4848
bool include_exif = true;
49+
/// Include IPTC-IIM derived portable XMP properties.
50+
bool include_iptc = true;
4951
/// Include \ref MetaKeyKind::XmpProperty entries already present in the store.
5052
///
5153
/// \note Currently only simple `property_path` values are emitted (no `/` nesting).
@@ -82,6 +84,7 @@ struct XmpSidecarRequest final {
8284

8385
/// Portable mode options (applied when format == Portable).
8486
bool include_exif = true;
87+
bool include_iptc = true;
8588
bool include_existing_xmp = false;
8689
bool portable_exiftool_gpsdatetime_alias = false;
8790

0 commit comments

Comments
 (0)