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.
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
.
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, lets 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 colour.
Saving the version header is rudimentary
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.
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.
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.
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.
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.
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
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
AdobeSwatchExchangeLoader-v2.zip
|
Sample project for loading and saving Adobe Swatch Exchange (ase) files using C#. |
2.0.0.0 | 21/10/2015 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Louise
#
Hello! I am trying to implement a browser js script version of something like this for p5js that will allow color downloads from p5js sketches in an ase format. I managed to get my script to write an ase binary file with the correct header, beginning of color block, correct name of the color, etc, but for the rgb encoding it produces extra unwanted characters. Do you have any ideas how to fix this? I am just using this for working with students on their p5js projects and showing them how what they code could transfer between platforms, it is all open source to everyone.
I don't know if you have any ideas of how I can fix it? https://openprocessing.org/sketch/2278700
You need to click on the </> button at the top to see the code, then the tab that says broken Adobe AS...
Thank you for reading