fix(bpc): estimate black through the tag family the xform actually uses - #2310
Merged
Conversation
maxderhak
requested review from
ChrisCoxArt,
colourbill-ctrl and
xsscx
as code owners
August 25, 2026 20:35
CIccXform::Create() chooses between the AToBx/BToAx colorimetric tags and the DToBx/BToDx MPE tags, and a caller forces the colorimetric ones by passing bUseD2BxB2DxTags=false to AddXform(). CIccApplyBPC rebuilds its own cmm objects to find black, and hardcoded that choice as "version >= V5 ? colorimetric : MPE" instead of asking the xform it is adjusting. For a v4 profile carrying both families that is backwards: the transform applies BToAx while black is measured through BToDx, so the scale and offset BPC installs come from a pipeline that is never applied. These are exactly the profiles where the two pipelines are known to differ -- that is why a caller opts out -- so the error is not marginal, and it is silent. Only the B-side is affected. Create()'s input branch gates its DToBx lookup on spectralPCS or version >= V5, so a plain v4 profile resolves device->PCS through AToBx whatever the flag says; the output branch has no such gate and follows the flag alone. m_bUseD2BTags already existed on CIccXform but was dead -- set false in the constructor and never written since. Record it at the SetParams() sites in CIccXform::Create() and CIccXformMpe::Create(), after the icXformLutSpectral and icXformLutColorimetric adjustments have rewritten it, so it names the family the selection code actually walked rather than what the caller asked for. The Create() overload that is handed a tag outright keeps the constructor default: there is no selection to record, and no way for CIccApplyBPC to reproduce the caller's choice by rebuilding from the profile. CalcFactors() already receives the xform, so only pixelXfm() and getBlackXfm() gain a parameter. The V5 arm of the ternary is preserved: only the explicit opt-out changes behaviour, so the default path is byte-identical and the CLI tools cannot reach the changed configuration (iccApplyToLink decodes intent 10+ and 40+ as mutually exclusive cases, so BPC always implies bUseD2BxB2DxTags=true there). Regression: iccdev.bpc-d2b-tag-family builds a v4.3 CMYK printer profile whose BToA0 and BToD0 return different inks and whose AToB0 turns those into L* 40 and L* 5, both clearing the L*<=50 clip as distinct values. Applying one pixel with BPC enabled gives 0.349680 with the flag false and 0.499921 with it true; before this change both gave 0.499921, matching the MPE result rather than the BToA0-only reference. Two pre-existing gaps are left open: pixelXfm() still hardcodes icXformLutColorimetric, which closes the DToBx path on the input side for any profile, and the tag-passing Create() overload records nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xsscx
force-pushed
the
fix/bpc-d2b-tag-family
branch
from
August 25, 2026 20:37
2fd8bdf to
e284f75
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Updates BPC black-point estimation to use the tag-family preference recorded by the adjusted transform.
Changes:
- Records and exposes the selected DToB/BToD tag-family preference.
- Propagates that preference through BPC helper transforms.
- Adds a focused CTest regression.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
IccProfLib/IccCmm.h |
Adds tag-family accessors. |
IccProfLib/IccCmm.cpp |
Records tag-family selection during transform creation. |
IccProfLib/IccApplyBPC.h |
Extends BPC helper signatures. |
IccProfLib/IccApplyBPC.cpp |
Reuses the transform’s tag-family preference. |
Build/Cmake/Testing/CMakeLists.txt |
Registers the regression test. |
.github/ci/regression/bpc-d2b-tag-family.cpp |
Adds the synthetic-profile regression fixture. |
Comment on lines
+235
to
+237
| CIccCmm cmm(icSigCmykData, icSigLabData, true); | ||
| if (cmm.AddXform(pICC, icPerceptual, icInterpTetrahedral, NULL, | ||
| icXformLutColor, bUseD2BxB2DxTags, &hint) != icCmmStatOk) |
Comment on lines
+1375
to
+1377
| add_test( | ||
| NAME iccdev.bpc-d2b-tag-family | ||
| COMMAND "$<TARGET_FILE:iccBpcD2bTagFamilyTest>" |
| @@ -0,0 +1,314 @@ | |||
| /* | |||
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.
Problem
CIccXform::Create()chooses between theAToBx/BToAxcolorimetric tags and theDToBx/BToDxMPE tags, and a caller forces the colorimetric ones by passingbUseD2BxB2DxTags=falsetoAddXform().CIccApplyBPCrebuilds its own cmm objects to find the black point, and hardcoded that choice asversion >= V5 ? colorimetric : MPEinstead of asking the xform it is adjusting:For a v4 profile carrying both families this is backwards. The transform applies
BToAxwhile black is measured throughBToDx, so the scale and offset BPC installs come from a pipeline that is never applied. These are exactly the profiles where the two pipelines are known to differ — that is why a caller opts out — so the error is not marginal, and it is silent.Only the B-side is affected.
Create()'s input branch gates itsDToBxlookup onspectralPCS || version >= V5, so a plain v4 profile resolves device→PCS throughAToBxwhatever the flag says. The output branch has no such gate and follows the flag alone.Fix
m_bUseD2BTagsalready existed onCIccXformbut was dead — setfalsein the constructor and never written since.SetParams()sites inCIccXform::Create()andCIccXformMpe::Create(), after theicXformLutSpectralandicXformLutColorimetricadjustments have rewritten it, so it names the family the selection code actually walked rather than what the caller asked for.UseD2BTags()/SetUseD2BTags()as inline accessors — no layout or vtable change, so ABI is unaffected.CalcFactors()already receives the xform, so onlypixelXfm()andgetBlackXfm()gain a parameter.The
Create()overload that is handed a tag outright keeps the constructor default, with a comment saying why: there is no selection to record, and no way forCIccApplyBPCto reproduce the caller's choice by rebuilding from the profile.Blast radius
The V5 arm of the ternary is preserved, so only the explicit opt-out changes behaviour and the default path is byte-identical. The CLI tools cannot reach the changed configuration —
iccApplyToLinkdecodes intent10 +(no D2Bx) and40 +(BPC) as mutually exclusiveswitchcases, so BPC there always impliesbUseD2BxB2DxTags=true. No existing end-to-end baseline can shift; this is a library-API-only change.Regression coverage
iccdev.bpc-d2b-tag-familybuilds a v4.3 CMYK printer profile in code whoseBToA0andBToD0return different inks (cyan 0.60 vs 0.95) and whoseAToB0turns those into L* 40 and L* 5 — both clearingcalcSrcBlackPoint()'s L*≤50 clip as distinct values, since a pair that both clipped would leave the test toothless.Applying one CMYK pixel with BPC enabled:
truefalseBToD0absent (reference)Before the change the flag was inert and black came from
BToD0either way. After,falsematches theBToA0-only reference exactly. The test exits 1 before and 0 after.Verification performed
Configured and built with the repo's own
vs2022-x64preset (VS2022 Professional, MSVC x64 Release, vcpkg toolchain) and exercised through CTest:iccdev.bpc-d2b-tag-familyregisters as test Update Xcode Project Files, Add Unit Test for CVE-2023-44062, Build Scripts for libtiff + libxml2, Fix FPE #67 with labelsbpc cmm iccdev regression xform, builds, and passes.ctest -L "cmm|xform"— 16/16 pass.ctest -L regression— 95/95 pass.Re-verified after the branch was rebased onto current master (53 newer commits, including the apply-path guard retirement #2307 and the CLUT channel-count bounds #2306): full reconfigure and rebuild, test #90 passes with byte-identical output, and the full non-slow suite is 133/133.
Also cross-checked earlier with g++ against a hand-built
IccProfLib, producing byte-identical numbers to the MSVC run.Left open
Two pre-existing gaps outside this fix's scope:
pixelXfm()still hardcodesicXformLutColorimetric, which closes theDToBxpath on the input side for any profile. Moot for v4 non-spectral, but it means a v5 MPE-only profile cannot resolve a source black point at all.Create()overload records nothing, so BPC on an xform built that way still assumes the colorimetric family.🤖 Generated with Claude Code