[DRAFT — for visibility, not merging] Color metadata: the combined branch behind the upcoming PR series - #5392
Draft
zachlewis wants to merge 212 commits into
Draft
Conversation
Compile a small OCIO config into libOpenImageIO (hex-embedded at configure time, same idiom as buildopts.h.in) that defines color spaces for the color-interop identities OIIO can reliably recognize and relate in other OCIO configs. A lazily-initialized, thread-safe accessor parses it once per process via OCIO::Config::CreateFromStream, for linked OCIO versions that predate native interop ID support. Zero public API and zero behavior change: the new machinery is anonymous-namespace/pvt-only, and nothing calls it yet outside a pvt::interop_identities_config_size() test shim exercised by unit_color. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add pvt::parse_interop_id/is_valid_interop_id/sanitize_id_token/
strip_leftmost_namespace/is_utility_interop_id: pure, stateless functions
implementing the ID grammar and sanitization rules from the CIF
recommendation ("An ID for Color Interop", Annexes B and C):
https://github.com/AcademySoftwareFoundation/ColorInterop/wiki
Kept in their own translation unit (interop_id.cpp) with no OCIO
dependency, so color_ocio.cpp doesn't have to grow to hold them. The
51-char id-set is a function-local constexpr lookup table (no runtime
init, no first-call mutex); Annex C sanitization walks the input by
UTF-8 code point so a multi-byte character collapses to exactly one '^'.
Zero public API and zero behavior change: everything lives under
OIIO::pvt and nothing calls it yet outside the round-trip unit tests
added to unit_color, which port every decisive test vector from the
CIF recommendation's grammar and sanitization tables.
Assisted-by: Claude (claude-fable-5 orchestration)
Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…egistry Build the built-in interop identities config on top of OCIO's own latest builtin studio config when the linked OCIO is >= 2.5, which already carries the CIF interop identities natively (every color space has getInteropID() set). Start from a mutable copy of that studio config and layer in only the embedded identities it doesn't already provide: skip the "ocio:" namespace (the studio config defines it), always add the "oiio:" namespace (OIIO-only additions), and add bare CIF identities only where the studio config doesn't already resolve the name, so its own definition wins where present. With OCIO < 2.5 the config is still the embedded bytes parsed as-is, unchanged. Add pvt::interop_identities_config_resolves as an internal/test accessor and extend the unit_color test: under OCIO >= 2.5 it asserts a studio-native identity still resolves (so the registry is the studio config's superset, hence its space count is at least the studio baseline) and that an OIIO-only identity layered on top resolves too. Zero public API and zero behavior change: the machinery stays anonymous-namespace/pvt-only and nothing calls it yet outside the test. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Extend the unit_color test with coverage for the built-in interop identities config's invariants: every declared name resolves to itself (name and interop_id are the same value by construction in the source config); every namespaced entry's bare stripped form resolves through OCIO's own alias resolution to the same color space, with zero registry-side code; `data` is a config entry while the `unknown`/`bypass` utility tokens are pure grammar-layer strings that never resolve as color spaces. Also cross-check a representative subset of the CIF wiki's published Color Interop IDs (https://github.com/AcademySoftwareFoundation/ColorInterop/wiki/Registered-Color-Interop-IDs) against the config, since no existing coverage did this. Add pvt::interop_identities_config_names(), a small internal/test-only accessor enumerating the config's declared names, needed to iterate the whole registry for the invariant checks above. Zero public API and zero behavior change. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Classify each color space for interop matching, entirely inside ColorConfig::Impl and computed lazily on first query -- constructing a ColorConfig does no classification work. - Extend CSInfo::Flags with classification bits (is_unique, should_skip_matching, has_complex_transform, is_simple, is_context_invariant) alongside the existing is_data, plus per-space `analyzed`/`active` state. - Add a shared simple-transform allowlist (isSimpleAtomicTransform) that accepts matrix/range/exponent/log/allocation/LUT1D/grading-curve and non-ACES builtins, and rejects LUT3D/CDL/LOOK/DISPLAY_VIEW, ACES-OUTPUT/ACES-LMT builtins, fixed-functions (except the 2.5+ lin-to-log styles) and unknown types. containsBlockableTransform walks authored transforms (recursing GROUP, resolving COLORSPACE/FILE references) and defers atomic types to the allowlist; getSimpleColorSpaces memoizes the sorted result. - Add transformUsesContextVars, a '$'-scan of authored src/dst/CCCId plus a search-path check, recursing through GROUP. - Add Impl::analyze(), a new lazy double-checked pass parallel to examine() (gathering the simple-space set before taking the lock), setting the classification bits from OCIO isData()/category/context-invariance and allowlist membership. It is a wholly new entry point, not wired through add()/inventory(). - Add a per-query learned-complex hint (a hint, not a permanent verdict; cleared with the config lifetime). - Expose the classification via pvt:: shims (imageio_pvt.h) so unit tests observe the flags and verify laziness on a minimal generated config. Zero public API and zero behavior change. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Fingerprint a color space by transforming a fixed probe from the reference role to the space, so equivalent spaces can be recognized by value rather than by name. Entirely inside ColorConfig::Impl and computed lazily on first fingerprint query -- constructing a ColorConfig does no probing work. - Add the probe protocol: six calibrated RGBA identity pixels per reference kind (scene ACES-AP0, display CIE-XYZ-D65) plus a trailing linearity quartet, normalized into the config's reference space and transformed to each space via OPTIMIZATION_NONE so results are byte-reproducible across builds. - Probe on a lazily built, processor-cache-disabled editable copy of the config: probe processors are one-shot, so OCIO's processor cache would only add contention and pin every probe processor for the config's life. - Add fingerprints_match(): an exact, tolerance-gated identity compare. Reference kinds must match (a scene and a display space never compare equal), lengths must match, and every identity-probe float must agree within 5e-3; the linearity quartet is excluded because its 4.0 inputs clamp differently through LUT-backed vs analytic curves. No best/closest scoring. - Add a bulk pass that fingerprints every "simple" space by iterating the classification's sorted simple-space cache, so the order is deterministic. - Expose the machinery through pvt:: shims (imageio_pvt.h): compute one fingerprint, compare two, and list the deterministic fingerprint order, with a unit test on OCIO's built-in default config (which carries the aces_interchange role) covering alias equivalence, distinct-space and scene-vs-display non-matches, and byte-reproducibility. Zero public API and zero behavior change. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Detect whether an OCIO config resolves a scene-referred interchange space (ACES2065-1 / the aces_interchange role) that cross-config color features anchor on, and for configs that don't, synthesize a repaired in-memory copy that does -- without ever mutating the original config. The assertion runs a fixed discovery order (the aces_interchange role, a list of well-known ACES2065-1 / AP0 aliases, OCIO builtin identification against OIIO's built-in interop identities config, then the ocio://default naming convention). Non-interoperable configs get an "interopified" copy: an editable copy repaired to resolve a scene (and, where possible, display) interchange by anchoring lin_ap0_scene on the config's scene-referred identity space and bootstrapping display interchange infrastructure. The copy has its OCIO processor cache disabled, since the one-shot probe path gains nothing from it, and is memoized process-wide by structural config cache id so instances of the same config share one copy. The fingerprint probe path now probes through this copy, so fingerprinting works even for configs lacking the interchange role. All of this is fully lazy: it runs on the first interop query (or fingerprint probe) under the same double-checked pattern as examine(), so constructing a ColorConfig does no interop work. When the assertion fails, the bootstrap warns exactly once per structural config across the process. No public API or default behavior change: the machinery is internal, exposed only through pvt:: shims that the unit test drives directly. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add a process-global flyweight cache of color space fingerprints, keyed on (structural config cache id, context cache id, color space name) with a context-invariant bucket collapse: a space proven independent of context variables is fingerprinted once and shared across every context of the same structural config, while every other space -- including one not yet proven invariant, or unknown -- stays context-scoped, so nothing is ever shared that wasn't proven stable. Using the structural (context-free) config id for that component is what makes the collapse sound: it does not change when only context variables do. The cache reuses OIIO's existing sharded unordered_map_concurrent (find_or_insert for first-writer-wins publish, retrieve for cheap read-locked hits) rather than a bespoke container. On a miss the fingerprint is computed outside the cache lock and then published first-writer-wins; a racing builder's entry wins and the duplicate is discarded (duplicate work allowed, blocking never). Keys are content-addressed, so a changed config or context simply produces new keys and old entries orphan harmlessly -- there is no invalidation or eviction path. clear() exists only for test/debug reset. A bulk "warm" pass fingerprints every simple color space and publishes each entry. All new machinery is internal (anonymous namespace / OIIO::pvt) with zero public API and zero default behavior change; unit tests reach it through imageio_pvt.h shims. Fully lazy: constructing a ColorConfig runs none of it. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Adds an opt-in --bench flag to color_test that times cold vs warm color space classification and fingerprint-cache phases plus a bulk fingerprint pass, logging candidate/fingerprint counts beside every timing. Also re-measures ColorConfig construction before and after the interop machinery has run in-process, and spawns a fresh subprocess for a true-cold construction number (a same-process measurement is warmed by process-level caches and understates it). No thresholds are asserted -- these numbers feed a design write-up. The default `ctest -R unit_color` run does not pass --bench and stays fast; construction cost stays flat with or without the bench phases, demonstrating the config's lazy-construction behavior. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Layer the fingerprint-free tiers of the Color Interop Forum reading-side resolution cascade onto ColorConfig::resolve(), inserted after the existing direct OCIO / informal-alias lookup and before the historical input-name passthrough: - stripped-namespace retry: an id carrying a namespace is retried with one leftmost "<ns>:" removed (blank-inner colons survive the strip, so "my-studio::srgb" deliberately does not match "srgb"). - config-local form "<config>:local:<space>": resolves against this config's own color space names/aliases when "<config>" sanitizes to the config's own name. - explicit interop_id attribute (OCIO 2.5+): matches a color space's interop_id exactly, or with exactly one side's leftmost namespace stripped (never both); utility tokens are excluded from this lookup. - utility tokens: the literal queries "data"/"bypass" resolve to a ranked data color space, while "unknown" stays a literal name/alias match only. No new public API -- resolve()'s existing string_view signature now gives richer answers. The direct OCIO lookup, the informal-alias table, and the input-name passthrough on total miss are all preserved unchanged; every tier returns a view backed by an existing color space name or alias, and none of them trigger the interoperability bootstrap. Grammar and sanitization reuse the existing pvt helpers. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…resolve Add the registry-equivalence tier of the Color Interop Forum reading-side resolution cascade to ColorConfig::resolve(): after the syntactic tiers miss, canonicalize the queried interop id through the built-in interop identities registry and return this config's OWN color space that is definitionally the same color, matched by value. The user's own equivalent space is preferred over building any cross-config processor -- this tier returns only names, never a processor. The match is backed by a new process-global registry fingerprint index: it fingerprints the built-in identities config's own simple color spaces through the same interopified / PROCESSOR_CACHE_OFF probe path the query side uses, so registry and query fingerprints compare directly. The index is the one new primitive this needs; it assembles entirely from the landed foundation (registry config, interopified copy, probe protocol, fingerprint compute/match, simple-space classification). It is built once, lazily, on the first query that reaches this tier, and is immutable and content-addressed for the life of the process. Its accessor is a self-contained anonymous-namespace free function so the write-side derivation can fingerprint the same registry the same way. Per query, the tier walks this config's simple spaces in the classification's sorted, deterministic order: a cheap explicit-interop-id compare first (OCIO >= 2.5), then a tolerance-gated fingerprint match through the process-global cached path; the first match wins. Utility tokens (data/unknown/bypass) name a color state, not a color, so they miss automatically and never reach a fingerprint compare. No new public API: resolve()'s existing string_view signature now gives richer answers. The syntactic tiers and the input-name passthrough on total miss are unchanged, and the tier is fully lazy, so ColorConfig construction and every earlier-terminating query stay fingerprint- and bootstrap-free. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add test_interop_resolve() to color_test.cpp, exercising every tier the enriched ColorConfig::resolve() now walks: the stripped-namespace retry through a real alias; the config-local "<config>:local:<base>" form, both a name/alias hit and a miss that doesn't fuzzy-fall-back; the explicit interop_id attribute (OCIO >= 2.5) matching with exactly one side's namespace stripped in both safe directions, and rejecting the both-sides-stripped cross-namespace false positive; the data/bypass utility-token ranking (self-identity short-circuit, and a plain data space beating one identified as the other token when no self-identified space exists); "unknown"'s asymmetry -- reachable only as a literal name/alias, never routed through the ranked search even when data spaces exist; the registry-equivalence (fingerprint) tier resolving a query to this config's own equivalent space, with a utility token an automatic miss even on an otherwise-interoperable config; and the historical input-name passthrough on total miss. A separate small fixture also documents that a literal, capitalized "Unknown"/"Bypass" color space is reachable through tier 1a's pre-existing case-insensitive OCIO lookup -- resolve() is deliberately not gated on CIF id-grammar validity, so the new utility-ranking machinery never even runs for those queries. Extend the python-colorconfig testsuite for bindings parity: one new resolve() call against the checked-in test config exercises the stripped-namespace tier from Python, where it previously only echoed the input back. Existing ref output is otherwise unchanged. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Enrich ColorConfig::get_color_interop_id(string_view) from "declared id, else
a static syntactic table, else empty" into the Color Interop Forum write-side
four-step derivation, reusing the registry fingerprint index. No new public
signatures -- the existing overload just gives richer answers.
Cascade (first step to produce an id wins):
1. An author-declared interop_id on the resolved space is returned verbatim
and is unconditionally authoritative. Only a non-empty value counts as
"declared": an unset attribute (empty string) now falls through to the
tiers below instead of short-circuiting to empty. Utility sub-case: a
data space with no declared token resolves to "data" here, before any
fingerprint tier (isData() works on all OCIO 2.x; the declared read is
OCIO 2.5+).
2. Fingerprint the resolved space and match it against the built-in interop
identities registry; return THAT registry identity's own interop id (a
process-global-stable string), not the query's name. Gated to skip data,
config-unique, and skip-matching spaces. Reuses the registry fingerprint
index and cached probe path built for the read side -- one new anon-ns
helper walks the index in its sorted deterministic order and returns the
matched entry's interop_id attribute (OCIO 2.5+, where the registry is
the studio config whose space names differ from the CIF ids), else its
name.
3. When the config has a name and the query resolves to a real space,
generate a config-local id "<config>:local:<space>", both segments
sanitized independently per the CIF grammar. The generated string is
interned via ustring so the returned view outlives the call.
4. Otherwise empty -- an unidentified space is never given a guessed default.
Two deliberate decisions, documented on the public method:
(a) The legacy static id/CICP table is kept as a syntactic fallback between
step 2's real fingerprint match and steps 3-4, never as the final-resort
match. Retiring it would change get_cicp(), which consults the same
table to map an id back to a CICP tuple; keeping it there leaves
get_cicp() unchanged while a genuine fingerprint match is always
preferred.
(b) Step 3 always attempts rather than hiding behind a knob: this overload
can gain no opt-in parameter, and its two natural preconditions (a
non-empty config name and a resolvable query) already keep it from
firing on a genuine miss.
Unchanged: the declared-id read still fires first and wins; the CICP-tuple
overload and get_cicp() are untouched; a total miss still returns empty;
nothing runs at construction time (step 2 wakes the fingerprint engine lazily,
on the first query that reaches it).
CIF recommendation "An ID for Color Interop":
https://github.com/AcademySoftwareFoundation/ColorInterop/wiki
Assisted-by: Claude (claude-fable-5 orchestration)
Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add test_interop_derive() to color_test.cpp, exercising ColorConfig::get_color_interop_id(string_view)'s four-step Color Interop Forum write cascade end to end: the isData utility sub-case beating a would-be fingerprint match, a genuine registry fingerprint match returning the registry identity's own id, a no-match space falling through to a generated "<config>:local:<space>" id (both segments sanitized independently via the landed pvt::sanitize_id_token, exercised directly rather than hand-typed), an unresolvable query staying empty, and a declared interop_id attribute winning unconditionally over what fingerprinting would otherwise produce -- the single most important regression vector for step 1's precedence over step 2. Small fixture configs isolate each step the same way test_interop_resolve does, so one config's setup can't accidentally satisfy a different step's assertion. Extend the python-colorconfig testsuite for bindings parity: one new print in test_colorconfig.py queries a color space in the named test fixture with no built-in registry equivalent, showing the enriched get_color_interop_id now reaches its generated config-local id where it previously returned empty. The new line is identical across every OCIO-version ref file (it only touches the second, explicitly-loaded test config, not the first config that varies per OCIO build), so all six ref files gain the same addition with existing lines otherwise unchanged. No new public API; both overloads' existing behavior is untouched. CIF recommendation "An ID for Color Interop": https://github.com/AcademySoftwareFoundation/ColorInterop/wiki Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
The utility-token ranking skipped any data space named "raw" to avoid matching the synthetic one-space OCIO::Config::CreateRaw() config, but that over-excluded a real config's legitimately-named "Raw" data space. Key the skip on the config's single-colorspace shape (one color space) rather than the name alone, so a "Raw" data space that sits alongside other spaces is a valid data/bypass target again. Also note in equivalent()'s doc comment that both names are resolved first, so color interop IDs and aliases participate on either side. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Sweep every grammar-valid canonical id the built-in interop identities registry declares: where it resolves to a real space in the active config (ocio://default, plus the builtin studio config on OCIO >= 2.5), assert get_color_interop_id(resolve(id)) returns the id up to removing one leftmost namespace from a single side (never both, the rule resolve() itself uses). Exercise the studio config's declared "ocio:g24_rec709_scene" live case explicitly -- it round-trips only via the stripped-form arm. Add a bypass-resolution test for a real "Raw" data space that sits alongside other spaces (regression for the single-colorspace-shape skip), and assert a color interop ID and a native config name for the same encoding are equivalent(). Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add a single internal wrapper over OCIO's two-config GetProcessorFromConfigs that dispatches between the plain and context-aware overloads and maps any OCIO failure to a null processor plus an error string, so cross-config color routing has one place to enforce its failure policy. No call sites route through it yet; a unit test exercises the success, missing-role, and context-aware paths via a probe-pixel wrapper. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
When createColorProcessor is asked for a color space this config lacks but that is a registry-known interop identity, and the config is color- interoperable (natively or via in-memory repair), reconcile the conversion across configs through the shared interchange roles instead of erroring on the name. The gate consults the interoperability state, not bare name- presence, and every route is drawn through the single cross-config chokepoint. Each reconciliation narrates its resolution on the debug channel and, on failure, on the ColorConfig error string. OCIO strict parsing opts out and restores today's hard error; non-strict parsing continues with a pass-through. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Route ColorConfig::createDisplayTransform through the interop bridge when the input color space is a registry-known interop identity the current config lacks: the foreign source is drawn from the built-in interop identities config and bridged into this config's display/view via a new display-view sibling of the cross-config chokepoint. Same strict/lenient/narration policy as the color-space route (one policy, two routes). On bridge failure the display path takes the strict-aware fallback -- strict OFF continues with a pass-through (the source is never silently reinterpreted as scene_linear), strict ON restores today's hard error -- replacing the prototype's silent setSrc(scene_linear) continue-on-failure. Adds the pvt display-route probe shim and unit_color coverage: cross-config display success (probe-pixel vs the direct chokepoint route, 1e-6), the not-reinterpreted-as-scene_linear regression (strict off pass-through + strict on hard error), and zero-change for local inputs. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…ld OCIO Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Add a dedicated testsuite directory covering the cross-config bridge end to end via oiiotool: a registry-known scene space absent from the current config routes through --colorconvert, the same for --ociodisplay with a foreign input source, OCIO strict parsing restoring today's hard error, non-strict parsing falling back to a pass-through with narration, and an ordinary local-to-local conversion proving the new machinery leaves that path untouched. Register the directory alongside oiiotool-color and document the behavior in the oiiotool color management docs. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…uite refs, strict fast path Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
When cross-config color-space reconciliation cannot build a real transform and OCIO strict parsing is off, the pipeline continues with a pass-through no-op processor instead of failing. The pixels are left untouched, but the caller was still tagging the result with the requested destination color space -- metadata asserting a conversion that never happened. Detect the fallback via the same signal the ColorConfig error string already carries for it (a non-null processor paired with a pending ColorConfig error means the lenient path was taken, since every genuine success path clears that error first). When detected, keep the output tagged with its actual, unconverted source space instead of the requested one, mirroring the existing "isData" convention that already preserves source tagging for non-color data. The equivalent display-transform pass-through (forward direction) gets the same treatment. Extends the fallback testsuite scenario to assert the resulting color-space tag by name, so a regression back to the old mistagging behavior would be caught. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
The ADR-0018 honest-tag fix for lenient cross-config pass-through covered colorconvert and the forward ociodisplay direction, but left the inverse direction still mistagging. On a lenient fallback (foreign from-space, non-interoperable config, strict parsing off) the inverse path continued with a pass-through no-op, yet tagged the output with the scene `from` space the inversion was reaching for -- while the pixels never left the display encoding they arrived in. The same lying-metadata class the ADR forbids, on an untested edge. Tag the inverse pass-through with the (display, view) color space the pixels are actually in -- getDisplayViewColorSpaceName(display, view) -- never the `from` we failed to produce. The display/view default resolution is hoisted above the direction split since both the forward tag and the inverse fallback tag need it. Detection uses the same signal as the forward fix: a non-null processor paired with a pending ColorConfig error means the lenient path was taken. Adds cross-config testsuite scenario (6): inverse --ociodisplay on the non-interoperable config, asserting the resulting color-space tag by name (version-stable), so a regression back to the old mistagging is caught. The non-interop test config gains a local display/view for the inverse path to target. Assisted-by: Claude (claude-opus-4-8 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
… on OCIO < 2.3.1 OCIO's Config::createEditableCopy() drops the config's default view transform name on OCIO versions before 2.3.1 (fixed upstream in bc8569b). ColorConfig::Impl::init() makes an editable copy at two call sites -- once for the built-in config (ocio://default) and once for a config loaded from file -- so both silently lost the default view transform name on affected OCIO versions. Add copy_config(), a small wrapper around createEditableCopy() that, on OCIO < 2.3.1, captures getDefaultViewTransformName() before the copy and restores it with setDefaultViewTransformName() if the copy dropped it. Both accessors are present in OCIO 2.3.0, so the gated block compiles cleanly there; on 2.3.1+ the guard compiles out and createEditableCopy() is used as before. Use copy_config() at both call sites in ColorConfig::Impl::init(). Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…sRGB The CICP tuple with Rec.709 primaries (color_primaries=1) and the IEC 61966-2-1 sRGB transfer (transfer_characteristics=13) describes display-referred sRGB: CICP, per ITU-T H.273, describes the encoding of the actual (already display-referred) pixel values. The reverse CICP->interop-ID lookup matched this tuple to the scene-referred identity first; reorder color_interop_ids[] so the display-referred identity wins, and add a unit test locking the mapping. Affects every reader that resolves a color space from a CICP tuple (PNG cICP, and any other format carrying CICP), since the tuple's meaning is format-independent. Assisted-by: Claude (claude-fable-5 orchestration) Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
png_set_cICP arrived in libpng 1.6.46 and png_set_mDCV in 1.6.50. Below those, PNG_cICP_SUPPORTED / PNG_mDCV_SUPPORTED are undefined, the writer compiles the chunk out and silently no-ops. The source has always handled this correctly; the tests did not, and asserted on chunks the build could not produce. Every ASWF CI container -- 2025 and 2026 alike -- ships PNG 1.6.34, so these tests could never have passed there. Compute the capability once in runtest.py (png_has_cicp / png_has_mdcv, with PNG_VERSION_OVERRIDE to exercise both paths) instead of each test re-parsing the version, and drop oiiotool-colorroundtrip's bespoke parser onto it. Two tests gated here, each with a reference for the unsupported case: - oiiotool-colorprofile skips ENTIRELY. It observes profile selection through a cICP-tagged PNG, so without the chunk the reader falls back to its fixed sRGB-scene assumption and all six cases echo the same answer. Asserting that collapsed output against a variant reference would be worse than failing -- a green test that verifies nothing. Verified against CI's actual old-libpng output, which does show all six lines identical. - cicp-write-strip skips only its PNG half; the EXR half is libpng-independent and still runs. Both paths verified locally via PNG_VERSION_OVERRIDE, and the version predicate checked at the 1.6.44/1.6.46 boundary. Still to do: oiiotool-colorroundtrip and oiiotool-colorpolicy-config need partial gates too, but their variant references have to be captured from a real old-libpng CI run -- a local simulation cannot produce them, because this machine's libpng does write the chunks, so the surviving lines would carry cICP-influenced values that old libpng never yields. Testsuite unchanged: 12 failures out of 222, same set. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-opus-5)
The 1-argument resolve() returns the name unchanged when no tier recognizes it -- longstanding shipped behavior that callers depend on, but which cannot distinguish "not recognized" from "resolves to itself". Add a second overload taking an explicit failover, returned only on a total miss; passing "" makes the distinction. The existing overload is untouched (a defaulted parameter would have changed its mangled name). Python spells it resolve(name, failover=None), where None dispatches to the 1-argument form.
…ucts ColorSpaceInfoOptions and ColorSpaceSearchOptions each gain `profile` (names a config-declared policy profile) and `policies` (inline oiio:colorpolicy:* overrides). Both are accepted and ignored today, and are honored once the policy layer lands; declaring them now keeps that landing from being an options-struct ABI break. They are policy inputs, not context variables: the existing `context` map stays the OCIO context mechanism. Both are std::string, matching every other string field in these structs -- a string_view field in a caller-constructed options struct is a dangling-lifetime trap.
The string getter forced every consumer to scrape a report documented as unparseable. Return a ColorConfigDebugInfo instead, with fixed fields for the stable identity (OIIO/OCIO versions, config name, structural and context-folded cache ids, interop registry data version) and a to_string() rendering the same paste-able report for bug reports. Interchange discovery becomes a ColorInterchangeState enum whose Pending value preserves the existing contract: querying never triggers the lazy discovery, so one that has not run reports as pending rather than as a negative result. Cache counts go in a std::map keyed by layer name rather than one field per layer: layers churn, and map contents are not ABI, whereas a field per layer would make every future cache change an ABI break. The name is snake_case, matching every sibling 3.2 addition (get_color_space_info, find_color_spaces, clear_caches); getDebugInfo was the only camelCase one. DebugInfoOptions stays alongside it. There was no oiiotool consumer to update -- the only callers were the unit test and the Python binding.
The modifier is the only one of the six whose spelling did not match the API parameter it feeds (ColorConfig::find_color_spaces takes `chromaticities`). Make `chromaticities=` canonical in --help and the docs, and accept `chrm=` as a shorthand -- that spelling echoes PNG's cHRM chunk rather than inventing an abbreviation. The testsuite now exercises both spellings.
The binding shipped with no Python-side exercise; its only guard was the C++ test_color_interop_ids_all_sync. Add testsuite coverage asserting the non-empty tuple of str, the canonical form of the registry's own ids (lowercase, no whitespace, unique, sorted), stability across calls, the spec-mandated members, and agreement with what ColorConfig.get_color_interop_id() hands out for registry-identified spaces. Properties are printed rather than the id list itself, so the reference survives the registry gaining identities.
get_debug_info() now returns a struct, so it is no longer one of the five proven verbs (serialize, from_text, evolve, archive, clear_caches) and may land separately. Record that its empty options struct has no other caller and must move with it rather than be left stranded.
The previous refresh of ref/out-libpng15.txt was wrong in a way that looked right. It assumed the historical one-line delta (the cICP line) was the whole difference, and regenerated the variant by deleting just that line from the modern output. It is not the whole difference. Without a cICP chunk the PNG reader falls back to its fixed sRGB-scene assumption, so two blocks also flip oiio:ColorSpace from srgb_rec709_display to srgb_rec709_scene. A locally-derived variant cannot capture that: a machine whose libpng writes the chunk produces surviving lines carrying cICP-influenced values that old libpng never yields. Replaced with the actual out.txt captured from the VFX2026 container (PNG 1.6.34). The delta against ref/out.txt is now exactly the two capability consequences -- the absent CICP line, and the two colorspace flips it causes. Same lesson as the capability-gating commit: a variant reference for a capability this build HAS must come from a build that lacks it, never from a simulation. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-opus-5)
Enumerating the canonical id set is development-time introspection, which ADR-0015 places with oicio and the phase-0 reference implementation rather than in OIIO. Callers obtain interop IDs from resolve() and get_color_interop_id(); nobody needs the list in order to use an ID. Deletes the header, the namespace and its implementation in color_registry.cpp, and the OpenImageIO.color_interop_ids() binding, and reverts 7bc2330 (the testsuite coverage of that binding), whose gap no longer exists. pvt::embedded_interop_identities_ids() stays -- it is the registry-side source the remaining drift guard reads. test_color_interop_ids_all_sync is deleted rather than rehomed: both things it asserted (all() is an exact-set match for the registry scan, and all()'s storage is stable) were properties of all() itself. With all() gone there is no second party to compare the registry against; test_legacy_table_registry_sync already guards the one remaining consumer, the static CICP/interop-id table, against the same pvt::embedded_interop_identities_ids() scan.
Restores the builtin-id list capability removed in 5a7f7c0, in its final shape. The list is wanted; what was not wanted was a set of constants, and separately its own header and namespace. static cspan<string_view> get_builtin_interop_ids(); Static because the builtin ids come from the embedded registry, not from any config instance -- there is no `this` to consult. Same backing data and the same process-lifetime span semantics as the removed ColorInteropIDs::all(), so the storage-identity assertion still holds. The name restores what ADR-0006 originally proposed; ColorInteropIDs was an artifact of the constants detour. color_interop_ids.h and its namespace stay deleted. The definition sits in the v3_1 block of color_registry.cpp alongside ColorConfig, not in the current-namespace pvt block where the registry scan it calls lives. Restores the testsuite consumer reverted in 5a7f7c0, retargeted at the new spelling with every assertion kept, plus a check that the static is reachable through an instance. Restores the registry-drift guard in color_test.cpp as test_builtin_interop_ids_sync.
…nsion Carries the fix from the P1-1 slice (zl-interop-id-grammar) back to dev, where it was missing. resolve()'s doc comment listed the tier that matches against a color space's explicit `interop_id` attribute inside a block introduced as "see the Color Interop Forum recommendation", so it read as part of the recommendation's search. It is not. The merged CIF v1.0.0 text states that the `interop_id` attribute is *not* used when searching a config, and gives the reason: the same id may legally appear on several color spaces, so the config author expresses precedence through aliases. Keep the behavior -- the tier fires only after the name/alias tiers have all missed, so it never overrides that precedence; it only makes an id reachable that no alias claimed. Label it accurately instead, the same treatment `bypass` already gets two lines below. Documentation only. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-opus-5)
Remove the branch-only public ColorConfig prototypes and Python surface for characterization, registry search, config construction, policy, archive, diagnostics, and cache control. Keep existing public integration seams stable, route processor creation and tool behavior through private full resolution and characterization helpers, and preserve the legacy cheap resolve semantics. Assisted-by: Codex / GPT-5 Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Hide the branch-only ColorConfig prototypes and Python surface for characterization, registry search, config construction, archive, diagnostics, and cache control from downstream 3.2 consumers. Retain the same internal C++ feature kernel, tool behavior, and native regression coverage as the 3.3 sibling so fixes can stay synchronized while 3.2 remains source-compatible with the established public API. Assisted-by: Codex / GPT-5 Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
Keep branch-only move construction available to OIIO_INTERNAL factories without exposing it as new 3.2 downstream API. Align internal and tool comments with the private facade boundary. Assisted-by: Codex / GPT-5 Signed-off-by: Zach Lewis <zachcanbereached@gmail.com>
…tate The "Planned" subsection of the color policy chapter still listed three features as "not yet available" that are implemented on this branch, so the doc contradicted the code. Replace the section with documentation of what exists, and complete the precedence table: - Composable profile selection (layer 3) is implemented: apply_profile_selection() in src/libOpenImageIO/color_metadata_plan.cpp (:49), layered as env var OPENIMAGEIO_COLORPOLICY then global attribute oiio:colorpolicy:profile (:109-125), exercised by testsuite/oiiotool-colorprofile. Documented in a new "Composable profile selection" subsection, and the precedence table gains its layer-3 row (the old text said "Layer 3 ... is planned"). - oiio:colorpolicy:write:force_interop_id and write:verbose are read in ColorWritePolicy::snapshot() (color_metadata_plan.cpp:327-329) and honored in the writers: fitsoutput.cpp:171, jpegoutput.cpp:274, jxloutput.cpp:388, png_pvt.h:830/:910, tiffoutput.cpp:937. Verbose derivation of chromaticities/gamma is at color_metadata_plan.cpp :428-476. Documented in a new "Verbose and forced write emission" subsection. - The oiio:broadcast delivery mapping is implemented behind the oiio:colorpolicy:write:broadcast key (color_metadata_plan.cpp:332, CICP container mapping :392-406, mDCV volume :503-513), with the profile-name convention shown in testsuite/oiiotool-colorroundtrip/src/broadcast.ocio. Documented in a new "The oiio:broadcast delivery profile" subsection. Nothing in the old list remains unimplemented, so no "Planned" section survives. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
IGNORE_HOMEBREWED_DEPS populates CMAKE_IGNORE_PATH, but that only blocks exact directories -- config packages (<prefix>/lib/cmake/<Pkg>) are found by prefix search and slip through, and local dependency child builds never saw even that much. Concretely: a local OpenEXR build resolved Homebrew Imath 3.2 while OIIO linked the local-dist Imath 3.1.10, failing the final link with mangled-symbol (Imath_3_1 vs Imath_3_2) undefined references. Add the Homebrew prefixes to CMAKE_IGNORE_PREFIX_PATH (CMake 3.23+), which does block config-package prefix search, and forward it to build_dependency_with_cmake children alongside CMAKE_IGNORE_PATH. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
…ce names ColorConfig::getDisplayViewColorSpaceName takes const std::string&, and ImageBufAlgo::ociodisplay passes string_views, so the call sites build temporary strings. The shared-view branch (<USE_DISPLAY_NAME>) returned c_str(display) -- a pointer into that temporary, dead at the end of the caller's full expression, one statement before the caller copies it. On macOS/libc++ the 22-char test display name sits in SSO stack storage and the stale read happens to survive (still UB); on Linux/libstdc++ (SSO cap 15) the buffer is heap-allocated, freed at the semicolon, and tcache immediately writes a heap pointer into it -- python-colorconfig died on every Linux CI job with 'utf-8' codec can't decode byte 0xf9 when pybind11 decoded the garbage oiio:ColorSpace value. 39d5285 materialized the result at one call site but one statement too late. Return ustring-interned storage from the shared-view branch instead, so every caller is covered. Fixes the cir-afj CI failure. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
Below OCIO 2.5 the config parser warns on and drops the interop_id ColorSpace key, so identity information can only come from the value (fingerprint) tier. Four tests asserted 2.5-only outcomes: - color-interop-convert, oiiotool-colorverbose: add ref/out-ocio24.txt variants taken verbatim from the OCIO 2.4 CI run (byte-identical on the 2.3 containers): the registry composite names spaces by CIID below 2.5 (lin_ap1_scene vs ACEScg), and the builtin-default write plan cannot derive cicp/interop_id from a bare CIID name there. - unit_characterization_search: probe whether the loaded config exposes the authored interop_id and skip the twin-inference expectations when it does not (the g26_p3d65_display twin link never forms). - unit_color registry round-trip: accept the one value-identical pair (srgbe_p3d65_display lands on srgb_p3d65_display) -- same math, different referredness, indistinguishable to pixel probes; declared ids disambiguate at 2.5+. Warning-noise suppression itself landed in 4a2c81a; these are the behavioral leftovers from CI run 30772613267. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
748eee7 gated cICP/mDCV-dependent assertions for cicp-write-strip and oiiotool-colorprofile, but oiiotool-colorpolicy-config and oiiotool-colorroundtrip still asserted chunks a pre-1.6.46 libpng build cannot write. Add ref/out-nocicp.txt for both, taken verbatim from the CI containers' actual output (libpng 1.6.34; identical bytes on the OCIO 2.4 and 2.5 jobs): no CICP chunk lines, PNG readback resolving to srgb_rec709_scene without the tuple, and no mDCV round-trip section. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
…olve The write planner requests the interop-id field through characterize_color_space, which returned an invalid record whenever the queried name landed on no color space in the ACTIVE config -- before the derive cascade ever ran. But a color interop ID is meaningful against ANY interoperable config: that is the point of the embedded registry, and the cascade's syntactic tiers (declared id, legacy-table equivalence, registry) answer without needing a config-resident space, never guessing. The direct pvt::derive_color_interop_id facade already behaved this way; the characterization path did not, so EXR writes under builtin configs lacking the CIID aliases (bundled with OCIO < 2.4) silently omitted colorInteropID (CI: unit_color_metadata_plan and openexr-suite red on OCIO 2.3 containers) and write plans degraded below 2.5. On a no-config-space miss, still run the interop-id cascade for the requested field; every other characterization field genuinely requires a config space and stays unavailable, and the record skips the shared cache (no honest config-space key). Reverts the oiiotool-colorverbose out-ocio24.txt reference added earlier today -- the plan behavior it pinned WAS this bug; below-2.5 plans now match the baseline reference. Fixes cir-1z8. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
…oftwareFoundation#5360) Windows Unity builds might combine exrinput.cpp and exrinput_c.cpp into one translation unit. The forward declaration in exrinput.cpp lacked OIIO_EXPORT while the definition in exrinput_c.cpp has it, causing MSVC error. I'm not sure why this never failed before today! Assisted-by: Claude Code / Sonnet 5 Signed-off-by: Larry Gritz <lg@larrygritz.com> (cherry picked from commit f0810af)
- oiiotool-colorpolicy-config and oiiotool-colorprofile capture raw oiiotool output; debug builds (VFX2025 Debug, Sanitizers) default OPENIMAGEIO_DEBUG=1 and leak advisory DBG lines the references do not expect. Pin OPENIMAGEIO_DEBUG=0 in both run.py files. - clang-format 17 over the files CI flagged (color.h, jxlinput.cpp, color_ocio.cpp, color_test.cpp, oiiotool.cpp) -- no code changes. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
Review pass over the day's changes: - characterize_color_space_impl: the no-config-space interop-id fallback now rides the normal flow (shared cache, derive tier, publication) under the raw query name instead of a bespoke uncached branch -- a repeated write of the same unresolvable name costs one cache hit, not a registry walk. Names the cascade cannot identify keep the original invalid-record contract. - test_registry_round_trip accepts the below-2.5 fingerprint collision by proving value-identity with equivalent() instead of hardcoding one pair; a non-equivalent mislanding still fails. - characterization_search_test anchors its capability probe: at OCIO 2.5+ the authored id must be visible, so a probe regression fails instead of silently skipping assertions. - OPENIMAGEIO_DEBUG pinned once in runtest.py (guarded, like OCIO_LOGGING_LEVEL) instead of per-test; color-interop-convert's deliberate =1 still overrides. - dependency_utils: fold the two ignore-var forwards into one loop. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
Today's unity-build fix let Windows CI compile and run tests for the first time on this branch, exposing that five color tests (oiiotool-colorwriteplan, -colorpolicy-config, -colorroundtrip, -colorverbose, -colorprofile) drive oiiotool through POSIX-shell constructs -- per-command 'env VAR=' prefixes, single-quoted echo, grep pipelines -- that cmd.exe cannot execute (the quotes reach oiiotool as literal argument bytes). Gate their registration on NOT WIN32 with a comment; the behavior they exercise is platform-neutral, and rewriting them portably is tracked follow-up work. Signed-off-by: Zach Lewis <zachcanbereached@gmail.com> Assisted-by: Claude (claude-fable-5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a draft for visibility, per the July TSC discussion — it is not intended to merge. It shows the complete working state behind the color-metadata PR series I'm posting separately, so reviewers can see where each small PR is headed and how the pieces fit. The real review units are the individual PRs; this branch is the map.
cc @lgritz @brechtvl @doug-walker
What's on this branch
Everything discussed at the TSC, working end to end and exercised against the full upstream CI matrix: the color suites pass on Linux and macOS against OCIO 2.3, 2.4, and 2.5 and across old/new libpng, including the sanitizer job. (One environmental straggler is tracked and doesn't touch color behavior: a Radiance-HDR round-trip on the no-SIMD job.)
resolve()/equivalent()(namespaced IDs, config-local form, reserved innerlocalnamespace), a built-in set of interop identities usable as aliases from any config on OCIO 2.3+, and cross-config conversion through the interchange roles — an ID resolves and converts even when the current config has no such space, falling back to the built-in implementations at operation time.oiiotool --colorwriteplan), fixed Forum-aligned defaults.oiiotool --colorinfoand--colorspacesearch(experimental flags), backed by internal machinery only: the series proposes zero new public C++ or Python symbols. Existing seams are enriched and the cheap calls stay cheap; promotion of any inspection API waits for a demonstrated consumer.serialize, construct-from-memory,.ociozarchiving,getDebugInfo,evolve(thread-safe immutable copy-with-modifications: working dir, context overrides), and cache management.How this lands for real
As the series of small PRs (first ones opening now): two standalone fixes, then the interop-ID grammar, the built-in identities registry (fronted by a design issue), central read reconciliation, the transform-comparison identification engine, native EXR
colorInteropIDwrite, and cross-config ID conversion. Each is independently reviewable and lands on its own merits; nothing in the series depends on a later PR to be correct, and none of them adds a public API symbol.This dovetails with the write-side color-metadata cleanups Brecht is landing right now (#5387, #5388, #5390, #5391, tracked in #4980) — same seams, compatible direction, and I'll rebase the series onto those as they merge rather than duplicate them.
Questions this branch is meant to make easy to ask
Pointing at concrete working code beats speculating: if a behavior here looks wrong for your pipeline, or a public signature looks like it will age badly, that's exactly the feedback the small PRs need — comment here or on the relevant PR as they open.
(Companion demo — the mechanisms runnable standalone against PyOpenColorIO, no OIIO build: https://github.com/zachlewis/color_interop_demo and https://zachlewis.github.io/color_interop_demo/.)