Reading Photoshop Color Swatch (aco) files using C#
In a previous article I described how to read the colour
map from a DeluxePaint LBM/BBM file. In the next pair of
articles, I'm going to describe how to load and save colour
swatch files used by Photoshop (those with the .aco
extension).
Caveat Emptor
As usual, I'll start with a warning. I have a very limited set of sample files to test with, so it may be that there's an error in this code which means it can't handle all files. Certainly it can't handle all colour spaces (more on that later). However, I've tested it on a number of files download from the internet without problems.
Structure of a Photoshop colour swatch file
The structure of the aco
file is straightforward, helped by
Adobe themselves publishing the specification which is
something to appreciate. This article was created using the
October 2013 edition of this specification.
According to the specification, there's two versions of the format both of which are are fairly similar. The specification also implies that applications which support version 2 should write a version 1 palette first, which would admirably solve backwards compatibility problems. In practice this doesn't seem to be the case, as some of the files I tested only had version 2 palettes in them.
The structure is simple. There's a 2-byte version code, followed by 2-bytes describing the number of colours. Then, for each colour, there are 10 further bytes, 2 each describing the colour space and then four values to describe the colour. Version two palettes also then follow this with a four byte integer describing the length of the name, then the bytes which make up said name.
Length | Description | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | Version | ||||||||||||
2 | Number of colours | ||||||||||||
count * 10 (+ 4 + variable (version 2 only)) |
Colour data
|
||||||||||||
Version 2 only
|
All the data in an aco
file is stored in big-endian
format and therefore needs to be reversed on Windows systems.
Most colour spaces only use three of the four available values, but regardless of how many are actually used, all must be specified.
Colour Spaces
I mentioned above that each colour has a description of what colour space it belongs to. The specification defines the following colour spaces:
Id | Description |
---|---|
0 | RGB. The first three values in the colour data are red, green, and blue. They are full unsigned 16-bit values as in Apple's RGBColordata structure. Pure red = 65535, 0, 0. |
1 | HSB. The first three values in the colour data are hue, saturation, and brightness. They are full unsigned 16-bit values as in Apple's HSVColordata structure. Pure red = 0,65535, 65535. |
2 | CMYK. The four values in the colour data are cyan, magenta, yellow, and black. They are full unsigned 16-bit values. For example, pure cyan = 0,65535,65535,65535. |
7 | Lab. The first three values in the colour data are lightness, a chrominance, and b chrominance. Lightness is a 16-bit value from 0...10000. Chrominance components are each 16-bit values from -12800...12700. Gray values are represented by chrominance components of 0. Pure white = 10000,0,0. |
8 | Grayscale. The first value in the colour data is the gray value, from 0...10000. |
To avoid complicating matters, this article will concentrate on
RGB
and Grayscale
colour spaces, although I'll include the
basics of HSV
too for if you have a conversion class kicking
around.
Reading short/int data types from bytes
As I mentioned above, the values in this file format are all
big-endian. As Windows uses little-endian, we need to do some
bit shifting when we read each byte comprising either a short
(Int16
) or an int
(Int32
), using the following helpers:
/// <summary>
/// Reads a 16bit unsigned integer in big-endian format.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The unsigned 16bit integer cast to an <c>Int32</c>.</returns>
private int ReadInt16(Stream stream)
{
return (stream.ReadByte() << 8) | (stream.ReadByte() << 0);
}
/// <summary>
/// Reads a 32bit unsigned integer in big-endian format.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <returns>The unsigned 32bit integer cast to an <c>Int32</c>.</returns>
private int ReadInt32(Stream stream)
{
return ((byte)stream.ReadByte() << 24) | ((byte)stream.ReadByte() << 16) | ((byte)stream.ReadByte() << 8) | ((byte)stream.ReadByte() << 0);
}
The
<< 0
bit-shift in the above methods is technically unnecessary and can be removed. However, I find it makes the intent of the code clearer.
Reading strings
For version 2 files, we need to read a string, which is
comprised of two bytes per character. Fortunately for us, the
.NET Framework includes a BigEndianUnicode
(MSDN) class
that we can use to convert a byte array to a string. As this
class does the endian conversion for us, we don't need to do
anything special when reading the bytes.
/// <summary>
/// Reads a unicode string of the specified length.
/// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <param name="length">The number of characters in the string.</param>
/// <returns>The string read from the stream.</returns>
private string ReadString(Stream stream, int length)
{
byte[] buffer;
buffer = new byte[length * 2];
stream.Read(buffer, 0, buffer.Length);
return Encoding.BigEndianUnicode.GetString(buffer);
}
Reading the file
With the preliminaries done with, lets read the file!
We start off by reading the file version so we know how to process the rest of the file, or at least the first part of it. If we don't have a version 1 or version 2 file, then we simply abort.
using (Stream stream = File.OpenRead(fileName))
{
FileVersion version;
// read the version, which occupies two bytes
version = (FileVersion)this.ReadInt16(stream);
if (version != FileVersion.Version1 && version != FileVersion.Version2)
throw new InvalidDataException("Invalid version information.");
colorPalette = this.ReadSwatches(stream, version);
if (version == FileVersion.Version1)
{
version = (FileVersion)this.ReadInt16(stream);
if (version == FileVersion.Version2)
colorPalette = this.ReadSwatches(stream, version);
}
}
In the above example, if a file has both versions, then I read
them both (assuming the file contains version 1 followed by
version 2). However, there's no point in doing this if you
aren't going to do anything with the swatch name. For example,
this demonstration program converts all the values into the
standard .NET Color
structure - which doesn't allow you to
set the Name
property. In this scenario, clearly it's a waste
of time reading the version 2 data if you've just read the data
from version 1. However, if you are storing the data in an
object that supports the name, then it's probably a good idea to
discard the previously read data and re-read the version 2 data.
Reading colour data
As the two documented file formats are almost identical, we can use the same code to handle reading the data, and then perform a little bit extra for the newer file format. The core of the code which reads the colour data looks like this.
// read the number of colors, which also occupies two bytes
colorCount = this.ReadInt16(stream);
for (int i = 0; i < colorCount; i++)
{
ColorSpace colorSpace;
int value1;
int value2;
int value3;
int value4;
// again, two bytes for the color space
colorSpace = (ColorSpace)(this.ReadInt16(stream));
// then the four values which comprise each color
value1 = this.ReadInt16(stream);
value2 = this.ReadInt16(stream);
value3 = this.ReadInt16(stream);
value4 = this.ReadInt16(stream);
// and finally, the name of the swatch (version2 only)
if (version == FileVersion.Version2)
{
int length;
string name;
length = ReadInt32(stream);
name = this.ReadString(stream, length);
}
}
Translating the colour spaces
Once we've read the colour space and the four values of the colour data, we need to process it.
The first space, RGB, is simple enough. The Adobe format is using the range 0-65535, so we just need to convert that to the standard 0-255 range:
switch (colorSpace)
{
case ColorSpace.Rgb:
int red;
int green;
int blue;
red = value1 / 256; // 0-255
green = value2 / 256; // 0-255
blue = value3 / 256; // 0-255
results.Add(Color.FromArgb(red, green, blue));
break;
Next is HSL. How you process that depends on the class you are using, and the range of values it accepts.
case ColorSpace.Hsb:
double hue;
double saturation;
double brightness;
hue = value1 / 182.04; // 0-359
saturation = value2 / 655.35; // 0-1
brightness = value3 / 655.35; // 0-1
results.Add(new HslColor(hue, saturation, brightness).ToRgbColor());
break;
The last colour space we can easily support is gray scale.
case AdobePhotoshopColorSwatchColorSpace.Grayscale:
int gray;
// Grayscale.
// The first value in the color data is the gray value, from 0...10000.
gray = (int)(value1 / 39.0625);
results.Add(Color.FromArgb(gray, gray, gray));
break;
Files using the Lab or CMYK spaces will throw an exception as these are beyond the scope of this example.
default:
throw new InvalidDataException(string.Format("Color space '{0}' not supported.", colorSpace));
}
Although none of the sample files I tested mixed colour spaces, they were either all RGB, all Lab or all CMYK, the specification suggests that it's at least possible. In this case, throwing an exception might not be the right idea as it could be possible to load other colours. Therefore it may be a better idea to just ignore such errors to allow any valid data to be read.
Wrapping up
As with reading LBM colour maps, reading the Photoshop colour swatches was also quite an easy process.
You can download a fully working sample from the link below, and
my next article will reverse the process to allow you to write
your own aco
files.
Update History
- 2014-01-22 - First published
- 2020-11-21 - Updated formatting
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
PhotoshopColorSwatchLoader.zip
|
Sample project for the Reading Photoshop Color Swatch (aco) files using C# blog article. |
1.0.0.0 | 22/01/2014 | 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
Brad Harris
#
AMAZING. I was looking for this but for PHP ... Amazing, none the less. Thanks for at least letting me know somebody did it.
Richard Moss
#
Hello,
Thanks for the comment. I have very little experience with PHP but I imagine you can do this using statements like
fread
. I have no idea how you'd do bit shifting to swap endians though :)Regards; Richard Moss
Tanner
#
Hi Richard. Thank you for this very helpful breakdown of swatch files. While Adobe's spec is helpful, it's always helpful to see a working implementation!
Besides adding a "thank you" I just wanted to pass along a heads-up that I think your content has been plagiarized at the following link:
~~link removed~~
I stumbled across that link first and only traced it back here by accident. Hopefully there's a straightforward way to resolve it.
Thank you again for your sharing your work.
Edit by Richard Moss: Apologies for editing your post but I don't wish to provide any form of link back to the plagiarisers website.
Richard Moss
#
Hello,
Thanks for the comments! I am aware of Warren's ripping off of several of my posts, but he never responded to requests to have them removed. Not really a huge amount I can do about it frustratingly. However, I'm glad that you found the "real" author - although this code is hardly rocket science and I've freely distributed it, it takes quite a bit of effort writing the posts themselves and it's galling that this person simply takes them, removes the preamble, changes any links and posts them as his own work. He even went to the trouble of unpacking the example code and replacing all the notices in the source code with this own stuff. The code is freely licensed, my blog post words certainly aren't!
Not to mention borrowing the styling of the posts which clashes with the original style of the site. Unless of course by some coincidence he bought the same template I did and restyled it using the exact highlight colour that Cyotek has used since around 1998. Some coincidence ;)
Thanks again for the kind words.
Regards;
Richard Moss
Richard Moss
#
Incidentally, thank you for your dithering article. I found that very helpful when I was creating my own set of dithering routines. (Which I still need to get back to at some point once I figure out where I'm going wrong with Bayer dithering.)
Small world :)
Regards;
Richard Moss