Quantcast
Viewing all articles
Browse latest Browse all 559

Writing Adobe Swatch Exchange (ase) files 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 program to save ase files as well as load them.

Image may be NSFW.
Clik here to view.
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.

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

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

Finally, lets not forget our length prefixed strings!

publicstaticvoid 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 rudimentry

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

privatevoid 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 howeverforeach (ColorEntry color inthis.Colors)
  {this.WriteBlock(stream, color);
  }// now write the groupsforeach (ColorGroup groupinthis.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.

privateint 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;
}privatevoid 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 groupforeach (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.

privateint 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;
}privatevoid 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.

Image may be NSFW.
Clik here to view.
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.

Image may be NSFW.
Clik here to view.
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.

Downloads

All content Copyright © by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is http://www.cyotek.com/blog/writing-adobe-swatch-exchange-ase-files-using-csharp?source=rss


Viewing all articles
Browse latest Browse all 559

Trending Articles