Skip to content

Need a way to construct an image with a palette/colormap. #2040

Description

@otac0n

Is your feature request related to a problem? Please describe

I am trying to remove references to System.Drawing.Bitmap in my own code and replace them with usages of MagickImage for purposes of resolving CA1416

This particular pattern is proving difficult to replicate in a performant way.

// Given:
//   int width, height;
//   Color[] palette;
//   int[] indexData (width * height)

var bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);

var p = bmp.Palette;
for (var i = 0; i < palette.Length; i++)
{
    p.Entries[i] = palette[i];
}
bmp.Palette = p;

var bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
var scan = bmpData.Scan0;
for (var y = 0; y < bmpData.Height; y++, scan += bmpData.Stride)
{
    Marshal.Copy(indexData, y * width, scan, bmpData.Width);
}

bmp.UnlockBits(bmpData);

Per #1828, it was revealed that there is no quick way to create an indexed image like this.

Describe the solution you'd like

Personally, I think the API should look like this:

//uint width, height;
//MagickColor[] palette;
//int[] indexData (width * height)

var bmp = new MagickImage(width: width, height: height, colormapSize: 1 << 8);
for (var i = 0; i < palette.Length; i++)
{
    bmp.SetColormapColor(i, palette[i]);
}
bmp.ImportPixels(indexData, new PixelImportSettings(width, height, StorageType.Int32, "I"));

See "Additional context" below for why this may not work directly. Perhaps the base library could be augmented to support an extra X map for inidices.

Describe alternatives you've considered

Alternatives:

  • Format the memory as a TGA file and load.
    • OK, but not ergonomic. May require large memory copies.
  • Load RGBA data and quantize afterwards.
    • Fails some uses cases. e.g If a palette swap mapped two entries to the same RGB value, then the indexes could not be recovered.

Additional context

If we look at the underlying library we can see the following patterns for the API:

GIF:

    q=QueueAuthenticPixels(image,0,offset,image->columns,1,exception);
    if (q == (Quantum *) NULL)
      break;
    for (x=0; x < (ssize_t) image->columns; )
    {
      c=ReadBlobLZWByte(lzw_info);
      if (c < 0)
        break;
      index=ConstrainColormapIndex(image,(ssize_t) c,exception);
      SetPixelIndex(image,(Quantum) index,q);
      SetPixelViaPixelInfo(image,image->colormap+index,q);
      SetPixelAlpha(image,index == opacity ? TransparentAlpha : OpaqueAlpha,q);
      x++;
      q+=(ptrdiff_t) GetPixelChannels(image);
    }

PCX:

      for (x=0; x < (ssize_t) image->columns; x++)
      {
        if (image->storage_class == PseudoClass)
          SetPixelIndex(image,*r++,q);
        else
          {
            SetPixelRed(image,ScaleCharToQuantum(*r++),q);
            SetPixelGreen(image,ScaleCharToQuantum(*r++),q);
            SetPixelBlue(image,ScaleCharToQuantum(*r++),q);
            if (image->alpha_trait != UndefinedPixelTrait)
              SetPixelAlpha(image,ScaleCharToQuantum(*r++),q);
          }
        q+=(ptrdiff_t) GetPixelChannels(image);
      }

PNG:

        q=GetAuthenticPixels(image,0,y,image->columns,1,exception);

        if (q == (Quantum *) NULL)
          break;
        for (x=0; x < (ssize_t) image->columns; x++)
        {
          ssize_t index=ConstrainColormapIndex(image,(ssize_t) *r,exception);
          SetPixelRed(image,ClampToQuantum(image->colormap[index].red),q);
          SetPixelGreen(image,ClampToQuantum(image->colormap[index].green),q);
          SetPixelBlue(image,ClampToQuantum(image->colormap[index].blue),q);
          SetPixelIndex(image,(Quantum) index,q);
          r++;
          q+=(ptrdiff_t) GetPixelChannels(image);
        }

TGA:

    q=QueueAuthenticPixels(image,0,y_offset,image->columns,1,exception);
    if (q == (Quantum *) NULL)
      break;
    if (flip_x != MagickFalse)
      q+=(ptrdiff_t) GetPixelChannels(image)*(image->columns-1);
    for (x=0; x < (ssize_t) image->columns; x++)
    {
      ...
      if (status == MagickFalse)
        ThrowReaderException(CorruptImageError,"UnableToReadImageData");
      if (image->storage_class == PseudoClass)
        SetPixelIndex(image,index,q);
      SetPixelRed(image,ClampToQuantum(pixel.red),q);
      SetPixelGreen(image,ClampToQuantum(pixel.green),q);
      SetPixelBlue(image,ClampToQuantum(pixel.blue),q);
      if (image->alpha_trait != UndefinedPixelTrait)
        SetPixelAlpha(image,ClampToQuantum(pixel.alpha),q);
      if (flip_x != MagickFalse)
        q-=GetPixelChannels(image);
      else
        q+=(ptrdiff_t) GetPixelChannels(image);
    }

However,
ImportImagePixels sets the colorspace to gray when using "I":

      case 'I':
      case 'i':
      {
        quantum_map[i]=IndexQuantum;
        (void) SetImageColorspace(image,GRAYColorspace,exception);
        break;
      }

While the ImportCharPixels family of functions use SetPixelGray and never SetPixelIndex.

  if (LocaleCompare(map,"I") == 0)
    {
      for (y=0; y < (ssize_t) roi->height; y++)
      {
        q=GetAuthenticPixels(image,roi->x,roi->y+y,roi->width,1,exception);
        if (q == (Quantum *) NULL)
          break;
        for (x=0; x < (ssize_t) roi->width; x++)
        {
          SetPixelGray(image,ScaleCharToQuantum(*p++),q);
          q+=(ptrdiff_t) GetPixelChannels(image);
        }
        if (SyncAuthenticPixels(image,exception) == MagickFalse)
          break;
      }
      return(y < (ssize_t) roi->height ? MagickFalse : MagickTrue);
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions