Reading Adobe Swatch Exchange (ASE) files using C#

Previously I wrote articles on how to read and writes files using the Photoshop Color Swatch file format. In this article, I’m now going to take a long overdue look at Adobe’s  Swatch Exchange file format and show how to read these files using C#.

An example of an ASE file with a single group containing 5 RGB colors

An example of an ASE file with a single group containing 5 RGB colors


Unlike some of Adobe’s other specification, they don’t seem to have published an official specification for the ase format. For the purposes of this article, I’ve had to manually poke around in sample files using a HEX editor to undercover their structure.

And, as with my previous articles, the code doesn’t handle CMYK or Lab color spaces.


Structure of a Adobe Swatch Exchange file

ase files support the notion of groups, so you can have multiple groups containing colors. Judging by the files I have tested, you can also have a bunch of colors without a group at all. I’m uncertain if groups can be nested, so I have assumed they cannot be.

With that said, the structure is relatively straight forward, and helpfully includes data that means I can skip the bits that I have no idea at all what they are. The format is comprised of a basic version header, then a number of blocks. Each block includes a type, data length, the block name, and then additional data specific to the block type, and optional custom data specific to that particular block.

Blocks can either be a color, the start of a group, or the end of a group.

Color blocks include the color space, 1-4 floating point values that describe the color (3 for RGB and LAB, 4 for CMYK and 1 for Grayscale), and a type.

Finally, all blocks can carry custom data. I have no idea what this data is, but it doesn’t seem to be essential nor are you required to know what it is for in order to pull out the color information. Fortunately, as you know how large each block is, you can skip the remaining bytes from the block and move onto the next one. As there seems to be little difference between the purposes of aco and asefiles (the obvious one being that the former is just a list of colors while the latter supports grouping). I assume this data is metadata from the application that created the file, but this is just a conjecture.

The following table attempts to describe the layout.

Length Description
4 Signature
2 Major Version
2 Minor Version
4 Number of blocks
variable Block data
Length Description
2 Type
4 Block length
2 Name length
(name length) Name
Colour blocks only
Length Description
4 Colour space
12 (RGB, LAB), 16 (CMYK), 4 (Grayscale) Colour data. Every four bytes represents one floating point value
2 Colour type
All blocks
Length Description
variable (Block length – previously read data) Unknown

As with aco files, all the data in an ase file is stored in big-endian format and therefore needs to be reversed on Windows systems. Unlike the aco files where four values are present for each color even if not required by the appropriate color space, the ase format uses between one and four values, make it slightly more compact than aco.


Color Spaces

I mentioned above that each color has a description of what color space it belongs to. There appear to be four supported color spaces. Note that space names are 4 characters long in an ase file, shorter names are therefore padded with spaces.

  • RGB
  • LAB
  • CMYK
  • Gray

In my experiments, RGB was easy enough – just multiply the value read from the file by 255 to get the right value to use with .NET’s Color structure. I don’t know the other 3 types however – I need more samples!


Big-endian conversion

I covered the basics of reading shorts, ints, and strings in big-endian in my previous article on aco files so I won’t cover that here.

However, this time around I do need to read floats from the files too. While the BitConverter class has a ToSingle method that will convert a 4-byte array to a float, of course it is for little-endian.

I looked at the reference source for this method and saw it does a neat little trick – it converts the four bytes to an integer, then creates a float from that integer via pointers.

So, I used the same approach – read an int in big-endian, then convert it to a float. The only caveat is that you are using pointers, meaning unsafe code. By default you can’t use the unsafe keyword without enabling a special option in project properties. I use unsafe code quite frequently when working with image data and generally don’t have a problem, if you are unwilling to enable this option then you can always take the four bytes, reverse the, and then call BitConverter.ToSingle with the reversed array.

public static float ReadSingleBigEndian(this Stream stream)
    int value;

    value = stream.ReadUInt32BigEndian();

    return *(float*)&value;

Another slight difference between aco and ase files is that in ase files, strings are null terminated, and the name length includes that terminator. Of course, when reading the strings back out, we really don’t want that terminator to be included. So I added another helper method to deal with that.

public static string ReadStringBigEndian(this Stream stream)
  int length;
  string value;

  // string is null terminated, value saved in file includes the terminator

  length = stream.ReadUInt16BigEndian() - 1;
  value = stream.ReadStringBigEndian(length);
  stream.ReadUInt16BigEndian(); // read and discard the terminator

  return value;


Storage classes

In my previous examples on reading color data from files, I’ve kept it simple and returned arrays of colors, discarding incidental details such as names. This time, I’ve created a small set of helper classes. to preserve this information and to make it easier to serialize it.

internal abstract class Block
  public byte[] ExtraData { get; set; }
  public string Name { get; set; }

internal class ColorEntry : Block
  public int B { get; set; }
  public int G { get; set; }
  public int R { get; set; }
  public ColorType Type { get; set; }

  public Color ToColor()
    return Color.FromArgb(this.R, this.G, this.B);

internal class ColorEntryCollection : Collection<ColorEntry>
{ }

internal class ColorGroup : Block, IEnumerable<ColorEntry>
  public ColorGroup()
    this.Colors = new ColorEntryCollection();

  public ColorEntryCollection Colors { get; set; }

  public IEnumerator<ColorEntry> GetEnumerator()
    return this.Colors.GetEnumerator();

  IEnumerator IEnumerable.GetEnumerator()
    return this.GetEnumerator();

internal class ColorGroupCollection : Collection<ColorGroup>
{ }

internal class SwatchExchangeData
  public SwatchExchangeData()
    this.Groups = new ColorGroupCollection();
    this.Colors = new ColorEntryCollection();

  public ColorEntryCollection Colors { get; set; }
  public ColorGroupCollection Groups { get; set; }

That should be all we need, time to load some files!


Reading the file

To start with, we create a new ColorEntryCollection that will be used for global colors (i.e. color blocks that don’t appear within a group). To make things simple, I’m also creating a Stack to which I push this global collection. Later on, when I encounter a start group block, I’ll Push a new ColorEntryCollection to this stack, and when I encounter an end group block, I’ll Pop the value at the top of the stack. This way, when I encounter a color block, I can easily add it to the right collection without needing to explicitly keep track of the active group or lack thereof.

public void Load(string fileName)
  Stack<ColorEntryCollection> colors;
  ColorGroupCollection groups;
  ColorEntryCollection globalColors;

  groups = new ColorGroupCollection();
  globalColors = new ColorEntryCollection();
  colors = new Stack<ColorEntryCollection>();

  // add the global collection to the bottom of the stack to handle color blocks outside of a group

  using (Stream stream = File.OpenRead(fileName))
    int blockCount;


    blockCount = stream.ReadUInt32BigEndian();

    for (int i = 0; i &lt; blockCount; i++)
      this.ReadBlock(stream, groups, colors);

  this.Groups = groups;
  this.Colors = globalColors;

After opening a Stream containing our file data, we need to check that the stream contains both ase data, and that the data is a version we can read. This is done by reading 8 bytes from the start of the data. The first four are ASCII characters which should match the string ASEFASEF, the next two are the major version and the final two the minor version.

private void ReadAndValidateVersion(Stream stream)
  string signature;
  int majorVersion;
  int minorVersion;

  // get the signature (4 ascii characters)
  signature = stream.ReadAsciiString(4);

  if (signature != "ASEF")
    throw new InvalidDataException("Invalid file format.");

  // read the version
  majorVersion = stream.ReadUInt16BigEndian();
  minorVersion = stream.ReadUInt16BigEndian();

  if (majorVersion != 1 && minorVersion != 0)
    throw new InvalidDataException("Invalid version information.");

Assuming the data is valid, we read the number of blocks in the file, and enter a loop to process each block. For each block, first we read the type of the block, and then the length of the block’s data.

How we continue reading from the stream depends on the block type (more on that later), after which we work out how much data is left in the block, read it, and store it as raw bytes on the off-chance the consuming application can do something with it, or for saving back into the file.

This technique assumes that the source stream is seekable. If this is not the case, you’ll need to manually keep track of how many bytes you have read from the block to calculate the remaining custom data left to read.

private void ReadBlock(Stream stream, ColorGroupCollection groups, Stack<ColorEntryCollection> colorStack)
  BlockType blockType;
  int blockLength;
  int offset;
  int dataLength;
  Block block;

  blockType = (BlockType)stream.ReadUInt16BigEndian();
  blockLength = stream.ReadUInt32BigEndian();

  // store the current position of the stream, so we can calculate the offset
  // from bytes read to the block length in order to skip the bits we can't use
  offset = (int)stream.Position;

  // process the actual block
  switch (blockType)
    case BlockType.Color:
      block = this.ReadColorBlock(stream, colorStack);
    case BlockType.GroupStart:
      block = this.ReadGroupBlock(stream, groups, colorStack);
    case BlockType.GroupEnd:
      block = null;
      throw new InvalidDataException($"Unsupported block type '{blockType}'.");

  // load in any custom data and attach it to the
  // current block (if available) as raw byte data
  dataLength = blockLength - (int)(stream.Position - offset);

  if (dataLength > 0)
    byte[] extraData;

    extraData = new byte[dataLength];
    stream.Read(extraData, 0, dataLength);

    if (block != null)
      block.ExtraData = extraData;

Processing groups

If we have found a “start group” block, then we create a new ColorGroup object and read the group name. We also push the group’s ColorEntryCollection to the stack I mentioned earlier.

private Block ReadGroupBlock(Stream stream, ColorGroupCollection groups, Stack<ColorEntryCollection> colorStack)
  ColorGroup block;
  string name;

  // read the name of the group
  name = stream.ReadStringBigEndian();

  // create the group and add it to the results set
  block = new ColorGroup
    Name = name


  // add the group color collection to the stack, so when subsequent colour blocks
  // are read, they will be added to the correct collection

  return block;

For “end group” blocks, we don’t do any custom processing as I do not think there is any data associated with these. Instead, we just pop the last value from our color stack. (Of course, that means if there is a malformed ase file containing a group end without a group start, this procedure is going to crash sooner or later!)

Processing colors

When we hit a color block, we read the color’s name and the color mode.

Then, depending on the mode, we read between 1 and 4 float values which describe the color. As anything other than RGB processing is beyond the scope of this article, I’m throwing an exception for the LAB, CMYK and Gray color spaces.

For RGB colors, I take each value and multiply it by 255 to get a value suitable for use with .NET’s Color struct.

After reading the color data, there’s one official value left to read, which is the color type. This can either be Global (0), Spot (1) or Normal (2).

Finally, I construct a new ColorEntry obect containing the color information and add it to whatever ColorEntryCollection is on top of the stack.

private Block ReadColorBlock(Stream stream, Stack<ColorEntryCollection> colorStack)
  ColorEntry block;
  string colorMode;
  int r;
  int g;
  int b;
  ColorType colorType;
  string name;
  ColorEntryCollection colors;

  // get the name of the color
  // this is stored as a null terminated string
  // with the length of the byte data stored before
  // the string data in a 16bit int
  name = stream.ReadStringBigEndian();

  // get the mode of the color, which is stored
  // as four ASCII characters
  colorMode = stream.ReadAsciiString(4);

  // read the color data
  // how much data we need to read depends on the
  // color mode we previously read
  switch (colorMode)
    case "RGB ":
      // RGB is comprised of three floating point values ranging from 0-1.0
      float value1;
      float value2;
      float value3;
      value1 = stream.ReadSingleBigEndian();
      value2 = stream.ReadSingleBigEndian();
      value3 = stream.ReadSingleBigEndian();
      r = Convert.ToInt32(value1 * 255);
      g = Convert.ToInt32(value2 * 255);
      b = Convert.ToInt32(value3 * 255);
    case "CMYK":
      // CMYK is comprised of four floating point values
      throw new InvalidDataException($"Unsupported color mode '{colorMode}'.");
    case "LAB ":
      // LAB is comprised of three floating point values
      throw new InvalidDataException($"Unsupported color mode '{colorMode}'.");
    case "Gray":
      // Grayscale is comprised of a single floating point value
      throw new InvalidDataException($"Unsupported color mode '{colorMode}'.");
      throw new InvalidDataException($"Unsupported color mode '{colorMode}'.");

  // the final "official" piece of data is a color type
  colorType = (ColorType)stream.ReadUInt16BigEndian();

  block = new ColorEntry
    R = r,
    G = g,
    B = b,
    Name = name,
    Type = colorType

  colors = colorStack.Peek();

  return block;




An example of a group-less ASE file

An example of a group-less ASE file

The ase format is pretty simple to process, although the fact there is still data in these files with an unknown purpose could be a potential issue.

However, I have tested this code on a number of files downloaded from the internet and have been able to pull out all the color information, so I suspect the Swatch Buckler library will be getting ase support fairly soon!


Download 10/16/2015 152 Kb

Leave a Reply

Your email address will not be published. Required fields are marked *