Writing Adobe Swatch Exchange (ase) file using C#

In my last post, I described how to read Adobe Swatch Exchange files using C#. Now I’m going to update that sample progtram to save ase files as well as load them.

An example of a multi-group ASE file created by the sample application

An example of a multi-group ASE file created by the sample application

Writing big endian values

I covered the basics of writing big-endian values in my original post on writing Photoshop aco files, so I’ll not cover that again but only mention the new pieces.

Firstly, we now need to store float values. I mentioned the trick that BitConverter.ToSIngle does where it converts a int to a pointer, and then the pointer to a float. I’m going to do exactly the reverse in order to write the float to a stream – convert the float to a pointer, then convert it to an int, then write the bytes of the int.

public static void WriteBigEndian(this Stream stream, float value)
{
  unsafe
  {
    stream.WriteBigEndian(*(int*)&value);
  }
}

We also need to store unsigned 2-byte integers, so we have another extension for that.

public static void WriteBigEndian(this Stream stream, ushort value)
{
  stream.WriteByte((byte)(value >> 8));
  stream.WriteByte((byte)(value >> 0));
}

Finally, let’s not forget our length prefixed strings!

public static void WriteBigEndian(this Stream stream, string value)
{
  byte[] data;

  data = Encoding.BigEndianUnicode.GetBytes(value);

  stream.WriteBigEndian(value.Length);
  stream.Write(data, 0, data.Length);
}

Saving the file

I covered the format of an ase file in the previous post, so I won’t cover that again either. In summary, you have a version header, a block count, then a number of blocks – of which a block can either be a group (start or end) or a color.

Saving the version header is rudimentry

private void WriteVersionHeader(Stream stream)
{
  stream.Write("ASEF");
  stream.WriteBigEndian((ushort)1);
  stream.WriteBigEndian((ushort)0);
}

After this, we write the number of blocks, then cycle each group and color in our document.

private void WriteBlocks(Stream stream)
{
  int blockCount;

  blockCount = (this.Groups.Count * 2) + this.Colors.Count + this.Groups.Sum(group => group.Colors.Count);

  stream.WriteBigEndian(blockCount);

  // write the global colors first
  // not sure if global colors + groups is a supported combination however
  foreach (ColorEntry color in this.Colors)
  {
    this.WriteBlock(stream, color);
  }

  // now write the groups
  foreach (ColorGroup group in this.Groups)
  {
    this.WriteBlock(stream, group);
  }
}

Writing a block is slightly complicated as you need to know – up front – the final size of all of the data belonging to that block. Originally I wrote the block to a temporary MemoryStream, then copied the length and the data into a real stream but that isn’t a very efficient approach, so now I just calculate the block size.

Writing Groups

If you recall from the previous article, a group is comprised of a least two blocks – one that starts the group (and includes the name), and one that finishes the group. There can also be any number of color blocks in between. Potentially you can have nested groups, but I haven’t coded for this yet – I need to experiment with ase files some more, at which point I’ll update these samples if need be.

private int GetBlockLength(Block block)
{
  int blockLength;

  // name data (2 bytes per character + null terminator, plus 2 bytes to describe that first number )
  blockLength = 2 + (((block.Name ?? string.Empty).Length + 1) * 2);

  if (block.ExtraData != null)
  {
    blockLength += block.ExtraData.Length; // data we can't process but keep anyway
  }

  return blockLength;
}

private void WriteBlock(Stream stream, ColorGroup block)
{
  int blockLength;

  blockLength = this.GetBlockLength(block);

  // write the start group block
  stream.WriteBigEndian((ushort)BlockType.GroupStart);
  stream.WriteBigEndian(blockLength);
  this.WriteNullTerminatedString(stream, block.Name);
  this.WriteExtraData(stream, block.ExtraData);

  // write the colors in the group
  foreach (ColorEntry color in block.Colors)
  {
    this.WriteBlock(stream, color);
  }

  // and write the end group block
  stream.WriteBigEndian((ushort)BlockType.GroupEnd);
  stream.WriteBigEndian(0); // there isn't any data, but we still need to specify that
}

Writing Colors

Writing a color block is fairly painless, at least for RGB colors. As with loading an ase file, I’m completely ignoring the existence of Lab, CMYK and Gray scale colors.

private int GetBlockLength(ColorEntry block)
{
  int blockLength;

  blockLength = this.GetBlockLength((Block)block);

  blockLength += 6; // 4 bytes for the color space and 2 bytes for the color type

  // TODO: Include support for other color spaces

  blockLength += 12; // length of RGB data (3 * 4 bytes)

  return blockLength;
}

private void WriteBlock(Stream stream, ColorEntry block)
{
  int blockLength;

  blockLength = this.GetBlockLength(block);

  stream.WriteBigEndian((ushort)BlockType.Color);
  stream.WriteBigEndian(blockLength);

  this.WriteNullTerminatedString(stream, block.Name);

  stream.Write("RGB");

  stream.WriteBigEndian((float)(block.R / 255.0));
  stream.WriteBigEndian((float)(block.G / 255.0));
  stream.WriteBigEndian((float)(block.B / 255.0));

  stream.WriteBigEndian((ushort)block.Type);

  this.WriteExtraData(stream, block.ExtraData);
}

Caveats

When I originally tested this code, I added a simple compare function which compared the bytes of a source ase file with a version written by the new code. For two of the three samples I was using, this was fine, but for the third the files didn’t match. As this didn’t help me in any way diagnose the issue, I ended up writing a very basic (and inefficient) hex viewer, artfully highlighted using the same colors as the ase format description on sepla.net

Comparing a third party ASE file with the version created by the sample application

Comparing a third party ASE file with the version created by the sample application

This allowed me to easily view the files side by side and be able to break them down into their sections to see what was wrong. The example screenshot above shows an identical comparison.

Another compare of a third party ASE file with the version created by the sample application, showing the color data is the same, but the raw file differs

Another compare of a third party ASE file with the version created by the sample application, showing the color data is the same, but the raw file differs

With that third sample file, it was more complicated. In the first case, the file sizes were different – the hex viewer very clearly showed that the sample file has 3 extra null bytes at the end of the file, which my version doesn’t bother writing. I’m not entirely sure what these bytes are for, but I can’t imagine they are official as it’s an odd number.

The second issue was potentially more problematic. In the screenshot above, you can see all the orange values which are the float point representations of the RGB colors, and the last byte of each of these values does not match. However, the translated RGB values do match, so I guess it is a rounding/precision issue.

When I turn this into something more production ready, I will probably store the original floating point values and write them back, rather than loosing precision by converting them to integers (well, bytes really as the range is 0-255) and back again.

 

Download

AdobeSwatchExchangeLoader-v2.zip 10/20/2015 324 Kb

Leave a Reply

Your email address will not be published.