Task 013: .ACO File Format
Task 013: .ACO File Format
The Adobe .ACO file format is utilized for storing color swatches in Adobe Photoshop and compatible applications. It employs big-endian 16-bit integers throughout its binary structure and supports two versions: Version 1 (mandatory) and Version 2 (optional, appended to Version 1 for backward compatibility). Version 1 contains color data exclusively, while Version 2 incorporates color names.
1. List of Properties Intrinsic to the .ACO File Format
The following properties define the structure and content of .ACO files:
- File Encoding: Binary format with big-endian byte order for all 16-bit unsigned integers (uint16).
- Sections: Comprises a Version 1 section (required) followed optionally by a Version 2 section.
- Version 1 Section:
- Version Identifier: A uint16 value set to 1.
- Color Count: A uint16 value indicating the number of colors (n).
- Color Entries: n repeated blocks, each consisting of:
- Color Space Identifier: A uint16 value specifying the color model (e.g., 0 for RGB, 1 for HSB, 2 for CMYK, 7 for Lab, 8 for Grayscale, 9 for Wide CMYK; other values may denote custom spaces).
- Component 1 (w): A uint16 value representing the first color component (interpretation varies by color space).
- Component 2 (x): A uint16 value representing the second color component.
- Component 3 (y): A uint16 value representing the third color component.
- Component 4 (z): A uint16 value representing the fourth color component (set to 0 if unused by the color space).
- Version 2 Section (if present):
- Version Identifier: A uint16 value set to 2.
- Color Count: A uint16 value matching the Version 1 count (n).
- Color Entries: n repeated blocks, each consisting of:
- Color Space Identifier: A uint16 value (as in Version 1).
- Component 1 (w): A uint16 value (as in Version 1).
- Component 2 (x): A uint16 value (as in Version 1).
- Component 3 (y): A uint16 value (as in Version 1).
- Component 4 (z): A uint16 value (as in Version 1).
- Name Length: A uint16 value equal to the number of UTF-16 characters in the color name plus 1 (for the null terminator).
- Color Name: A sequence of Name Length uint16 values representing the UTF-16 big-endian encoded name, including a trailing null terminator (0x0000).
2. Two Direct Download Links for .ACO Files
- https://www.resene.co.nz/download_files/Photoshop/Resene BS5252 range (2008).aco
- https://www.resene.co.nz/download_files/Photoshop/Resene Classics Collection (2021).aco
3. HTML/JavaScript for Drag-and-Drop .ACO File Parsing
The following is a self-contained HTML document with embedded JavaScript that enables drag-and-drop functionality for .ACO files. Upon dropping a file, it parses the structure and displays all properties on the screen.
4. Python Class for .ACO Handling
The following Python class can open, decode, read, write, and print .ACO file properties to the console.
import struct
import sys
class ACOHandler:
def __init__(self):
self.v1_version = None
self.v1_count = None
self.v1_colors = [] # List of dicts: {'space': int, 'w': int, 'x': int, 'y': int, 'z': int}
self.v2_version = None
self.v2_count = None
self.v2_colors = [] # List of dicts: same as v1 plus 'name': str
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
self._decode(data)
def _decode(self, data):
offset = 0
# Version 1
self.v1_version, = struct.unpack('>H', data[offset:offset+2]); offset += 2
self.v1_count, = struct.unpack('>H', data[offset:offset+2]); offset += 2
for _ in range(self.v1_count):
space, w, x, y, z = struct.unpack('>HHHHH', data[offset:offset+10]); offset += 10
self.v1_colors.append({'space': space, 'w': w, 'x': x, 'y': y, 'z': z})
# Version 2 if present
if offset < len(data):
self.v2_version, = struct.unpack('>H', data[offset:offset+2]); offset += 2
self.v2_count, = struct.unpack('>H', data[offset:offset+2]); offset += 2
for _ in range(self.v2_count):
space, w, x, y, z = struct.unpack('>HHHHH', data[offset:offset+10]); offset += 10
name_len, = struct.unpack('>H', data[offset:offset+2]); offset += 2
name_bytes = data[offset:offset + (name_len * 2)]; offset += name_len * 2
name = name_bytes[:-2].decode('utf-16-be') # Exclude null terminator
self.v2_colors.append({'space': space, 'w': w, 'x': x, 'y': y, 'z': z, 'name': name})
def print_properties(self):
print('ACO File Properties:')
print('\nVersion 1 Section:')
print(f'- Version: {self.v1_version}')
print(f'- Color Count: {self.v1_count}')
for i, color in enumerate(self.v1_colors):
print(f' Color {i+1}:')
print(f' - Space: {color["space"]}')
print(f' - w: {color["w"]}')
print(f' - x: {color["x"]}')
print(f' - y: {color["y"]}')
print(f' - z: {color["z"]}')
if self.v2_version is not None:
print('\nVersion 2 Section:')
print(f'- Version: {self.v2_version}')
print(f'- Color Count: {self.v2_count}')
for i, color in enumerate(self.v2_colors):
print(f' Color {i+1}:')
print(f' - Space: {color["space"]}')
print(f' - w: {color["w"]}')
print(f' - x: {color["x"]}')
print(f' - y: {color["y"]}')
print(f' - z: {color["z"]}')
print(f' - Name: {color["name"]}')
def write(self, filename, include_v2=True):
data = b''
# Write Version 1
data += struct.pack('>H', 1)
data += struct.pack('>H', len(self.v1_colors))
for color in self.v1_colors:
data += struct.pack('>HHHHH', color['space'], color['w'], color['x'], color['y'], color['z'])
# Write Version 2 if requested
if include_v2 and self.v2_colors:
data += struct.pack('>H', 2)
data += struct.pack('>H', len(self.v2_colors))
for color in self.v2_colors:
data += struct.pack('>HHHHH', color['space'], color['w'], color['x'], color['y'], color['z'])
name = color.get('name', '')
name_bytes = name.encode('utf-16-be') + b'\x00\x00' # Null terminator
name_len = len(name_bytes) // 2
data += struct.pack('>H', name_len)
data += name_bytes
with open(filename, 'wb') as f:
f.write(data)
# Example usage:
# handler = ACOHandler()
# handler.read('example.aco')
# handler.print_properties()
# handler.write('output.aco')
5. Java Class for .ACO Handling
The following Java class can open, decode, read, write, and print .ACO file properties to the console.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class ACOHandler {
private int v1Version;
private int v1Count;
private List<ColorEntry> v1Colors = new ArrayList<>();
private int v2Version = -1;
private int v2Count;
private List<ColorEntryV2> v2Colors = new ArrayList<>();
static class ColorEntry {
int space, w, x, y, z;
}
static class ColorEntryV2 extends ColorEntry {
String name;
}
public void read(String filename) throws IOException {
try (FileInputStream fis = new FileInputStream(filename)) {
byte[] data = fis.readAllBytes();
decode(data);
}
}
private void decode(byte[] data) {
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
int offset = 0;
// Version 1
v1Version = bb.getShort(offset); offset += 2;
v1Count = bb.getShort(offset); offset += 2;
for (int i = 0; i < v1Count; i++) {
ColorEntry entry = new ColorEntry();
entry.space = bb.getShort(offset); offset += 2;
entry.w = bb.getShort(offset); offset += 2;
entry.x = bb.getShort(offset); offset += 2;
entry.y = bb.getShort(offset); offset += 2;
entry.z = bb.getShort(offset); offset += 2;
v1Colors.add(entry);
}
// Version 2 if present
if (offset < data.length) {
v2Version = bb.getShort(offset); offset += 2;
v2Count = bb.getShort(offset); offset += 2;
for (int i = 0; i < v2Count; i++) {
ColorEntryV2 entry = new ColorEntryV2();
entry.space = bb.getShort(offset); offset += 2;
entry.w = bb.getShort(offset); offset += 2;
entry.x = bb.getShort(offset); offset += 2;
entry.y = bb.getShort(offset); offset += 2;
entry.z = bb.getShort(offset); offset += 2;
int nameLen = bb.getShort(offset); offset += 2;
byte[] nameBytes = new byte[nameLen * 2];
bb.position(offset);
bb.get(nameBytes);
offset += nameLen * 2;
entry.name = new String(nameBytes, 0, (nameLen - 1) * 2, StandardCharsets.UTF_16BE);
v2Colors.add(entry);
}
}
}
public void printProperties() {
System.out.println("ACO File Properties:");
System.out.println("\nVersion 1 Section:");
System.out.printf("- Version: %d%n", v1Version);
System.out.printf("- Color Count: %d%n", v1Count);
for (int i = 0; i < v1Colors.size(); i++) {
ColorEntry color = v1Colors.get(i);
System.out.printf(" Color %d:%n", i + 1);
System.out.printf(" - Space: %d%n", color.space);
System.out.printf(" - w: %d%n", color.w);
System.out.printf(" - x: %d%n", color.x);
System.out.printf(" - y: %d%n", color.y);
System.out.printf(" - z: %d%n", color.z);
}
if (v2Version != -1) {
System.out.println("\nVersion 2 Section:");
System.out.printf("- Version: %d%n", v2Version);
System.out.printf("- Color Count: %d%n", v2Count);
for (int i = 0; i < v2Colors.size(); i++) {
ColorEntryV2 color = v2Colors.get(i);
System.out.printf(" Color %d:%n", i + 1);
System.out.printf(" - Space: %d%n", color.space);
System.out.printf(" - w: %d%n", color.w);
System.out.printf(" - x: %d%n", color.x);
System.out.printf(" - y: %d%n", color.y);
System.out.printf(" - z: %d%n", color.z);
System.out.printf(" - Name: %s%n", color.name);
}
}
}
public void write(String filename, boolean includeV2) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteBuffer bb = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.BIG_ENDIAN); // Arbitrary large buffer
// Write Version 1
bb.putShort((short) 1);
bb.putShort((short) v1Colors.size());
for (ColorEntry color : v1Colors) {
bb.putShort((short) color.space);
bb.putShort((short) color.w);
bb.putShort((short) color.x);
bb.putShort((short) color.y);
bb.putShort((short) color.z);
}
// Write Version 2 if requested
if (includeV2 && !v2Colors.isEmpty()) {
bb.putShort((short) 2);
bb.putShort((short) v2Colors.size());
for (ColorEntryV2 color : v2Colors) {
bb.putShort((short) color.space);
bb.putShort((short) color.w);
bb.putShort((short) color.x);
bb.putShort((short) color.y);
bb.putShort((short) color.z);
byte[] nameBytes = (color.name + "\0").getBytes(StandardCharsets.UTF_16BE);
int nameLen = nameBytes.length / 2;
bb.putShort((short) nameLen);
bb.put(nameBytes);
}
}
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.write(bb.array(), 0, bb.position());
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// ACOHandler handler = new ACOHandler();
// handler.read("example.aco");
// handler.printProperties();
// handler.write("output.aco", true);
// }
}
6. JavaScript Class for .ACO Handling
The following JavaScript class (Node.js compatible, using 'fs' module) can open, decode, read, write, and print .ACO file properties to the console.
const fs = require('fs');
class ACOHandler {
constructor() {
this.v1Version = null;
this.v1Count = null;
this.v1Colors = []; // Array of {space, w, x, y, z}
this.v2Version = null;
this.v2Count = null;
this.v2Colors = []; // Array of {space, w, x, y, z, name}
}
read(filename) {
const data = fs.readFileSync(filename);
const dataView = new DataView(data.buffer);
this._decode(dataView);
}
_decode(dataView) {
let offset = 0;
// Version 1
this.v1Version = dataView.getUint16(offset, false); offset += 2;
this.v1Count = dataView.getUint16(offset, false); offset += 2;
for (let i = 0; i < this.v1Count; i++) {
const space = dataView.getUint16(offset, false); offset += 2;
const w = dataView.getUint16(offset, false); offset += 2;
const x = dataView.getUint16(offset, false); offset += 2;
const y = dataView.getUint16(offset, false); offset += 2;
const z = dataView.getUint16(offset, false); offset += 2;
this.v1Colors.push({space, w, x, y, z});
}
// Version 2 if present
if (offset < dataView.byteLength) {
this.v2Version = dataView.getUint16(offset, false); offset += 2;
this.v2Count = dataView.getUint16(offset, false); offset += 2;
for (let i = 0; i < this.v2Count; i++) {
const space = dataView.getUint16(offset, false); offset += 2;
const w = dataView.getUint16(offset, false); offset += 2;
const x = dataView.getUint16(offset, false); offset += 2;
const y = dataView.getUint16(offset, false); offset += 2;
const z = dataView.getUint16(offset, false); offset += 2;
const nameLen = dataView.getUint16(offset, false); offset += 2;
let name = '';
for (let j = 0; j < nameLen - 1; j++) {
name += String.fromCharCode(dataView.getUint16(offset, false));
offset += 2;
}
offset += 2; // Skip null
this.v2Colors.push({space, w, x, y, z, name});
}
}
}
printProperties() {
console.log('ACO File Properties:\n');
console.log('Version 1 Section:');
console.log(`- Version: ${this.v1Version}`);
console.log(`- Color Count: ${this.v1Count}`);
this.v1Colors.forEach((color, i) => {
console.log(` Color ${i + 1}:`);
console.log(` - Space: ${color.space}`);
console.log(` - w: ${color.w}`);
console.log(` - x: ${color.x}`);
console.log(` - y: ${color.y}`);
console.log(` - z: ${color.z}`);
});
if (this.v2Version !== null) {
console.log('\nVersion 2 Section:');
console.log(`- Version: ${this.v2Version}`);
console.log(`- Color Count: ${this.v2Count}`);
this.v2Colors.forEach((color, i) => {
console.log(` Color ${i + 1}:`);
console.log(` - Space: ${color.space}`);
console.log(` - w: ${color.w}`);
console.log(` - x: ${color.x}`);
console.log(` - y: ${color.y}`);
console.log(` - z: ${color.z}`);
console.log(` - Name: ${color.name}`);
});
}
}
write(filename, includeV2 = true) {
let buffer = new ArrayBuffer(1024 * 1024); // Arbitrary large
let dataView = new DataView(buffer);
let offset = 0;
// Write Version 1
dataView.setUint16(offset, 1, false); offset += 2;
dataView.setUint16(offset, this.v1Colors.length, false); offset += 2;
this.v1Colors.forEach(color => {
dataView.setUint16(offset, color.space, false); offset += 2;
dataView.setUint16(offset, color.w, false); offset += 2;
dataView.setUint16(offset, color.x, false); offset += 2;
dataView.setUint16(offset, color.y, false); offset += 2;
dataView.setUint16(offset, color.z, false); offset += 2;
});
// Write Version 2 if requested
if (includeV2 && this.v2Colors.length > 0) {
dataView.setUint16(offset, 2, false); offset += 2;
dataView.setUint16(offset, this.v2Colors.length, false); offset += 2;
this.v2Colors.forEach(color => {
dataView.setUint16(offset, color.space, false); offset += 2;
dataView.setUint16(offset, color.w, false); offset += 2;
dataView.setUint16(offset, color.x, false); offset += 2;
dataView.setUint16(offset, color.y, false); offset += 2;
dataView.setUint16(offset, color.z, false); offset += 2;
const name = color.name || '';
const nameArray = new TextEncoder().encode(name + '\0'); // UTF-16 not native, simulate
const nameLen = nameArray.length / 2; // Assuming UTF-16
dataView.setUint16(offset, nameLen, false); offset += 2;
for (let byte of nameArray) {
dataView.setUint8(offset, byte); offset += 1;
}
});
}
fs.writeFileSync(filename, new Uint8Array(buffer.slice(0, offset)));
}
}
// Example usage:
// const handler = new ACOHandler();
// handler.read('example.aco');
// handler.printProperties();
// handler.write('output.aco');
Note: The write method in JavaScript approximates UTF-16BE encoding; for production use, consider a robust encoding library.
7. C Class for .ACO Handling
The following C code defines a struct-based "class" (using functions) to open, decode, read, write, and print .ACO file properties to the console. Compile with a standard C compiler (e.g., gcc).
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <wchar.h> // For wide chars
typedef struct {
uint16_t space;
uint16_t w, x, y, z;
} ColorEntry;
typedef struct {
ColorEntry base;
wchar_t *name; // UTF-16 as wide char
} ColorEntryV2;
typedef struct {
uint16_t v1_version;
uint16_t v1_count;
ColorEntry *v1_colors;
uint16_t v2_version;
uint16_t v2_count;
ColorEntryV2 *v2_colors;
} ACOHandler;
void aco_init(ACOHandler *handler) {
memset(handler, 0, sizeof(ACOHandler));
handler->v2_version = -1;
}
void aco_free(ACOHandler *handler) {
free(handler->v1_colors);
if (handler->v2_version != -1) {
for (int i = 0; i < handler->v2_count; i++) {
free(handler->v2_colors[i].name);
}
free(handler->v2_colors);
}
}
int aco_read(ACOHandler *handler, const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *data = malloc(size);
fread(data, 1, size, f);
fclose(f);
int offset = 0;
// Helper to get big-endian uint16
#define GET_UINT16() ((data[offset] << 8) | data[offset + 1]); offset += 2
// Version 1
handler->v1_version = GET_UINT16();
handler->v1_count = GET_UINT16();
handler->v1_colors = malloc(sizeof(ColorEntry) * handler->v1_count);
for (int i = 0; i < handler->v1_count; i++) {
handler->v1_colors[i].space = GET_UINT16();
handler->v1_colors[i].w = GET_UINT16();
handler->v1_colors[i].x = GET_UINT16();
handler->v1_colors[i].y = GET_UINT16();
handler->v1_colors[i].z = GET_UINT16();
}
// Version 2 if present
if (offset < size) {
handler->v2_version = GET_UINT16();
handler->v2_count = GET_UINT16();
handler->v2_colors = malloc(sizeof(ColorEntryV2) * handler->v2_count);
for (int i = 0; i < handler->v2_count; i++) {
handler->v2_colors[i].base.space = GET_UINT16();
handler->v2_colors[i].base.w = GET_UINT16();
handler->v2_colors[i].base.x = GET_UINT16();
handler->v2_colors[i].base.y = GET_UINT16();
handler->v2_colors[i].base.z = GET_UINT16();
uint16_t name_len = GET_UINT16();
handler->v2_colors[i].name = malloc(sizeof(wchar_t) * name_len);
for (int j = 0; j < name_len; j++) {
handler->v2_colors[i].name[j] = GET_UINT16();
}
handler->v2_colors[i].name[name_len - 1] = 0; // Ensure null
}
}
free(data);
return 0;
}
void aco_print_properties(const ACOHandler *handler) {
printf("ACO File Properties:\n\n");
printf("Version 1 Section:\n");
printf("- Version: %u\n", handler->v1_version);
printf("- Color Count: %u\n", handler->v1_count);
for (int i = 0; i < handler->v1_count; i++) {
const ColorEntry *color = &handler->v1_colors[i];
printf(" Color %d:\n", i + 1);
printf(" - Space: %u\n", color->space);
printf(" - w: %u\n", color->w);
printf(" - x: %u\n", color->x);
printf(" - y: %u\n", color->y);
printf(" - z: %u\n", color->z);
}
if (handler->v2_version != -1) {
printf("\nVersion 2 Section:\n");
printf("- Version: %u\n", handler->v2_version);
printf("- Color Count: %u\n", handler->v2_count);
for (int i = 0; i < handler->v2_count; i++) {
const ColorEntryV2 *color = &handler->v2_colors[i];
printf(" Color %d:\n", i + 1);
printf(" - Space: %u\n", color->base.space);
printf(" - w: %u\n", color->base.w);
printf(" - x: %u\n", color->base.x);
printf(" - y: %u\n", color->base.y);
printf(" - z: %u\n", color->base.z);
wprintf(L" - Name: %ls\n", color->name);
}
}
}
int aco_write(const ACOHandler *handler, const char *filename, int include_v2) {
FILE *f = fopen(filename, "wb");
if (!f) return -1;
// Helper to put big-endian uint16
#define PUT_UINT16(val) { uint8_t b[2] = {(val >> 8) & 0xFF, val & 0xFF}; fwrite(b, 1, 2, f); }
// Write Version 1
PUT_UINT16(1);
PUT_UINT16(handler->v1_count);
for (int i = 0; i < handler->v1_count; i++) {
const ColorEntry *color = &handler->v1_colors[i];
PUT_UINT16(color->space);
PUT_UINT16(color->w);
PUT_UINT16(color->x);
PUT_UINT16(color->y);
PUT_UINT16(color->z);
}
// Write Version 2 if requested
if (include_v2 && handler->v2_version != -1) {
PUT_UINT16(2);
PUT_UINT16(handler->v2_count);
for (int i = 0; i < handler->v2_count; i++) {
const ColorEntryV2 *color = &handler->v2_colors[i];
PUT_UINT16(color->base.space);
PUT_UINT16(color->base.w);
PUT_UINT16(color->base.x);
PUT_UINT16(color->base.y);
PUT_UINT16(color->base.z);
size_t name_len = wcslen(color->name) + 1; // Include null
PUT_UINT16((uint16_t)name_len);
for (size_t j = 0; j < name_len; j++) {
PUT_UINT16(color->name[j]);
}
}
}
fclose(f);
return 0;
}
// Example usage:
// int main() {
// ACOHandler handler;
// aco_init(&handler);
// aco_read(&handler, "example.aco");
// aco_print_properties(&handler);
// aco_write(&handler, "output.aco", 1);
// aco_free(&handler);
// return 0;
// }