Skip to content

CMM: spectral PCS port at a chain edge gets no relative/absolute conversion #2331

Description

@maxderhak

Describe the Bug

A spectral PCS port at a chain edge gets no relative/absolute conversion.
Interior spectral connections convert correctly against the spectral white point;
the first and last transform in a chain do not.

On master the same port gets something worse — the XYZ media-white affine applied
to spectral samples 0..2, leaving the rest of the spectrum untouched, because
AdjustPCS() reads pixel[0..2] as X, Y, Z regardless of what the port carries.

The correct behaviour

Relative spectra are absolute spectra divided by the spectral white point; absolute
spectra are relative spectra multiplied by it. No conversion is needed when the
tag's absolute-ness matches the rendering intent's — absolute tags at absolute
intent, relative tags at relative intent — and the conversion applies to
reflectance, transmission and radiant spectral PCS. Bidirectional reflectance and
sparse-matrix PCS are excluded: their sample vectors are not spectra, so
element-wise scaling is not dimensionally meaningful.

This is implemented on branch refactor/pcs-adjust-in-pcsxform — see
docs/superpowers/plans/2026-08-26-spectral-pcs-white-point-conversion.md — and is
live at interior connections.

Why the edges miss it

CIccPcsXform::ConnectFirst() and ConnectLast() carry the spectral conversion and
are unit-tested, but they cannot run for a spectral port: their only caller,
CIccCmm::CheckPCSConnections(), gates both chain-edge blocks on
IsSpaceColorimetricPCS(), and a spectral xform's GetSrcSpace()/GetDstSpace()
returns the reduced spectral type, which never satisfies it. The interior loop does
admit spectral ports; the edge blocks do not.

Measured on that branch: pcsXformCount() == 0 for a spectral chain edge — no
CIccPcsXform is constructed there at all.

Build Instructions

git clone https://github.com/InternationalColorConsortium/DemoIccMAX.git
cd DemoIccMAX
cmake --preset vs2022-x64 -B msvc -S Build/Cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build msvc -- /m /maxcpucount

Reproduce Bug or Crash

Build a v5 profile declaring pcs = icSigLabData and spectralPCS with a
reflectance signature, give it a DToB1 tag, and apply it at absolute colorimetric
intent with the spectral signature as the CMM destination — so the spectral port is
the trailing edge of the chain rather than an interior connection.

On master, samples 0..2 come back altered and 3..n untouched. A pipeline emitting
the constants 0.20 0.30 0.40 0.50 0.60 0.70 yields:

observed  0.193897  0.301255  0.406660  0.500000  0.600000  0.700000
                    ← altered as if XYZ →  ← untouched →

A relative-intent control over the identical fixture emits all six constants exactly,
isolating the adjustment as the cause.

On branch refactor/pcs-adjust-in-pcsxform the samples pass through unmodified — no
longer corrupted, but also not converted.

Root cause shared with #2330

m_bAdjustPCS can be set on an xform with no guarantee its port is XYZ or Lab
the two IsSpacePCS(m_Header.pcs) setters test the profile header, which stays Lab
or XYZ on a v5 spectral profile, not the port — while AdjustPCS() unconditionally
reads pixel[0..2] as X, Y, Z. A spectral PCS port and an MCS port are two
consequences of that single assumption.

Proposed fix

Widen CheckPCSConnections()'s two chain-edge conditions from
IsSpaceColorimetricPCS() to IsSpacePCS() so spectral ports reach
ConnectFirst()/ConnectLast(), whose spectral branches already implement the
conversion.

Deliberately not done on refactor/pcs-adjust-in-pcsxform: it makes two
previously unreachable functions live for a new port type, and on that branch every
latent defect found lived in exactly such code — ConnectLast()'s adjustment branch,
for one, had been silently halving XYZ values since it was written because nothing
ever executed it. It wants its own change and its own tests rather than riding along.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BuildBuild system, CMake, compiler, or packagingDocumentationDocumentation-only or documentation-related changePythonPython, Cython, PyPI, pip, or packaging scopeTestingCTest, regression, or test coverageWindowsWindows, MSVC, clang-cl, or PowerShell scopeciContinuous integration workflow changesneeds-triageMaintainer triage required

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions