Skip to content

[DRAFT — for visibility, not merging] Color metadata: the combined branch behind the upcoming PR series - #5392

Draft
zachlewis wants to merge 212 commits into
AcademySoftwareFoundation:mainfrom
zachlewis:zl-color-interop-3.2
Draft

[DRAFT — for visibility, not merging] Color metadata: the combined branch behind the upcoming PR series#5392
zachlewis wants to merge 212 commits into
AcademySoftwareFoundation:mainfrom
zachlewis:zl-color-interop-3.2

Conversation

@zachlewis

Copy link
Copy Markdown
Collaborator

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.)

  • Color interop ID support end to end — reading, resolving, writing, and converting per the Color Interop Forum's published guidance: ID grammar in the existing resolve()/equivalent() (namespaced IDs, config-local form, reserved inner local namespace), 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.
  • Transform-comparison identification — recognizing a space by what its transforms do (a config's "Studio Linear" is numerically ACEScg), used to derive correct interop IDs on write for configs that predate the ID.
  • Central read-side reconciliation — one priority-list heuristic adjudicating contradictory signals (interop ID, CICP, ICC, chromaticities), OpenEXR first; byte-identical resolution at defaults.
  • Write planning — per-signal decisions with a dry-run preview (oiiotool --colorwriteplan), fixed Forum-aligned defaults.
  • Inspection and search diagnosticsoiiotool --colorinfo and --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.
  • Config utilitiesserialize, construct-from-memory, .ocioz archiving, getDebugInfo, evolve (thread-safe immutable copy-with-modifications: working dir, context overrides), and cache management.
  • Automatic metadata maintenance around the color operations (update-or-erase, never guess), plus source-provenance attributes.

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 colorInteropID write, 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/.)

zachlewis added 30 commits July 12, 2026 15:57
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>
zachlewis and others added 30 commits July 28, 2026 04:40
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants