chemigram.core.xmp¶
chemigram.core.xmp
¶
Parse and write darktable XMP sidecar files.
XMP is darktable's per-image edit state format: RDF/XML with a
<rdf:Seq> of history entries (per-module configurations).
Calibrated to darktable 5.4.1 (see tests/fixtures/README.md and
docs/adr/TA.md contracts/xmp-darktable-history).
Binary blobs (params, blendop_params) are opaque (ADR-008) and
are never decoded. defusedxml is used for parsing untrusted input;
output uses the standard library's ElementTree (which we control).
Round-trip property: parse_xmp(write_xmp(x, p)) == x for any
well-formed Xmp x (semantic equality of the dataclass, not byte
identity of the file).
Public API
- :func:
parse_xmp, :func:write_xmp - :class:
Xmp, :class:HistoryEntry— frozen dataclasses - :class:
XmpParseError— exception raised on malformed input
XmpParseError
¶
Bases: Exception
Raised when an XMP file cannot be parsed.
HistoryEntry
dataclass
¶
HistoryEntry(num, operation, enabled, modversion, params, multi_name, multi_name_hand_edited, multi_priority, blendop_version, blendop_params, iop_order=None)
One <rdf:li> entry in a darktable XMP <rdf:Seq>.
Calibrated to darktable 5.4.1. params and blendop_params
are opaque blobs (ADR-008) and are never decoded.
iop_order is None in 5.4.1 .dtstyle files and is unnecessary
for Path B (per RFC-018 v0.2 empirical evidence). darktable resolves
pipeline order from the parent's darktable:iop_order_version + an
internal iop_list. The field stays Optional + float because rendered
XMP sidecars can carry per-entry iop_order as a float (e.g.
47.4747); the parser must round-trip those.
Xmp
dataclass
¶
Xmp(rating, label, auto_presets_applied, history_end, iop_order_version, history, raw_extra_fields=())
A parsed darktable XMP file.
First-class fields are those the synthesizer (Issue #3) reads or
writes. Everything else on <rdf:Description> (timestamps, hashes,
creator metadata, masks_history, etc.) is preserved opaquely in
raw_extra_fields for round-trip fidelity.
raw_extra_fields entries are 3-tuples (kind, qname, value):
kind == "attr": an attribute on<rdf:Description>.qnameis the prefixed name (e.g."darktable:xmp_version");valueis the raw attribute string.kind == "elem": a child element of<rdf:Description>.qnameis the prefixed element name;valueis the entire subtree serialized as XML text.
parse_xmp
¶
Parse a darktable XMP sidecar file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Path to an XMP file. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
An |
Xmp
|
class: |
Xmp
|
unmodeled attributes / nested elements via |
Raises:
| Type | Description |
|---|---|
XmpParseError
|
malformed XML; missing |
FileNotFoundError
|
|
Source code in src/chemigram/core/xmp.py
parse_xmp_from_bytes
¶
Parse an XMP from in-memory bytes.
Counterpart to :func:parse_xmp that avoids a filesystem
round-trip. Useful when the bytes already live in memory — e.g.,
content-addressed reads from
:class:~chemigram.core.versioning.repo.ImageRepo.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
UTF-8 encoded XMP bytes. |
required |
source
|
str
|
Human-readable label used in error messages (e.g.,
|
'<bytes>'
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
Xmp
|
class: |
Raises:
| Type | Description |
|---|---|
XmpParseError
|
malformed XML, invalid UTF-8, or missing
|
Source code in src/chemigram/core/xmp.py
write_xmp
¶
Serialize an :class:Xmp back to an XMP file.
Round-trip property (semantic equality): parse_xmp(write_xmp(x, p)) == x
Field ordering on output: raw_extra_fields attributes come
first (in their stored order), then first-class fields (rating,
label if non-empty, auto_presets_applied, history_end,
iop_order_version), then raw_extra_fields child elements,
then the synthesized <darktable:history> if non-empty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xmp
|
Xmp
|
The :class: |
required |
path
|
Path
|
Destination path. Parent directory must exist; file is overwritten if present. |
required |
Source code in src/chemigram/core/xmp.py
synthesize_xmp
¶
Compose vocabulary entries onto a baseline XMP (Path A and Path B).
SET semantics (ADR-002, RFC-006 closure / ADR-051): a plugin entry
whose (operation, multi_priority) tuple matches a baseline
history entry REPLACES that entry in place. num and
iop_order are preserved from the baseline slot — Phase 0 finding:
SET-replace inherits position implicitly because darktable computes
pipeline ordering from the parent iop_order_version and an
internal iop_list, not per-<rdf:li> metadata.
Path B (new-instance addition at a previously-unused
(operation, multi_priority)) appends a fresh HistoryEntry
at num = max(existing) + 1 with iop_order=None. Per
RFC-018 v0.2's empirical evidence
(tests/fixtures/preflight-evidence/), darktable 5.4.1 resolves
pipeline order from the description-level iop_order_version +
internal iop_list, so per-entry iop_order is unnecessary.
history_end increments to match. Closes RFC-001's iop_order
open question (deferred under ADR-051) and supersedes that ADR's
NotImplementedError stance.
Among multiple input plugins targeting the same
(operation, multi_priority), the last one wins (input order).
This deviates from RFC-006's original "synthesizer error" proposal;
the closing ADR-051 captures the rationale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
baseline
|
Xmp
|
starting :class: |
required |
entries
|
list[DtstyleEntry]
|
vocabulary entries; order matters for last-writer-wins
among entries that share |
required |
Returns:
| Type | Description |
|---|---|
Xmp
|
A new frozen :class: |
Xmp
|
metadata ( |
Xmp
|
|
Xmp
|
verbatim. |
Xmp
|
— typically equal to the baseline value for Path A, larger |
Xmp
|
for Path B. |
Source code in src/chemigram/core/xmp.py
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 | |