diff --git a/colorconv/colorconv.go b/colorconv/colorconv.go index a925676..29eab9d 100644 --- a/colorconv/colorconv.go +++ b/colorconv/colorconv.go @@ -384,6 +384,7 @@ func (c *ConvertColor) gamutMapChromaScale(L, a, b float64) (r, g, bl float64) { hi := 1.0 var mid float64 var foundR, foundG, foundB float64 + found := false // If even fully desaturated (a=b=0) is out of gamut, we'll clip for range 24 { mid = (lo + hi) / 2.0 @@ -392,6 +393,7 @@ func (c *ConvertColor) gamutMapChromaScale(L, a, b float64) (r, g, bl float64) { r0, g0, b0 := c.LabToSRGBNoGamutMap(L, a2, b2) if inGamut(r0, g0, b0) { foundR, foundG, foundB = r0, g0, b0 + found = true // can try to keep more chroma lo = mid } else { @@ -399,7 +401,7 @@ func (c *ConvertColor) gamutMapChromaScale(L, a, b float64) (r, g, bl float64) { } } // If we never found a valid in-gamut during binary search, try a= b =0 - if !(inGamut(foundR, foundG, foundB)) { + if !found { r0, g0, b0 := c.LabToSRGBNoGamutMap(L, 0, 0) // if still out-of-gamut (very unlikely), clip return clamp01(r0), clamp01(g0), clamp01(b0) diff --git a/prism/meta/icc/pcs.go b/prism/meta/icc/pcs.go index a382790..7febfc8 100644 --- a/prism/meta/icc/pcs.go +++ b/prism/meta/icc/pcs.go @@ -188,25 +188,34 @@ func NewUniformFunctionTransformer(name string, f func(unit_float) unit_float) * } type XYZtosRGB struct { - c *colorconv.ConvertColor - t func(l, a, b unit_float) (x, y, z unit_float) + c *colorconv.ConvertColor + t func(l, a, b unit_float) (x, y, z unit_float) + map_gamut bool } func NewXYZtosRGB(whitepoint XYZType, clamp, map_gamut bool) *XYZtosRGB { c := colorconv.NewConvertColor(whitepoint.X, whitepoint.Y, whitepoint.Z, 1) if clamp { if map_gamut { - return &XYZtosRGB{c, c.XYZToSRGB} + return &XYZtosRGB{c, c.XYZToSRGB, true} } - return &XYZtosRGB{c, c.XYZToSRGBNoGamutMap} + return &XYZtosRGB{c, c.XYZToSRGBNoGamutMap, false} } - return &XYZtosRGB{c, c.XYZToSRGBNoClamp} + return &XYZtosRGB{c, c.XYZToSRGBNoClamp, false} } func (n *XYZtosRGB) AddPreviousMatrix(m Matrix3) { n.c.AddPreviousMatrix(m[0], m[1], m[2]) } +// CanAbsorbMatrix reports whether it is safe for the pipeline optimizer to +// fuse preceding matrices into this transformer. When gamut mapping is +// active, the fallback path converts via Lab which requires the input to be +// actual XYZ values, so matrix absorption would break that invariant. +func (n *XYZtosRGB) CanAbsorbMatrix() bool { + return !n.map_gamut +} + func (c *XYZtosRGB) Transform(l, a, b unit_float) (unit_float, unit_float, unit_float) { return c.t(l, a, b) } diff --git a/prism/meta/icc/pipeline.go b/prism/meta/icc/pipeline.go index 315b162..7186ed4 100644 --- a/prism/meta/icc/pipeline.go +++ b/prism/meta/icc/pipeline.go @@ -64,7 +64,7 @@ func (p *Pipeline) finalize(optimize bool) { // Check if the last transform can absorb previous matrices if len(p.transformers) > 1 { last := p.transformers[len(p.transformers)-1] - if apm, ok := last.(*XYZtosRGB); ok { + if apm, ok := last.(*XYZtosRGB); ok && apm.CanAbsorbMatrix() { p.transformers = p.transformers[:len(p.transformers)-1] for { m := p.remove_last_matrix3()