In my last post, I described how to read Adobe Swatch Exchange files using C#. Now I'm going to update that sample program 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 bits.

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.

csharp
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.

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

Finally, lets not forget our length prefixed strings!

csharp
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 colour.

Saving the version header is rudimentary

csharp
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 colour in our document.

csharp
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 the 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 at 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 colour blocks in between. Potentially you can have nested groups, but I haven't coded for this - I need to grab myself a Creative Cloud subscription and experiment with ase files, at which point I'll update these samples if need be.

csharp
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 Colours

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

csharp
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, or why this took longer than it should have done

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 colours 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 the files down into their sections and 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 colour 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 colour 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 colours, 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.

On with the show

The updated demonstration application is available for download below, including new sample files generated directly by the program.

Update History

  • 2015-10-21 - First published
  • 2020-11-21 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments