Writing Adobe Color Swatch (.aco) files using C#

Getting started

The previous article described how to read files in Adobe’s Swatch File format as used by Photoshop and other high-end photo editors. In this accompanying article, I’ll describe how to write such files. I’m not going to go over the structure again, so you haven’t already done so, please read the previous article for full details on the file structure and how to read it.

Writing big-endian values

All the data in an aco file is stored in big-endian format and therefore needs to be reversed on Windows systems before writing it back into the file.

We can use the following two methods to write a short or an int respectively into a stream as a series of bytes. Of course, if you just want functions to convert these into bytes you could use similar code, just remove the bit-shift.

private void WriteInt16(Stream stream, short value)
{
  stream.WriteByte((byte)(value >> 8));
  stream.WriteByte((byte)(value >> 0));
}
 
private void WriteInt32(Stream stream, int value)
{
  stream.WriteByte((byte)((value & 0xFF000000) >> 24));
  stream.WriteByte((byte)((value & 0x00FF0000) >> 16));
  stream.WriteByte((byte)((value & 0x0000FF00) >> 8));
  stream.WriteByte((byte)((value & 0x000000FF) >> 0));
}

As with the equivalent read functions, the >> 0 shift is unnecessary but it does clarify the code.

We also need to store color swatch names, so again we’ll make use of the Encoding.BigEndianUnicode property to convert a string into a series of bytes to write out.

private void WriteString(Stream stream, string value)
{
  stream.Write(Encoding.BigEndianUnicode.GetBytes(value), 0, value.Length * 2);
}

Writing the file

When writing the file, I’m going to follow the specification’s suggestion of writing a version 1 palette (for backwards compatibility), followed by a version 2 palette (for applications that support swatch names).

using (Stream stream = File.Create(fileName))
{
  this.WritePalette(stream, palette, FileVersion.Version1, ColorSpace.Rgb);
  this.WritePalette(stream, palette, FileVersion.Version2, ColorSpace.Rgb);
}

The core save routine follows. First, we write the version of format and then the number of colors in the palette.

private void WritePalette(Stream stream, ICollection<Color> palette, FileVersion version, ColorSpace colorSpace)
{
  int swatchIndex;
 
  this.WriteInt16(stream, (short)version);
  this.WriteInt16(stream, (short)palette.Count);
 
  swatchIndex = 0;

With that done, we loop through each color, calculate the four values that comprise the color data and then write that.

If it’s a version 2 file, we also write the swatch name. As these basic examples are just using the Color class, there’s no real flexibility in names, so we cheat – if it’s a “named” color, then we use the Color.Name property. Otherwise, we generate a Swatch name.

  foreach (Color color in palette)
  {
    short value1;
    short value2;
    short value3;
    short value4;
 
    swatchIndex++;
 
    switch (colorSpace)
    {
      // Calculate color space values here!
      default:
        throw new InvalidOperationException("Color space not supported.");
    }
 
    this.WriteInt16(stream, (short)colorSpace);
    this.WriteInt16(stream, value1);
    this.WriteInt16(stream, value2);
    this.WriteInt16(stream, value3);
    this.WriteInt16(stream, value4);
 
    if (version == FileVersion.Version2)
    {
      string name;
 
      name = color.IsNamedColor ? color.Name : string.Format("Swatch {0}", swatchIndex);
 
      this.WriteInt32(stream, name.Length);
      this.WriteString(stream, name);
    }
  }
}

Converting color spaces

As previously mentioned, the specification states that each color is comprised of four values. Even if a particular color space doesn’t use all four (for example Grayscale just uses one), you still need to write the other values, typically as zero’s.

Although it’s a slight duplication, I’ll include the description table for color spaces to allow easy reference of the value types.

Id Description
0 RGB. The first three values in the color data are red, green, and blue. They are full unsigned 16-bit values as in Apple’s RGBColordata structure. Pure red = 65535, 0, 0.
7 Lab. The first three values in the color data are lightness, a chrominance, and b chrominance. Lightness is a 16-bit value from 0…10000. Chrominance components are each 16-bit values from -12800…12700. Gray values are represented by chrominance components of 0. Pure white = 10000,0,0.
1 HSB. The first three values in the color data are hue, saturation, and brightness. They are full unsigned 16-bit values as in Apple’s HSVColordata structure. Pure red = 0,65535, 65535.
8 Grayscale. The first value in the color data is the gray value, from 0…10000.
2 CMYK. The four values in the color data are cyan, magenta, yellow, and black. They are full unsigned 16-bit values. For example, pure cyan = 0,65535,65535,65535.

While supporting CMYK colors are beyond the scope of this article as they require color profiles, we can easily support RGB, HSL and Grayscale spaces.

RGB is the simplest as .NET colors are already in this format. The only thing we have to do is divide each channel by 255 as the specification uses the range 0-65535 rather than the typical 0-255.

Notice value4 is simply initialized to zero as this space only needs 3 of the 4 values.

case ColorSpace.Rgb:
  value1 = (short)(color.R * 256);
  value2 = (short)(color.G * 256);
  value3 = (short)(color.B * 256);
  value4 = 0;
  break;

We can also support HSL without too much trouble as the Color class already includes methods for extracting these values. Again, we need to do a little fiddling to change the number into the range used by the specification.

case ColorSpace.Hsb:
  value1 = (short)(color.GetHue() * 182.04);
  value2 = (short)(color.GetSaturation() * 655.35);
  value3 = (short)(color.GetBrightness() * 655.35);
  value4 = 0;
  break;

The last format we can easily support is grayscale. If the source color is already gray (i.e. the red, green and blue channels are all the same value), then we use that, otherwise we’ll average the 3 channels and use that as the value.

case ColorSpace.Grayscale:
  if (color.R == color.G &amp;&amp; color.R == color.B)
  {
    // already grayscale
    value1 = (short)(color.R * 39.0625);
  }
  else
  {
    // color is not grayscale, convert
    value1 = (short)(((color.R + color.G + color.B) / 3.0) * 39.0625);
  }
  value2 = 0;
  value3 = 0;
  value4 = 0;
  break;

Demo Application

The sample generates a random 255 color palette, then writes this to a temporary file using the specified color space. It then reads it back in, and displays both palettes side by side for comparison.

Downloads

PhotoshopColorSwatchWriter.zip 21 July 2014 78.7 KB

Leave a Reply

Your email address will not be published.