Commit 7b3c0db4b for imagemagick.org

commit 7b3c0db4b1b02277b379929558409a271c3f8e53
Author: Cristy <urban-warrior@imagemagick.org>
Date:   Tue Jun 16 19:11:04 2026 -0400

    https://github.com/ImageMagick/ImageMagick/issues/8802

diff --git a/coders/sixel.c b/coders/sixel.c
index 2b7514137..90845b7b2 100644
--- a/coders/sixel.c
+++ b/coders/sixel.c
@@ -45,6 +45,7 @@
 #include "MagickCore/blob.h"
 #include "MagickCore/blob-private.h"
 #include "MagickCore/cache.h"
+#include "MagickCore/channel.h"
 #include "MagickCore/color.h"
 #include "MagickCore/color-private.h"
 #include "MagickCore/colormap.h"
@@ -268,7 +269,8 @@ static MagickBooleanType sixel_decode(Image *image,unsigned char *p,
     r,
     repeat_count,
     sixel_palet[SIXEL_PALETTE_MAX],
-    sixel_vertical_mask;
+    sixel_vertical_mask,
+    transparent_index; /* NEW: track transparent palette index */

   sixel_pixel_t
     *dmbuf,
@@ -297,6 +299,7 @@ static MagickBooleanType sixel_decode(Image *image,unsigned char *p,
   color_index=0;
   background_color_index=0;
   max_color_index=2;
+  transparent_index=-1; /* NEW: initialize as "no transparency" */
   memset(param,0,sizeof(param));
   imsx=2048;
   imsy=2048;
@@ -469,15 +472,30 @@ static MagickBooleanType sixel_decode(Image *image,unsigned char *p,
               }
             else if (param[1] == 2)
               {
-                /* RGB */
-                if (param[2] > 100)
-                  param[2]=100;
-                if (param[3] > 100)
-                  param[3]=100;
-                if (param[4] > 100)
-                  param[4]=100;
-                sixel_palet[color_index]=SIXEL_XRGB(param[2],param[3],
-                  param[4]);
+                /*
+                  RGB or transparency.
+
+                  libsixel-style transparency is encoded as:
+                    #N;2;0;0;0
+                  Treat this as "palette index N is fully transparent".
+                */
+                if (param[2] == 0 && param[3] == 0 && param[4] == 0)
+                  {
+                    transparent_index=color_index;
+                    /* leave sixel_palet[color_index] as-is; alpha will be 0 */
+                  }
+                else
+                  {
+                    /* RGB */
+                    if (param[2] > 100)
+                      param[2]=100;
+                    if (param[3] > 100)
+                      param[3]=100;
+                    if (param[4] > 100)
+                      param[4]=100;
+                    sixel_palet[color_index]=SIXEL_XRGB(param[2],param[3],
+                      param[4]);
+                  }
               }
           }
       }
@@ -651,7 +669,9 @@ static MagickBooleanType sixel_decode(Image *image,unsigned char *p,
     (*palette)[n*4+0]=sixel_palet[n] >> 16 & 0xff;
     (*palette)[n*4+1]=sixel_palet[n] >> 8 & 0xff;
     (*palette)[n*4+2]=sixel_palet[n] & 0xff;
-    (*palette)[n*4+3]=0xff;
+    (*palette)[n*4+3]=0xff; /* default: opaque */
+    if (n == transparent_index)
+      (*palette)[n*4+3]=0x00; /* NEW: transparent entry */
   }
   return(MagickTrue);
 }
@@ -764,8 +784,8 @@ static int sixel_put_node(sixel_output_t *const context,int x,sixel_node_t *np,
       /* designate palette index */
       if (context->active_palette != np->color)
         {
-          nwrite=(int) FormatLocaleString((char *) context->buffer+context->pos,
-            sizeof(context->buffer),"#%d",np->color);
+          nwrite=(int) FormatLocaleString((char *) context->buffer+
+            context->pos,sizeof(context->buffer),"#%d",np->color);
           sixel_advance(context,nwrite);
           context->active_palette=np->color;
         }
@@ -1052,44 +1072,38 @@ static MagickBooleanType IsSIXEL(const unsigned char *magick,
 static Image *ReadSIXELImage(const ImageInfo *image_info,
   ExceptionInfo *exception)
 {
-  char
-    *p,
-    *sixel_buffer;
-
   Image
     *image;

   MagickBooleanType
+    has_alpha = MagickFalse,
     status;

-  Quantum
-    *q;
-
   size_t
-    length;
+    height,
+    length,
+    ncolors,
+    width;

   sixel_pixel_t
-    *sixel_pixels;
+    *pixels = NULL;

   ssize_t
     i,
-    j,
+    x,
     y;

   unsigned char
-    *sixel_palette;
+    *p,
+    *palette = NULL,
+    *sixel_data = NULL;

   /*
-    Open image file.
+    Allocate image structure.
   */
-  assert(image_info != (const ImageInfo *) NULL);
-  assert(image_info->signature == MagickCoreSignature);
-  assert(exception != (ExceptionInfo *) NULL);
-  assert(exception->signature == MagickCoreSignature);
-  if (IsEventLogging() != MagickFalse)
-    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",
-      image_info->filename);
-  image=AcquireImage(image_info,exception);
+  image = AcquireImage(image_info,exception);
+  if (image == (Image *) NULL)
+    return((Image *) NULL);
   status=OpenBlob(image_info,image,ReadBinaryBlobMode,exception);
   if (status == MagickFalse)
     {
@@ -1100,113 +1114,116 @@ static Image *ReadSIXELImage(const ImageInfo *image_info,
     Read SIXEL file.
   */
   length=MagickPathExtent;
-  sixel_buffer=(char *) AcquireQuantumMemory((size_t) length+MagickPathExtent,
-    sizeof(*sixel_buffer));
-  p=sixel_buffer;
-  if (sixel_buffer != (char *) NULL)
-    while (ReadBlobString(image,p) != (char *) NULL)
+  sixel_data=(unsigned char *) AcquireQuantumMemory((size_t) length+
+    MagickPathExtent,sizeof(*sixel_data));
+  p=sixel_data;
+  if (sixel_data != (unsigned char *) NULL)
+    while (ReadBlobString(image,(char *) p) != (char *) NULL)
     {
       ssize_t
        offset;

-      if ((*p == '#') && ((p == sixel_buffer) || (*(p-1) == '\n')))
+      if ((*p == '#') && ((p == sixel_data) || (*(p-1) == '\n')))
         continue;
       if ((*p == '}') && (*(p+1) == ';'))
         break;
-      p+=(ptrdiff_t) strlen(p);
-      offset=p-sixel_buffer;
+      p+=(ptrdiff_t) strlen((char *) p);
+      offset=p-sixel_data;
       if ((size_t) (offset+MagickPathExtent+1) < length)
         continue;
       length<<=1;
-      sixel_buffer=(char *) ResizeQuantumMemory(sixel_buffer,length+
-        MagickPathExtent+1,sizeof(*sixel_buffer));
-      if (sixel_buffer == (char *) NULL)
+      sixel_data=(unsigned char *) ResizeQuantumMemory(sixel_data,length+
+        MagickPathExtent+1,sizeof(*sixel_data));
+      if (sixel_data == (unsigned char *) NULL)
         break;
-      p=sixel_buffer+offset;
+      p=sixel_data+offset;
     }
-  if (sixel_buffer == (char *) NULL)
+  if (sixel_data == (unsigned char *) NULL)
     ThrowReaderException(ResourceLimitError,"MemoryAllocationFailed");
-  sixel_buffer[length]='\0';
+  sixel_data[length]='\0';
   /*
-    Decode SIXEL.
+    Decode SIXEL into:
+      - pixels[] (indexed)
+      - palette[] (RGBA)
+      - width, height
+      - ncolors
   */
-  sixel_pixels=(sixel_pixel_t *) NULL;
-  status=sixel_decode(image,(unsigned char *) sixel_buffer,&sixel_pixels,
-    &image->columns,&image->rows,&sixel_palette,&image->colors,
-    exception);
+  status=sixel_decode(image,sixel_data,&pixels,&width,&height,&palette,
+    &ncolors,exception);
   if (status == MagickFalse)
-    {
-      sixel_buffer=(char *) RelinquishMagickMemory(sixel_buffer);
-      if (sixel_pixels != (sixel_pixel_t *) NULL)
-        sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
-      ThrowReaderException(CorruptImageError,"CorruptImage");
-    }
-  sixel_buffer=(char *) RelinquishMagickMemory(sixel_buffer);
-  image->depth=24;
-  image->storage_class=PseudoClass;
-  status=SetImageExtent(image,image->columns,image->rows,exception);
-  if (status == MagickFalse)
-    {
-      sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
-      sixel_palette=(unsigned char *) RelinquishMagickMemory(sixel_palette);
-      return(DestroyImageList(image));
-    }
-  if (AcquireImageColormap(image,image->colors, exception) == MagickFalse)
-    {
-      sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
-      sixel_palette=(unsigned char *) RelinquishMagickMemory(sixel_palette);
-      ThrowReaderException(ResourceLimitError,"MemoryAllocationFailed");
-    }
-  for (i = 0; i < (ssize_t) image->colors; ++i)
+    goto cleanup;
+  /*
+    Prepare image geometry.
+  */
+  if (SetImageExtent(image,width,height,exception) == MagickFalse)
+    goto cleanup;
+  /*
+    Allocate colormap.
+  */
+  image->colors = ncolors;
+  if (AcquireImageColormap(image,image->colors,exception) == MagickFalse)
+    goto cleanup;
+  /*
+    Load palette into image->colormap and detect transparency.
+  */
+  for (i = 0; i < (ssize_t) image->colors; i++)
   {
-    image->colormap[i].red=ScaleCharToQuantum(sixel_palette[i * 4 + 0]);
-    image->colormap[i].green=ScaleCharToQuantum(sixel_palette[i * 4 + 1]);
-    image->colormap[i].blue=ScaleCharToQuantum(sixel_palette[i * 4 + 2]);
+    unsigned char r = palette[i*4+0];
+    unsigned char g = palette[i*4+1];
+    unsigned char b = palette[i*4+2];
+    unsigned char a = palette[i*4+3];
+
+    image->colormap[i].red   = ScaleCharToQuantum(r);
+    image->colormap[i].green = ScaleCharToQuantum(g);
+    image->colormap[i].blue  = ScaleCharToQuantum(b);
+    /* alpha stored as Quantum in ImageMagick */
+    image->colormap[i].alpha = (double) ScaleCharToQuantum(a);
+    if (a != 0xff)
+      has_alpha = MagickTrue;
   }
-  j=0;
-  if (image_info->ping == MagickFalse)
+  if (has_alpha != MagickFalse)
     {
       /*
-        Read image pixels.
+        If any palette entry is transparent, enable alpha channel.
       */
-      for (y=0; y < (ssize_t) image->rows; y++)
-      {
-        ssize_t
-          x;
-
-        q=QueueAuthenticPixels(image,0,y,image->columns,1,exception);
-        if (q == (Quantum *) NULL)
-          break;
-        for (x=0; x < (ssize_t) image->columns; x++)
-        {
-          j=(ssize_t) sixel_pixels[y*(ssize_t) image->columns+x];
-          j=ConstrainColormapIndex(image,j,exception);
-          SetPixelIndex(image,(Quantum) j,q);
-          SetPixelRed(image,(Quantum) image->colormap[j].red,q);
-          SetPixelGreen(image,(Quantum) image->colormap[j].green,q);
-          SetPixelBlue(image,(Quantum) image->colormap[j].blue,q);
-          q+=(ptrdiff_t) GetPixelChannels(image);
-        }
-        if (SyncAuthenticPixels(image,exception) == MagickFalse)
-          break;
-      }
-      if (y < (ssize_t) image->rows)
-        {
-          sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
-          sixel_palette=(unsigned char *) RelinquishMagickMemory(sixel_palette);
-          ThrowReaderException(CorruptImageError,"NotEnoughPixelData");
-        }
+      image->alpha_trait = BlendPixelTrait;
+      (void) SetImageAlphaChannel(image,OnAlphaChannel,exception);
     }
   /*
-    Relinquish resources.
+    Transfer indexed pixels into the image.
   */
-  sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
-  sixel_palette=(unsigned char *) RelinquishMagickMemory(sixel_palette);
-  if (CloseBlob(image) == MagickFalse)
-    status=MagickFalse;
-  if (status == MagickFalse)
-    return(DestroyImageList(image));
-  return(GetFirstImageInList(image));
+  for (y = 0; y < (ssize_t) image->rows; y++)
+  {
+    Quantum *q = GetAuthenticPixels(image,0,y,image->columns,1,exception);
+    if (q == (Quantum *) NULL)
+      break;
+    for (x = 0; x < (ssize_t) image->columns; x++)
+    {
+      Quantum index = (Quantum) pixels[y*(ssize_t) image->columns + x];
+      SetPixelIndex(image,index,q);
+      if (has_alpha != MagickFalse)
+        {
+          /*
+            If this palette entry is transparent, set pixel alpha=0.
+          */
+          Quantum alpha_q = (Quantum) image->colormap[(int) index].alpha;
+          SetPixelAlpha(image,alpha_q,q);
+        }
+      q+=GetPixelChannels(image);
+    }
+    if (SyncAuthenticPixels(image,exception) == MagickFalse)
+      break;
+  }
+
+cleanup:
+  if (pixels != NULL)
+    pixels = (sixel_pixel_t *) RelinquishMagickMemory(pixels);
+  if (palette != NULL)
+    palette = (unsigned char *) RelinquishMagickMemory(palette);
+  if (sixel_data != NULL)
+    sixel_data = (unsigned char *) RelinquishMagickMemory(sixel_data);
+
+  return(image);
 }

 /*
@@ -1313,23 +1330,21 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
   const Quantum
     *q;

-  ssize_t
-    i,
-    x;
+  sixel_output_t
+    *output;
+
+  sixel_pixel_t
+    *sixel_pixels;

   ssize_t
+    i,
+    x,
     opacity,
     y;

-  sixel_output_t
-    *output;
-
   unsigned char
     sixel_palette[SIXEL_PALETTE_MAX*3];

-  sixel_pixel_t
-    *sixel_pixels;
-
   /*
     Open output image file.
   */
@@ -1342,12 +1357,22 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
   status=OpenBlob(image_info,image,WriteBinaryBlobMode,exception);
   if (status == MagickFalse)
     return(status);
+  /*
+    Ensure sRGB colorspace.
+  */
   if (IssRGBCompatibleColorspace(image->colorspace) == MagickFalse)
     (void) TransformImageColorspace(image,sRGBColorspace,exception);
+  /*
+    Prepare palette and identify transparent index (if any).
+  */
   opacity=(-1);
   if ((image->alpha_trait & BlendPixelTrait) == 0)
     {
-      if ((image->storage_class == DirectClass) || (image->colors > SIXEL_PALETTE_MAX))
+      /*
+        No alpha: just ensure we have a palette.
+      */
+      if ((image->storage_class == DirectClass) ||
+          (image->colors > SIXEL_PALETTE_MAX))
         (void) SetImageType(image,PaletteType,exception);
     }
   else
@@ -1357,7 +1382,8 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
         beta;

       /*
-        Identify transparent colormap index.
+        We have alpha: convert to palette with bilevel alpha and
+        choose the most transparent colormap entry as "keycolor".
       */
       if ((image->storage_class == DirectClass) ||
           (image->colors > SIXEL_PALETTE_MAX))
@@ -1375,9 +1401,12 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
             if (alpha < beta)
               opacity=i;
           }
+      /*
+        If we didn't find one on the first pass, try again after
+        PaletteBilevelAlphaType conversion.
+      */
       if (opacity == -1)
         {
-          (void) SetImageType(image,PaletteBilevelAlphaType,exception);
           for (i=0; i < (ssize_t) image->colors; i++)
             if (image->colormap[i].alpha != (double) OpaqueAlpha)
               {
@@ -1392,22 +1421,26 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
                   opacity=i;
               }
         }
-      if (opacity >= 0)
-        {
-          image->colormap[opacity].red=image->transparent_color.red;
-          image->colormap[opacity].green=image->transparent_color.green;
-          image->colormap[opacity].blue=image->transparent_color.blue;
-        }
+      /*
+        NOTE: we do NOT overwrite the transparent colormap entry
+        with image->transparent_color here. The SIXEL encoder will
+        treat 'opacity' as a keycolor and skip drawing pixels with
+        that index, effectively making them transparent.
+      */
     }
+  /*
+    SIXEL palette size limit.
+  */
   if (image->colors > SIXEL_PALETTE_MAX)
     return(MagickFalse);
   /*
-    SIXEL header.
+    Build SIXEL palette (RGB only, no alpha).
   */
   for (i=0; i < (ssize_t) image->colors; i++)
   {
     sixel_palette[3*i+0]=ScaleQuantumToChar((Quantum) image->colormap[i].red);
-    sixel_palette[3*i+1]=ScaleQuantumToChar((Quantum) image->colormap[i].green);
+    sixel_palette[3*i+1]=ScaleQuantumToChar((Quantum)
+      image->colormap[i].green);
     sixel_palette[3*i+2]=ScaleQuantumToChar((Quantum) image->colormap[i].blue);
   }
   /*
@@ -1423,6 +1456,10 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
       output=(sixel_output_t *) RelinquishMagickMemory(output);
       ThrowWriterException(ResourceLimitError,"MemoryAllocationFailed");
     }
+  /*
+    Map image pixels to palette indices, using 'opacity' as keycolor
+    for transparent pixels when alpha is present.
+  */
   for (y=0; y < (ssize_t) image->rows; y++)
   {
     q=GetVirtualPixels(image,0,y,image->columns,1,exception);
@@ -1430,13 +1467,33 @@ static MagickBooleanType WriteSIXELImage(const ImageInfo *image_info,
       break;
     for (x=0; x < (ssize_t) image->columns; x++)
     {
-      sixel_pixels[y*(ssize_t) image->columns+x]=((sixel_pixel_t)
-        GetPixelIndex(image,q));
+      ssize_t
+        index;
+
+      if ((image->alpha_trait & BlendPixelTrait) != 0 && opacity >= 0)
+        {
+          /*
+            If this pixel is transparent (or mostly transparent), assign the
+            keycolor index so sixel_encode_impl() can skip drawing it.
+          */
+          Quantum alpha_q = GetPixelAlpha(image,q);
+          if (alpha_q < (Quantum) (QuantumRange/2))
+            index=opacity;
+          else
+            index=(ssize_t) GetPixelIndex(image,q);
+        }
+      else
+        index=(ssize_t) GetPixelIndex(image,q); /* No alpha. */
+      sixel_pixels[y*(ssize_t) image->columns+x]=(sixel_pixel_t) index;
       q+=(ptrdiff_t) GetPixelChannels(image);
     }
   }
+  /*
+    Encode SIXEL, passing 'opacity' as keycolor if we have one.
+    keycolor == -1 means "no transparency".
+  */
   status=sixel_encode_impl(sixel_pixels,image->columns,image->rows,
-    sixel_palette,image->colors,-1,output);
+    sixel_palette,image->colors,(int) opacity,output);
   sixel_pixels=(sixel_pixel_t *) RelinquishMagickMemory(sixel_pixels);
   output=(sixel_output_t *) RelinquishMagickMemory(output);
   if (CloseBlob(image) == MagickFalse)