Commit 0730f0f2f for imagemagick.org
commit 0730f0f2f91f0c1af8157acb0d1c8f8d78a25bd7
Author: Black-Cat <iblackcatw@gmail.com>
Date: Tue Oct 6 11:51:11 2026 +0100
Fix LCH hue parsing and conversion of non-RGB fill colors (#9003)
The hue of lchab, lchuv and lch colors was parsed as a Lab "b", so every
hue came out wrong.
Colors given in a non-RGB colorspace were also copied unconverted into
the image by ConformPixelInfo() and LevelImageColors(), which broke
-fill with -opaque and +level-colors.
Fixes #8093
Co-authored-by: Cristy <12486802+urban-warrior@users.noreply.github.com>
diff --git a/MagickCore/color.c b/MagickCore/color.c
index 3d972ceeb..d55db3e9b 100644
--- a/MagickCore/color.c
+++ b/MagickCore/color.c
@@ -2549,8 +2549,17 @@ MagickExport MagickBooleanType QueryColorCompliance(const char *name,
color->green=(MagickRealType) ClampToQuantum((MagickRealType)
(scale*geometry_info.sigma+((double) QuantumRange+1)/2.0));
if ((flags & XiValue) != 0)
- color->blue=(MagickRealType) ClampToQuantum((MagickRealType)
- (scale*geometry_info.xi+((double) QuantumRange+1)/2.0));
+ {
+ if ((color->colorspace == LCHColorspace) ||
+ (color->colorspace == LCHabColorspace) ||
+ (color->colorspace == LCHuvColorspace))
+ color->blue=(MagickRealType) ClampToQuantum((MagickRealType)
+ ((double) QuantumRange*fmod(fmod(geometry_info.xi,360.0)+
+ 360.0,360.0)/360.0));
+ else
+ color->blue=(MagickRealType) ClampToQuantum((MagickRealType)
+ (scale*geometry_info.xi+((double) QuantumRange+1)/2.0));
+ }
}
else
{
diff --git a/MagickCore/colorspace-private.h b/MagickCore/colorspace-private.h
index b741db4a7..9b3b0dfca 100644
--- a/MagickCore/colorspace-private.h
+++ b/MagickCore/colorspace-private.h
@@ -1898,6 +1898,7 @@ static inline MagickBooleanType IsYCbCrCompatibleColorspace(
}
extern MagickPrivate void
+ ConformPixelInfoColorspace(const Image *,PixelInfo *,ExceptionInfo *),
ConvertGenericToRGB(const ColorspaceType,const double,const double,
const double,const double,const IlluminantType,double *,double *,double *),
ConvertRGBToGeneric(const ColorspaceType,const double,const double,
diff --git a/MagickCore/colorspace.c b/MagickCore/colorspace.c
index ce100176a..9d0f973b0 100644
--- a/MagickCore/colorspace.c
+++ b/MagickCore/colorspace.c
@@ -97,6 +97,96 @@ static MagickBooleanType
% %
% %
% %
++ C o n f o r m P i x e l I n f o C o l o r s p a c e %
+% %
+% %
+% %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% ConformPixelInfoColorspace() ensures the pixel conforms with the colorspace
+% of the image. A pixel that is sRGB compatible or CMYK is left as is.
+%
+% The format of the ConformPixelInfoColorspace method is:
+%
+% void ConformPixelInfoColorspace(const Image *image,PixelInfo *pixel,
+% ExceptionInfo *exception)
+%
+% A description of each parameter follows:
+%
+% o image: the image.
+%
+% o pixel: the pixel info.
+%
+% o exception: return any errors or warnings in this structure.
+%
+*/
+MagickPrivate void ConformPixelInfoColorspace(const Image *image,
+ PixelInfo *pixel,ExceptionInfo *exception)
+{
+ const char
+ *value;
+
+ double
+ blue,
+ green,
+ red,
+ white_luminance = 10000.0;
+
+ IlluminantType
+ illuminant = D65Illuminant;
+
+ assert(image != (const Image *) NULL);
+ assert(image->signature == MagickCoreSignature);
+ assert(pixel != (PixelInfo *) NULL);
+ if ((pixel->colorspace == image->colorspace) ||
+ (pixel->colorspace == CMYKColorspace) ||
+ (IssRGBCompatibleColorspace(pixel->colorspace) != MagickFalse))
+ return;
+ value=GetImageArtifact(image,"color:illuminant");
+ if (value != (const char *) NULL)
+ {
+ ssize_t
+ illuminant_type;
+
+ illuminant_type=ParseCommandOption(MagickIlluminantOptions,MagickFalse,
+ value);
+ if (illuminant_type < 0)
+ illuminant=UndefinedIlluminant;
+ else
+ illuminant=(IlluminantType) illuminant_type;
+ }
+ value=GetImageProperty(image,"white-luminance",exception);
+ if (value != (const char *) NULL)
+ white_luminance=StringToDouble(value,(char **) NULL);
+ ConvertGenericToRGB(pixel->colorspace,QuantumScale*pixel->red,
+ QuantumScale*pixel->green,QuantumScale*pixel->blue,white_luminance,
+ illuminant,&red,&green,&blue);
+ pixel->colorspace=sRGBColorspace;
+ if ((image->colorspace != CMYKColorspace) &&
+ (IssRGBCompatibleColorspace(image->colorspace) == MagickFalse))
+ {
+ double
+ X,
+ Y,
+ Z;
+
+ ConvertRGBToGeneric(image->colorspace,red,green,blue,white_luminance,
+ illuminant,&X,&Y,&Z);
+ red=(double) QuantumRange*X;
+ green=(double) QuantumRange*Y;
+ blue=(double) QuantumRange*Z;
+ pixel->colorspace=image->colorspace;
+ }
+ pixel->red=(MagickRealType) ClampToQuantum(red);
+ pixel->green=(MagickRealType) ClampToQuantum(green);
+ pixel->blue=(MagickRealType) ClampToQuantum(blue);
+}
+
+/*
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% %
+% %
+% %
% C o n v e r t G e n e r i c T o R G B %
% %
% %
diff --git a/MagickCore/enhance.c b/MagickCore/enhance.c
index 48b9f11d4..9f8d2cfd2 100644
--- a/MagickCore/enhance.c
+++ b/MagickCore/enhance.c
@@ -3231,6 +3231,10 @@ MagickExport MagickBooleanType LevelImageColors(Image *image,
MagickStatusType
status;
+ PixelInfo
+ black,
+ white;
+
/*
Allocate and initialize levels map.
*/
@@ -3242,6 +3246,12 @@ MagickExport MagickBooleanType LevelImageColors(Image *image,
((IsGrayColorspace(black_color->colorspace) == MagickFalse) ||
(IsGrayColorspace(white_color->colorspace) == MagickFalse)))
(void) SetImageColorspace(image,sRGBColorspace,exception);
+ black=(*black_color);
+ ConformPixelInfoColorspace(image,&black,exception);
+ black_color=(&black);
+ white=(*white_color);
+ ConformPixelInfoColorspace(image,&white,exception);
+ white_color=(&white);
status=MagickTrue;
if (invert == MagickFalse)
{
diff --git a/MagickCore/pixel.c b/MagickCore/pixel.c
index d0d502dca..e1f2beab1 100644
--- a/MagickCore/pixel.c
+++ b/MagickCore/pixel.c
@@ -215,6 +215,7 @@ MagickExport void ConformPixelInfo(Image *image,const PixelInfo *source,
assert(image->signature == MagickCoreSignature);
assert(destination != (const PixelInfo *) NULL);
*destination=(*source);
+ ConformPixelInfoColorspace(image,destination,exception);
if (image->colorspace == CMYKColorspace)
{
if (IssRGBCompatibleColorspace(destination->colorspace) != MagickFalse)
diff --git a/tests/cli-colorspace.tap b/tests/cli-colorspace.tap
index d1ee5853a..488bbfae7 100755
--- a/tests/cli-colorspace.tap
+++ b/tests/cli-colorspace.tap
@@ -23,7 +23,7 @@ if [ "X$depth" = "X255" ]; then
echo "ok"
exit 0
fi
-echo "1..19"
+echo "1..28"
# how to generate a one pixel (average rose) color and output its values
in="rose: -scale 1x1" # a one pixel image of the average color.
@@ -117,4 +117,41 @@ test_color YIQ sRGB && echo "ok" || echo "not ok"
test_color YUV sRGB && echo "ok" || echo "not ok"
test_color YCbCr sRGB && echo "ok" || echo "not ok"
test_color OHTA sRGB && echo "ok" || echo "not ok"
+
+# ----------------
+
+#
+# Colors given in a non-RGB colorspace
+#
+# Expected values come from LCH -> XYZ (D65) -> sRGB
+#
+test_color_to_srgb() {
+ color=`eval ${MAGICK} "$1" -colorspace sRGB "$out"`
+
+ if [ "X$color" = "X$2" ]; then
+ return 0
+ fi
+ error=true
+ return 1
+}
+
+test_color_to_srgb "xc:'lchab(70,30,0)'" \
+ 223,151,172 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:'lchab(60,50,120)'" \
+ 125,155,64 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:'lchab(60,30,270)'" \
+ 108,147,197 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:'lchab(65,30,-30)'" \
+ 192,142,185 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:'lchuv(65,20,20)'" \
+ 181,151,147 && echo "ok" || echo "not ok"
+
+test_color_to_srgb "xc:white -fill 'lchab(55,30,130)' -opaque white" \
+ 112,140,91 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:white -fill 'lchuv(55,45,130)' -opaque white" \
+ 85,145,90 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:white -fill 'lab(60,40,0)' -opaque white" \
+ 209,116,146 && echo "ok" || echo "not ok"
+test_color_to_srgb "xc:black +level-colors 'lchab(70,30,0),white'" \
+ 223,151,172 && echo "ok" || echo "not ok"
: