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"
 :