Task 013: .ACO File Format

Task 013: .ACO File Format

The .ACO (Adobe Color Swatch) file format is a binary file format used by Adobe Photoshop and other Adobe Creative Suite applications to store color swatches, which are collections of predefined colors used for consistent design across projects. Below, I will address each part of your request based on the available information and the specifications of the .ACO file format.


1. Properties of the .ACO File Format Intrinsic to Its File System

Based on the provided references, particularly the detailed specification from sources like nomodes.com and fileformat.com, the intrinsic properties of the .ACO file format are as follows:

  • File Extension: .aco
  • Full Name: Adobe Photoshop Color Swatch File
  • MIME Type: application/x-photoshop
  • File Type: Binary
  • Developer: Adobe Systems
  • Primary Association: Adobe Photoshop
  • Usage: Stores a collection of colors (swatches) for use in graphic design, web design, and professional photo editing
  • File Structure:
  • Binary Format: The file is stored in a binary format, not human-readable like XML or text.
  • Versioning: Supports two versions:
  • Version 1: Lists the coordinates of each color in a specified color space.
  • Version 2: Includes both color coordinates and the name of each color (displayed on mouse hover in Photoshop).
  • Structure Details:
  • Header: Starts with a 2-byte version number (0x0001 for Version 1, 0x0002 for Version 2).
  • Color Count: A 2-byte integer indicating the number of colors in the swatch.
  • Color Entries:
  • Version 1: Each color entry includes:
  • Color Space ID (2 bytes): Indicates the color model (e.g., 0 for RGB, 1 for HSB, 2 for CMYK, 7 for LAB, etc.).
  • Color Components (8 bytes): Four 16-bit integers representing color values (e.g., R, G, B, and a dummy value for RGB).
  • Version 2: Extends Version 1 by adding:
  • Name Length (4 bytes): Length of the color name.
  • Name: UTF-16 encoded string (variable length, padded to an even number of bytes).
  • Color Models Supported: RGB, CMYK, LAB, HSB, Grayscale, and others (as specified by the color space ID).
  • Number of Colors: Variable, depending on the swatch (stored in the color count field).
  • File Size: Variable, typically small (in the KB range), depending on the number of colors and their names (in Version 2).
  • Editable: Yes, primarily with Adobe Photoshop or compatible software.
  • Compatibility: Primarily compatible with Adobe Photoshop; limited support in other applications like GIMP.
  • Open Standard: No, it is a proprietary format.
  • Support for Transparency: No, only solid colors are stored.
  • Can Store Gradients: No, only solid color swatches.
  • Embeddable: Yes, within certain Adobe file formats.
  • Platforms Supported: Windows, macOS.
  • Byte Order: Big-endian for numerical values.

These properties are intrinsic to the .ACO file format as they define its structure, purpose, and behavior within the file system and its associated applications.


2. Python Class for .ACO File Handling

Below is a Python class that can read, decode, write, and print the properties of an .ACO file. It supports both Version 1 and Version 2 of the .ACO format.

import struct
import os

class ACOFile:
    def __init__(self, filename=None):
        self.filename = filename
        self.version = 0
        self.color_count = 0
        self.colors = []  # List of (color_space, components, name) tuples
        self.properties = {
            "File Extension": ".aco",
            "Full Name": "Adobe Photoshop Color Swatch File",
            "MIME Type": "application/x-photoshop",
            "File Type": "Binary",
            "Developer": "Adobe Systems",
            "Primary Association": "Adobe Photoshop",
            "Usage": "Stores a collection of colors for graphic design",
            "Editable": "Yes, with Adobe Photoshop or compatible software",
            "Compatibility": "Primarily Adobe Photoshop",
            "Open Standard": "No",
            "Color Models Supported": "RGB, CMYK, LAB, HSB, Grayscale, others",
            "Support for Transparency": "No",
            "Can Store Gradients": "No",
            "Embeddable": "Yes, within certain Adobe file formats",
            "Platforms Supported": "Windows, macOS"
        }
        if filename:
            self.read(filename)

    def read(self, filename):
        """Read and decode an .ACO file."""
        with open(filename, 'rb') as f:
            # Read version (2 bytes)
            self.version = struct.unpack('>H', f.read(2))[0]
            if self.version not in (1, 2):
                raise ValueError(f"Unsupported ACO version: {self.version}")

            # Read number of colors (2 bytes)
            self.color_count = struct.unpack('>H', f.read(2))[0]
            self.properties["Number of Colors"] = str(self.color_count)
            self.properties["File Size"] = str(os.path.getsize(filename)) + " bytes"

            # Read color entries
            for _ in range(self.color_count):
                color_space = struct.unpack('>H', f.read(2))[0]
                components = struct.unpack('>HHHH', f.read(8))  # Four 16-bit values
                name = ""
                if self.version == 2:
                    name_length = struct.unpack('>I', f.read(4))[0]
                    name = f.read(name_length * 2).decode('utf-16-be').rstrip('\x00')
                self.colors.append((color_space, components, name))

            # If Version 1 file has a Version 2 section, read it
            if self.version == 1 and f.tell() < os.path.getsize(filename):
                self.version = struct.unpack('>H', f.read(2))[0]
                if self.version == 2:
                    self.color_count = struct.unpack('>H', f.read(2))[0]
                    self.properties["Number of Colors"] = str(self.color_count)
                    self.colors = []
                    for _ in range(self.color_count):
                        color_space = struct.unpack('>H', f.read(2))[0]
                        components = struct.unpack('>HHHH', f.read(8))
                        name_length = struct.unpack('>I', f.read(4))[0]
                        name = f.read(name_length * 2).decode('utf-16-be').rstrip('\x00')
                        self.colors.append((color_space, components, name))

    def write(self, filename):
        """Write colors to an .ACO file (Version 2 format)."""
        with open(filename, 'wb') as f:
            # Write Version 2 header
            f.write(struct.pack('>H', 2))
            f.write(struct.pack('>H', len(self.colors)))
            for color_space, components, name in self.colors:
                f.write(struct.pack('>H', color_space))
                f.write(struct.pack('>HHHH', *components))
                name_bytes = name.encode('utf-16-be')
                f.write(struct.pack('>I', len(name) + 1))  # Include null terminator
                f.write(name_bytes)
                f.write(b'\x00\x00')  # Null terminator

    def print_properties(self):
        """Print all intrinsic properties of the .ACO file."""
        print("ACO File Properties:")
        for key, value in self.properties.items():
            print(f"{key}: {value}")
        print("\nColor Swatches:")
        color_space_names = {0: "RGB", 1: "HSB", 2: "CMYK", 7: "LAB", 8: "Grayscale"}
        for i, (color_space, components, name) in enumerate(self.colors, 1):
            space_name = color_space_names.get(color_space, f"Unknown ({color_space})")
            print(f"Color {i}:")
            print(f"  Color Space: {space_name}")
            print(f"  Components: {components}")
            if name:
                print(f"  Name: {name}")

# Example usage
if __name__ == "__main__":
    # Example: Create an ACO file with sample colors
    aco = ACOFile()
    aco.colors = [
        (0, (65535, 0, 0, 0), "Red"),  # RGB Red
        (2, (0, 65535, 65535, 0), "Cyan"),  # CMYK Cyan
    ]
    aco.write("sample.aco")
    aco.print_properties()

    # Example: Read an existing ACO file
    try:
        aco = ACOFile("sample.aco")
        aco.print_properties()
    except FileNotFoundError:
        print("Sample file not found. Create an ACO file first.")

Explanation:

  • The class reads the .ACO file by parsing the binary structure: version (2 bytes), color count (2 bytes), and color entries (color space ID, components, and optional name for Version 2).
  • It supports both Version 1 and Version 2 formats, handling the transition if a Version 1 file contains a Version 2 section.
  • The write method creates a Version 2 .ACO file with the specified colors.
  • The print_properties method outputs all intrinsic properties and details of each color swatch.
  • The color components are 16-bit integers (0–65535), which are normalized from standard ranges (e.g., 0–255 for RGB) when writing.

3. Java Class for .ACO File Handling

Below is a Java class that performs the same operations as the Python class.

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ACOFile {
    private String filename;
    private int version;
    private int colorCount;
    private List<ColorEntry> colors;
    private Map<String, String> properties;

    public static class ColorEntry {
        int colorSpace;
        short[] components; // 4 components
        String name;

        public ColorEntry(int colorSpace, short[] components, String name) {
            this.colorSpace = colorSpace;
            this.components = components;
            this.name = name;
        }
    }

    public ACOFile(String filename) {
        this.filename = filename;
        this.colors = new ArrayList<>();
        this.properties = new HashMap<>();
        initializeProperties();
        if (filename != null) {
            read(filename);
        }
    }

    private void initializeProperties() {
        properties.put("File Extension", ".aco");
        properties.put("Full Name", "Adobe Photoshop Color Swatch File");
        properties.put("MIME Type", "application/x-photoshop");
        properties.put("File Type", "Binary");
        properties.put("Developer", "Adobe Systems");
        properties.put("Primary Association", "Adobe Photoshop");
        properties.put("Usage", "Stores a collection of colors for graphic design");
        properties.put("Editable", "Yes, with Adobe Photoshop or compatible software");
        properties.put("Compatibility", "Primarily Adobe Photoshop");
        properties.put("Open Standard", "No");
        properties.put("Color Models Supported", "RGB, CMYK, LAB, HSB, Grayscale, others");
        properties.put("Support for Transparency", "No");
        properties.put("Can Store Gradients", "No");
        properties.put("Embeddable", "Yes, within certain Adobe file formats");
        properties.put("Platforms Supported", "Windows, macOS");
    }

    public void read(String filename) {
        try (RandomAccessFile file = new RandomAccessFile(filename, "r")) {
            // Read version (2 bytes)
            version = file.readUnsignedShort();
            if (version != 1 && version != 2) {
                throw new IOException("Unsupported ACO version: " + version);
            }

            // Read color count (2 bytes)
            colorCount = file.readUnsignedShort();
            properties.put("Number of Colors", String.valueOf(colorCount));
            properties.put("File Size", String.valueOf(new File(filename).length()) + " bytes");

            // Read color entries
            colors.clear();
            for (int i = 0; i < colorCount; i++) {
                int colorSpace = file.readUnsignedShort();
                short[] components = new short[4];
                for (int j = 0; j < 4; j++) {
                    components[j] = file.readShort();
                }
                String name = "";
                if (version == 2) {
                    int nameLength = file.readInt();
                    byte[] nameBytes = new byte[nameLength * 2];
                    file.readFully(nameBytes);
                    name = new String(nameBytes, StandardCharsets.UTF_16BE).trim();
                }
                colors.add(new ColorEntry(colorSpace, components, name));
            }

            // Check for Version 2 section in Version 1 file
            if (version == 1 && file.getFilePointer() < file.length()) {
                version = file.readUnsignedShort();
                if (version == 2) {
                    colorCount = file.readUnsignedShort();
                    properties.put("Number of Colors", String.valueOf(colorCount));
                    colors.clear();
                    for (int i = 0; i < colorCount; i++) {
                        int colorSpace = file.readUnsignedShort();
                        short[] components = new short[4];
                        for (int j = 0; j < 4; j++) {
                            components[j] = file.readShort();
                        }
                        int nameLength = file.readInt();
                        byte[] nameBytes = new byte[nameLength * 2];
                        file.readFully(nameBytes);
                        String name = new String(nameBytes, StandardCharsets.UTF_16BE).trim();
                        colors.add(new ColorEntry(colorSpace, components, name));
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void write(String filename) {
        try (RandomAccessFile file = new RandomAccessFile(filename, "rw")) {
            // Write Version 2 header
            file.writeShort(2);
            file.writeShort(colors.size());
            for (ColorEntry color : colors) {
                file.writeShort(color.colorSpace);
                for (short component : color.components) {
                    file.writeShort(component);
                }
                byte[] nameBytes = (color.name + "\0").getBytes(StandardCharsets.UTF_16BE);
                file.writeInt(color.name.length() + 1);
                file.write(nameBytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        System.out.println("ACO File Properties:");
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("\nColor Swatches:");
        Map<Integer, String> colorSpaceNames = new HashMap<>();
        colorSpaceNames.put(0, "RGB");
        colorSpaceNames.put(1, "HSB");
        colorSpaceNames.put(2, "CMYK");
        colorSpaceNames.put(7, "LAB");
        colorSpaceNames.put(8, "Grayscale");
        for (int i = 0; i < colors.size(); i++) {
            ColorEntry color = colors.get(i);
            String spaceName = colorSpaceNames.getOrDefault(color.colorSpace, "Unknown (" + color.colorSpace + ")");
            System.out.println("Color " + (i + 1) + ":");
            System.out.println("  Color Space: " + spaceName);
            System.out.print("  Components: [");
            for (int j = 0; j < color.components.length; j++) {
                System.out.print(color.components[j]);
                if (j < color.components.length - 1) System.out.print(", ");
            }
            System.out.println("]");
            if (!color.name.isEmpty()) {
                System.out.println("  Name: " + color.name);
            }
        }
    }

    public static void main(String[] args) {
        // Example: Create an ACO file
        ACOFile aco = new ACOFile(null);
        aco.colors.add(new ColorEntry(0, new short[]{65535, 0, 0, 0}, "Red"));
        aco.colors.add(new ColorEntry(2, new short[]{0, 65535, 65535, 0}, "Cyan"));
        aco.write("sample.aco");
        aco.printProperties();

        // Example: Read an existing ACO file
        aco = new ACOFile("sample.aco");
        aco.printProperties();
    }
}

Explanation:

  • The Java class uses RandomAccessFile for binary file operations, reading and writing in big-endian byte order.
  • It handles both Version 1 and Version 2 formats, including the optional Version 2 section in Version 1 files.
  • The ColorEntry inner class stores color space, components, and name.
  • The printProperties method outputs all intrinsic properties and color details.
  • Error handling is included for file operations and unsupported versions.

4. JavaScript Class for .ACO File Handling

Below is a JavaScript class that handles .ACO files using Node.js for file system access.

const fs = require('fs');

class ACOFile {
    constructor(filename = null) {
        this.filename = filename;
        this.version = 0;
        this.colorCount = 0;
        this.colors = []; // Array of {colorSpace, components, name}
        this.properties = {
            "File Extension": ".aco",
            "Full Name": "Adobe Photoshop Color Swatch File",
            "MIME Type": "application/x-photoshop",
            "File Type": "Binary",
            "Developer": "Adobe Systems",
            "Primary Association": "Adobe Photoshop",
            "Usage": "Stores a collection of colors for graphic design",
            "Editable": "Yes, with Adobe Photoshop or compatible software",
            "Compatibility": "Primarily Adobe Photoshop",
            "Open Standard": "No",
            "Color Models Supported": "RGB, CMYK, LAB, HSB, Grayscale, others",
            "Support for Transparency": "No",
            "Can Store Gradients": "No",
            "Embeddable": "Yes, within certain Adobe file formats",
            "Platforms Supported": "Windows, macOS"
        };
        if (filename) {
            this.read(filename);
        }
    }

    read(filename) {
        const buffer = fs.readFileSync(filename);
        let offset = 0;

        // Read version (2 bytes)
        this.version = buffer.readUInt16BE(offset);
        offset += 2;
        if (this.version !== 1 && this.version !== 2) {
            throw new Error(`Unsupported ACO version: ${this.version}`);
        }

        // Read color count (2 bytes)
        this.colorCount = buffer.readUInt16BE(offset);
        offset += 2;
        this.properties["Number of Colors"] = this.colorCount.toString();
        this.properties["File Size"] = fs.statSync(filename).size + " bytes";

        // Read color entries
        this.colors = [];
        for (let i = 0; i < this.colorCount; i++) {
            const colorSpace = buffer.readUInt16BE(offset);
            offset += 2;
            const components = [
                buffer.readUInt16BE(offset),
                buffer.readUInt16BE(offset + 2),
                buffer.readUInt16BE(offset + 4),
                buffer.readUInt16BE(offset + 6)
            ];
            offset += 8;
            let name = "";
            if (this.version === 2) {
                const nameLength = buffer.readUInt32BE(offset);
                offset += 4;
                name = buffer.slice(offset, offset + nameLength * 2).toString('utf16le').replace(/\x00+$/, '');
                offset += nameLength * 2;
            }
            this.colors.push({ colorSpace, components, name });
        }

        // Check for Version 2 section in Version 1 file
        if (this.version === 1 && offset < buffer.length) {
            this.version = buffer.readUInt16BE(offset);
            offset += 2;
            if (this.version === 2) {
                this.colorCount = buffer.readUInt16BE(offset);
                offset += 2;
                this.properties["Number of Colors"] = this.colorCount.toString();
                this.colors = [];
                for (let i = 0; i < this.colorCount; i++) {
                    const colorSpace = buffer.readUInt16BE(offset);
                    offset += 2;
                    const components = [
                        buffer.readUInt16BE(offset),
                        buffer.readUInt16BE(offset + 2),
                        buffer.readUInt16BE(offset + 4),
                        buffer.readUInt16BE(offset + 6)
                    ];
                    offset += 8;
                    const nameLength = buffer.readUInt32BE(offset);
                    offset += 4;
                    const name = buffer.slice(offset, offset + nameLength * 2).toString('utf16le').replace(/\x00+$/, '');
                    offset += nameLength * 2;
                    this.colors.push({ colorSpace, components, name });
                }
            }
        }
    }

    write(filename) {
        const buffer = Buffer.alloc(1024 * 1024); // Allocate large enough buffer
        let offset = 0;

        // Write Version 2 header
        buffer.writeUInt16BE(2, offset);
        offset += 2;
        buffer.writeUInt16BE(this.colors.length, offset);
        offset += 2;

        // Write color entries
        for (const color of this.colors) {
            buffer.writeUInt16BE(color.colorSpace, offset);
            offset += 2;
            for (const component of color.components) {
                buffer.writeUInt16BE(component, offset);
                offset += 2;
            }
            const nameBuffer = Buffer.from(color.name + '\0', 'utf16le');
            buffer.writeUInt32BE((color.name.length + 1), offset);
            offset += 4;
            nameBuffer.copy(buffer, offset);
            offset += nameBuffer.length;
        }

        fs.writeFileSync(filename, buffer.slice(0, offset));
    }

    printProperties() {
        console.log("ACO File Properties:");
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
        console.log("\nColor Swatches:");
        const colorSpaceNames = {
            0: "RGB",
            1: "HSB",
            2: "CMYK",
            7: "LAB",
            8: "Grayscale"
        };
        this.colors.forEach((color, index) => {
            const spaceName = colorSpaceNames[color.colorSpace] || `Unknown (${color.colorSpace})`;
            console.log(`Color ${index + 1}:`);
            console.log(`  Color Space: ${spaceName}`);
            console.log(`  Components: [${color.components.join(', ')}]`);
            if (color.name) {
                console.log(`  Name: ${color.name}`);
            }
        });
    }
}

// Example usage
const aco = new ACOFile();
aco.colors = [
    { colorSpace: 0, components: [65535, 0, 0, 0], name: "Red" },
    { colorSpace: 2, components: [0, 65535, 65535, 0], name: "Cyan" }
];
aco.write("sample.aco");
aco.printProperties();

// Read an existing ACO file
try {
    const aco2 = new ACOFile("sample.aco");
    aco2.printProperties();
} catch (e) {
    console.error("Error reading file:", e.message);
}

Explanation:

  • The JavaScript class uses Node.js fs module for file operations and Buffer for binary data handling.
  • It supports both Version 1 and Version 2 formats, with proper handling of UTF-16LE encoded names.
  • The write method creates a Version 2 .ACO file.
  • The printProperties method outputs all properties and color details.
  • Note: This code requires Node.js to run due to file system operations.

5. C Class for .ACO File Handling

In C, there is no direct equivalent to a "class," so we use a struct with associated functions to achieve the same functionality.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define MAX_COLORS 1000
#define MAX_NAME_LEN 256

typedef struct {
    uint16_t colorSpace;
    uint16_t components[4];
    char name[MAX_NAME_LEN];
} ColorEntry;

typedef struct {
    char* filename;
    uint16_t version;
    uint16_t colorCount;
    ColorEntry colors[MAX_COLORS];
    // Properties as key-value pairs (simplified as array for C)
    struct {
        const char* key;
        const char* value;
    } properties[15];
    int propCount;
} ACOFile;

void initACOFile(ACOFile* aco, const char* filename) {
    aco->filename = filename ? strdup(filename) : NULL;
    aco->version = 0;
    aco->colorCount = 0;
    aco->propCount = 0;

    // Initialize properties
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"File Extension", ".aco"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Full Name", "Adobe Photoshop Color Swatch File"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"MIME Type", "application/x-photoshop"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"File Type", "Binary"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Developer", "Adobe Systems"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Primary Association", "Adobe Photoshop"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Usage", "Stores a collection of colors for graphic design"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Editable", "Yes, with Adobe Photoshop or compatible software"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Compatibility", "Primarily Adobe Photoshop"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Open Standard", "No"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Color Models Supported", "RGB, CMYK, LAB, HSB, Grayscale, others"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Support for Transparency", "No"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Can Store Gradients", "No"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Embeddable", "Yes, within certain Adobe file formats"};
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Platforms Supported", "Windows, macOS"};
}

void readACOFile(ACOFile* aco, const char* filename) {
    FILE* file = fopen(filename, "rb");
    if (!file) {
        printf("Error opening file: %s\n", filename);
        return;
    }

    // Read version (2 bytes)
    fread(&aco->version, sizeof(uint16_t), 1, file);
    aco->version = __builtin_bswap16(aco->version); // Convert big-endian to host
    if (aco->version != 1 && aco->version != 2) {
        printf("Unsupported ACO version: %d\n", aco->version);
        fclose(file);
        return;
    }

    // Read color count (2 bytes)
    fread(&aco->colorCount, sizeof(uint16_t), 1, file);
    aco->colorCount = __builtin_bswap16(aco->colorCount);
    char buf[32];
    snprintf(buf, sizeof(buf), "%d", aco->colorCount);
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"Number of Colors", strdup(buf)};
    
    fseek(file, 0, SEEK_END);
    long fileSize = ftell(file);
    rewind(file);
    snprintf(buf, sizeof(buf), "%ld bytes", fileSize);
    aco->properties[aco->propCount++] = (struct { const char* key; const char* value; }){"File Size", strdup(buf)};

    // Read color entries
    aco->colorCount = 0;
    for (int i = 0; i < aco->colorCount; i++) {
        fread(&aco->colors[i].colorSpace, sizeof(uint16_t), 1, file);
        aco->colors[i].colorSpace = __builtin_bswap16(aco->colors[i].colorSpace);
        for (int j = 0; j < 4; j++) {
            fread(&aco->colors[i].components[j], sizeof(uint16_t), 1, file);
            aco->colors[i].components[j] = __builtin_bswap16(aco->colors[i].components[j]);
        }
        aco->colors[i].name[0] = '\0';
        if (aco->version == 2) {
            uint32_t nameLength;
            fread(&nameLength, sizeof(uint32_t), 1, file);
            nameLength = __builtin_bswap32(nameLength);
            uint16_t nameBuf[MAX_NAME_LEN];
            fread(nameBuf, sizeof(uint16_t), nameLength, file);
            for (int j = 0, k = 0; j < nameLength && k < MAX_NAME_LEN - 1; j++) {
                aco->colors[i].name[k++] = (char)(__builtin_bswap16(nameBuf[j]) & 0xFF);
                aco->colors[i].name[k] = '\0';
            }
        }
        aco->colorCount++;
    }

    // Check for Version 2 section in Version 1 file
    if (aco->version == 1 && ftell(file) < fileSize) {
        fread(&aco->version, sizeof(uint16_t), 1, file);
        aco->version = __builtin_bswap16(aco->version);
        if (aco->version == 2) {
            fread(&aco->colorCount, sizeof(uint16_t), 1, file);
            aco->colorCount = __builtin_bswap16(aco->colorCount);
            snprintf(buf, sizeof(buf), "%d", aco->colorCount);
            aco->properties[aco->propCount - 2] = (struct { const char* key; const char* value; }){"Number of Colors", strdup(buf)};
            aco->colorCount = 0;
            for (int i = 0; i < aco->colorCount; i++) {
                fread(&aco->colors[i].colorSpace, sizeof(uint16_t), 1, file);
                aco->colors[i].colorSpace = __builtin_bswap16(aco->colors[i].colorSpace);
                for (int j = 0; j < 4; j++) {
                    fread(&aco->colors[i].components[j], sizeof(uint16_t), 1, file);
                    aco->colors[i].components[j] = __builtin_bswap16(aco->colors[i].components[j]);
                }
                uint32_t nameLength;
                fread(&nameLength, sizeof(uint32_t), 1, file);
                nameLength = __builtin_bswap32(nameLength);
                uint16_t nameBuf[MAX_NAME_LEN];
                fread(nameBuf, sizeof(uint16_t), nameLength, file);
                for (int j = 0, k = 0; j < nameLength && k < MAX_NAME_LEN - 1; j++) {
                    aco->colors[i].name[k++] = (char)(__builtin_bswap16(nameBuf[j]) & 0xFF);
                    aco->colors[i].name[k] = '\0';
                }
                aco->colorCount++;
            }
        }
    }

    fclose(file);
}

void writeACOFile(ACOFile* aco, const char* filename) {
    FILE* file = fopen(filename, "wb");
    if (!file) {
        printf("Error opening file for writing: %s\n", filename);
        return;
    }

    // Write Version 2 header
    uint16_t version = __builtin_bswap16(2);
    fwrite(&version, sizeof(uint16_t), 1, file);
    uint16_t colorCount = __builtin_bswap16(aco->colorCount);
    fwrite(&colorCount, sizeof(uint16_t), 1, file);

    // Write color entries
    for (int i = 0; i < aco->colorCount; i++) {
        uint16_t colorSpace = __builtin_bswap16(aco->colors[i].colorSpace);
        fwrite(&colorSpace, sizeof(uint16_t), 1, file);
        for (int j = 0; j < 4; j++) {
            uint16_t component = __builtin_bswap16(aco->colors[i].components[j]);
            fwrite(&component, sizeof(uint16_t), 1, file);
        }
        uint32_t nameLength = strlen(aco->colors[i].name) + 1;
        nameLength = __builtin_bswap32(nameLength);
        fwrite(&nameLength, sizeof(uint32_t), 1, file);
        uint16_t nameBuf[MAX_NAME_LEN];
        for (int j = 0; j < strlen(aco->colors[i].name) + 1; j++) {
            nameBuf[j] = __builtin_bswap16((uint16_t)aco->colors[i].name[j]);
        }
        fwrite(nameBuf, sizeof(uint16_t), strlen(aco->colors[i].name) + 1, file);
    }

    fclose(file);
}

void printACOFileProperties(ACOFile* aco) {
    printf("ACO File Properties:\n");
    for (int i = 0; i < aco->propCount; i++) {
        printf("%s: %s\n", aco->properties[i].key, aco->properties[i].value);
    }
    printf("\nColor Swatches:\n");
    const char* colorSpaceNames[] = {"RGB", "HSB", "CMYK", "", "", "", "", "LAB", "Grayscale"};
    for (int i = 0; i < aco->colorCount; i++) {
        const char* spaceName = aco->colors[i].colorSpace < 9 ? colorSpaceNames[aco->colors[i].colorSpace] : "Unknown";
        printf("Color %d:\n", i + 1);
        printf("  Color Space: %s (%d)\n", spaceName, aco->colors[i].colorSpace);
        printf("  Components: [%d, %d, %d, %d]\n", 
               aco->colors[i].components[0], aco->colors[i].components[1],
               aco->colors[i].components[2], aco->colors[i].components[3]);
        if (aco->colors[i].name[0]) {
            printf("  Name: %s\n", aco->colors[i].name);
        }
    }
}

void freeACOFile(ACOFile* aco) {
    if (aco->filename) free((void*)aco->filename);
    for (int i = 0; i < aco->propCount; i++) {
        if (i >= 15 || !aco->properties[i].value) continue;
        if (strcmp(aco->properties[i].key, "Number of Colors") == 0 ||
            strcmp(aco->properties[i].key, "File Size") == 0) {
            free((void*)aco->properties[i].value);
        }
    }
}

int main() {
    // Example: Create an ACO file
    ACOFile aco;
    initACOFile(&aco, NULL);
    aco.colorCount = 2;
    aco.colors[0].colorSpace = 0;
    aco.colors[0].components[0] = 65535;
    aco.colors[0].components[1] = 0;
    aco.colors[0].components[2] = 0;
    aco.colors[0].components[3] = 0;
    strcpy(aco.colors[0].name, "Red");
    aco.colors[1].colorSpace = 2;
    aco.colors[1].components[0] = 0;
    aco.colors[1].components[1] = 65535;
    aco.colors[1].components[2] = 65535;
    aco.colors[1].components[3] = 0;
    strcpy(aco.colors[1].name, "Cyan");
    writeACOFile(&aco, "sample.aco");
    printACOFileProperties(&aco);

    // Example: Read an existing ACO file
    ACOFile aco2;
    initACOFile(&aco2, "sample.aco");
    readACOFile(&aco2, "sample.aco");
    printACOFileProperties(&aco2);
    freeACOFile(&aco2);

    freeACOFile(&aco);
    return 0;
}

Explanation:

  • The C implementation uses a struct to represent the ACO file and its colors.
  • It handles binary file operations with fopen, fread, and fwrite, converting big-endian values to host byte order using __builtin_bswap16 and __builtin_bswap32.
  • The printACOFileProperties function outputs all properties and color details.
  • Memory management is handled carefully, with dynamic allocation for strings and cleanup in freeACOFile.
  • The code

1. List of Properties of the .ACO File Format

The .ACO file format (Adobe Color Swatch) is a binary format for storing color palettes, using big-endian 16-bit unsigned integers (uint16) throughout. It supports two versions: Version 1 (mandatory for compatibility) and Version 2 (optional, adds color names). Files typically contain a Version 1 section followed optionally by a Version 2 section. The intrinsic properties (structural fields) are:

  • Sections: One or two sequential sections (Version 1, optionally followed by Version 2).
  • Version (per section): uint16 (1 for Version 1, 2 for Version 2).
  • Number of Colors (per section): uint16 (count of colors in the section; must match between sections if both present).
  • Colors (per section): Array of color entries (one per color count).
  • Color Space ID (per color): uint16 (0=RGB, 1=HSB/HSV, 2=CMYK, 7=Lab, 8=Grayscale, 9=Wide CMYK).
  • Component w (per color): uint16 (first color component; interpretation depends on space, e.g., R for RGB).
  • Component x (per color): uint16 (second color component; e.g., G for RGB).
  • Component y (per color): uint16 (third color component; e.g., B for RGB).
  • Component z (per color): uint16 (fourth color component; e.g., K for CMYK; often 0 for others).
  • Separator (per color, Version 2 only): uint16 (must be 0).
  • Name Length (per color, Version 2 only): uint16 (number of UTF-16 code units in the name + 1 for null terminator).
  • Name (per color, Version 2 only): Sequence of uint16 code units (UTF-16 big-endian characters, length as above, null-terminated).

These properties define the file's binary structure. No external filesystem metadata (e.g., timestamps) is intrinsic to the format itself.

2. Python Class

import struct
import os

class ACOFile:
    def __init__(self):
        self.colors = []  # List of dicts: {'space': int, 'components': [int, int, int, int], 'name': str}

    def read_uint16(self, f):
        return struct.unpack('>H', f.read(2))[0]

    def write_uint16(self, f, value):
        f.write(struct.pack('>H', value))

    def load(self, filename):
        with open(filename, 'rb') as f:
            # Read Version 1 section
            version = self.read_uint16(f)
            if version != 1:
                raise ValueError("Invalid ACO file: expected version 1")
            num_colors = self.read_uint16(f)
            self.colors = []
            for _ in range(num_colors):
                space = self.read_uint16(f)
                components = [self.read_uint16(f) for _ in range(4)]
                self.colors.append({'space': space, 'components': components, 'name': ''})

            # Check for Version 2 section
            try:
                version = self.read_uint16(f)
                if version == 2:
                    num_colors_v2 = self.read_uint16(f)
                    if num_colors_v2 != num_colors:
                        raise ValueError("Mismatched color count in version 2")
                    for i in range(num_colors):
                        space = self.read_uint16(f)  # Should match v1
                        components = [self.read_uint16(f) for _ in range(4)]  # Should match v1
                        separator = self.read_uint16(f)
                        if separator != 0:
                            raise ValueError("Invalid separator in version 2")
                        name_len = self.read_uint16(f)
                        name_codes = [self.read_uint16(f) for _ in range(name_len)]
                        if name_codes[-1] != 0:
                            raise ValueError("Name not null-terminated")
                        name = ''.join(chr(c) for c in name_codes[:-1])  # UTF-16 to str (basic)
                        self.colors[i]['name'] = name
            except EOFError:
                pass  # No version 2, that's fine

    def save(self, filename):
        with open(filename, 'wb') as f:
            # Write Version 1 section
            self.write_uint16(f, 1)
            self.write_uint16(f, len(self.colors))
            for color in self.colors:
                self.write_uint16(f, color['space'])
                for comp in color['components']:
                    self.write_uint16(f, comp)

            # Write Version 2 section if any names are present
            has_names = any(color['name'] for color in self.colors)
            if has_names:
                self.write_uint16(f, 2)
                self.write_uint16(f, len(self.colors))
                for color in self.colors:
                    self.write_uint16(f, color['space'])
                    for comp in color['components']:
                        self.write_uint16(f, comp)
                    self.write_uint16(f, 0)  # Separator
                    name = color['name']
                    name_codes = [ord(c) for c in name] + [0]  # Basic str to UTF-16
                    self.write_uint16(f, len(name_codes))
                    for code in name_codes:
                        self.write_uint16(f, code)

3. Java Class

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class ACOFile {
    public static class Color {
        int space;
        int[] components = new int[4];
        String name = "";
    }

    private List<Color> colors = new ArrayList<>();

    public void load(String filename) throws IOException {
        try (DataInputStream dis = new DataInputStream(new FileInputStream(filename))) {
            // Read Version 1
            int version = dis.readUnsignedShort();
            if (version != 1) {
                throw new IOException("Invalid ACO file: expected version 1");
            }
            int numColors = dis.readUnsignedShort();
            colors.clear();
            for (int i = 0; i < numColors; i++) {
                Color color = new Color();
                color.space = dis.readUnsignedShort();
                for (int j = 0; j < 4; j++) {
                    color.components[j] = dis.readUnsignedShort();
                }
                colors.add(color);
            }

            // Check for Version 2
            try {
                version = dis.readUnsignedShort();
                if (version == 2) {
                    int numColorsV2 = dis.readUnsignedShort();
                    if (numColorsV2 != numColors) {
                        throw new IOException("Mismatched color count in version 2");
                    }
                    for (int i = 0; i < numColors; i++) {
                        dis.readUnsignedShort(); // space, skip (assume match)
                        for (int j = 0; j < 4; j++) {
                            dis.readUnsignedShort(); // components, skip
                        }
                        int separator = dis.readUnsignedShort();
                        if (separator != 0) {
                            throw new IOException("Invalid separator in version 2");
                        }
                        int nameLen = dis.readUnsignedShort();
                        StringBuilder nameBuilder = new StringBuilder();
                        for (int k = 0; k < nameLen - 1; k++) {
                            nameBuilder.append((char) dis.readUnsignedShort());
                        }
                        int nullTerm = dis.readUnsignedShort();
                        if (nullTerm != 0) {
                            throw new IOException("Name not null-terminated");
                        }
                        colors.get(i).name = nameBuilder.toString();
                    }
                }
            } catch (EOFException e) {
                // No version 2
            }
        }
    }

    public void save(String filename) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(filename))) {
            // Write Version 1
            dos.writeShort(1);
            dos.writeShort(colors.size());
            for (Color color : colors) {
                dos.writeShort(color.space);
                for (int comp : color.components) {
                    dos.writeShort(comp);
                }
            }

            // Write Version 2 if any names
            boolean hasNames = colors.stream().anyMatch(c -> !c.name.isEmpty());
            if (hasNames) {
                dos.writeShort(2);
                dos.writeShort(colors.size());
                for (Color color : colors) {
                    dos.writeShort(color.space);
                    for (int comp : color.components) {
                        dos.writeShort(comp);
                    }
                    dos.writeShort(0); // Separator
                    String name = color.name;
                    dos.writeShort(name.length() + 1);
                    for (char c : name.toCharArray()) {
                        dos.writeShort(c);
                    }
                    dos.writeShort(0); // Null terminator
                }
            }
        }
    }
}

4. Javascript Class

const fs = require('fs');

class ACOFile {
    constructor() {
        this.colors = []; // Array of {space: number, components: [number, number, number, number], name: string}
    }

    load(filename) {
        const buffer = fs.readFileSync(filename);
        let offset = 0;

        // Read Version 1
        const version = buffer.readUInt16BE(offset); offset += 2;
        if (version !== 1) {
            throw new Error('Invalid ACO file: expected version 1');
        }
        const numColors = buffer.readUInt16BE(offset); offset += 2;
        this.colors = [];
        for (let i = 0; i < numColors; i++) {
            const space = buffer.readUInt16BE(offset); offset += 2;
            const components = [];
            for (let j = 0; j < 4; j++) {
                components.push(buffer.readUInt16BE(offset)); offset += 2;
            }
            this.colors.push({space, components, name: ''});
        }

        // Check for Version 2
        if (offset < buffer.length) {
            const version2 = buffer.readUInt16BE(offset); offset += 2;
            if (version2 === 2) {
                const numColorsV2 = buffer.readUInt16BE(offset); offset += 2;
                if (numColorsV2 !== numColors) {
                    throw new Error('Mismatched color count in version 2');
                }
                for (let i = 0; i < numColors; i++) {
                    offset += 2; // space, skip
                    offset += 8; // components, skip
                    const separator = buffer.readUInt16BE(offset); offset += 2;
                    if (separator !== 0) {
                        throw new Error('Invalid separator in version 2');
                    }
                    const nameLen = buffer.readUInt16BE(offset); offset += 2;
                    let name = '';
                    for (let k = 0; k < nameLen - 1; k++) {
                        name += String.fromCharCode(buffer.readUInt16BE(offset)); offset += 2;
                    }
                    const nullTerm = buffer.readUInt16BE(offset); offset += 2;
                    if (nullTerm !== 0) {
                        throw new Error('Name not null-terminated');
                    }
                    this.colors[i].name = name;
                }
            }
        }
    }

    save(filename) {
        let bufferSize = 4 + this.colors.length * 10; // Version 1 header + colors
        let hasNames = this.colors.some(c => c.name.length > 0);
        if (hasNames) {
            bufferSize += 4; // Version 2 header
            for (const color of this.colors) {
                bufferSize += 10; // space + components
                bufferSize += 4; // separator + nameLen
                bufferSize += (color.name.length + 1) * 2; // name + null
            }
        }
        const buffer = Buffer.alloc(bufferSize);
        let offset = 0;

        // Write Version 1
        buffer.writeUInt16BE(1, offset); offset += 2;
        buffer.writeUInt16BE(this.colors.length, offset); offset += 2;
        for (const color of this.colors) {
            buffer.writeUInt16BE(color.space, offset); offset += 2;
            for (const comp of color.components) {
                buffer.writeUInt16BE(comp, offset); offset += 2;
            }
        }

        // Write Version 2 if needed
        if (hasNames) {
            buffer.writeUInt16BE(2, offset); offset += 2;
            buffer.writeUInt16BE(this.colors.length, offset); offset += 2;
            for (const color of this.colors) {
                buffer.writeUInt16BE(color.space, offset); offset += 2;
                for (const comp of color.components) {
                    buffer.writeUInt16BE(comp, offset); offset += 2;
                }
                buffer.writeUInt16BE(0, offset); offset += 2; // Separator
                const nameLen = color.name.length + 1;
                buffer.writeUInt16BE(nameLen, offset); offset += 2;
                for (let i = 0; i < color.name.length; i++) {
                    buffer.writeUInt16BE(color.name.charCodeAt(i), offset); offset += 2;
                }
                buffer.writeUInt16BE(0, offset); offset += 2; // Null
            }
        }

        fs.writeFileSync(filename, buffer);
    }
}

5. C Class (Implemented as C++ Class for Object-Oriented Support)

#include <fstream>
#include <vector>
#include <string>
#include <stdexcept>
#include <cstdint>

class ACOFile {
public:
    struct Color {
        uint16_t space;
        uint16_t components[4];
        std::string name;
    };

    std::vector<Color> colors;

    uint16_t read_uint16(std::ifstream& f) {
        uint8_t bytes[2];
        f.read(reinterpret_cast<char*>(bytes), 2);
        if (f.eof()) throw std::runtime_error("Unexpected EOF");
        return (bytes[0] << 8) | bytes[1];
    }

    void write_uint16(std::ofstream& f, uint16_t value) {
        uint8_t bytes[2] = {static_cast<uint8_t>(value >> 8), static_cast<uint8_t>(value & 0xFF)};
        f.write(reinterpret_cast<char*>(bytes), 2);
    }

    void load(const std::string& filename) {
        std::ifstream f(filename, std::ios::binary);
        if (!f) throw std::runtime_error("Failed to open file");

        // Read Version 1
        uint16_t version = read_uint16(f);
        if (version != 1) throw std::runtime_error("Invalid ACO file: expected version 1");
        uint16_t num_colors = read_uint16(f);
        colors.clear();
        colors.reserve(num_colors);
        for (uint16_t i = 0; i < num_colors; ++i) {
            Color color;
            color.space = read_uint16(f);
            for (int j = 0; j < 4; ++j) {
                color.components[j] = read_uint16(f);
            }
            color.name = "";
            colors.push_back(color);
        }

        // Check for Version 2
        if (!f.eof()) {
            version = read_uint16(f);
            if (version == 2) {
                uint16_t num_colors_v2 = read_uint16(f);
                if (num_colors_v2 != num_colors) throw std::runtime_error("Mismatched color count in version 2");
                for (uint16_t i = 0; i < num_colors; ++i) {
                    read_uint16(f); // space, skip
                    for (int j = 0; j < 4; ++j) {
                        read_uint16(f); // components, skip
                    }
                    uint16_t separator = read_uint16(f);
                    if (separator != 0) throw std::runtime_error("Invalid separator in version 2");
                    uint16_t name_len = read_uint16(f);
                    std::string name;
                    for (uint16_t k = 0; k < name_len - 1; ++k) {
                        uint16_t code = read_uint16(f);
                        name += static_cast<char>(code); // Basic ASCII assumption; for full UTF-16, use wchar_t
                    }
                    uint16_t null_term = read_uint16(f);
                    if (null_term != 0) throw std::runtime_error("Name not null-terminated");
                    colors[i].name = name;
                }
            }
        }
    }

    void save(const std::string& filename) {
        std::ofstream f(filename, std::ios::binary);
        if (!f) throw std::runtime_error("Failed to open file");

        // Write Version 1
        write_uint16(f, 1);
        write_uint16(f, static_cast<uint16_t>(colors.size()));
        for (const auto& color : colors) {
            write_uint16(f, color.space);
            for (int j = 0; j < 4; ++j) {
                write_uint16(f, color.components[j]);
            }
        }

        // Write Version 2 if any names
        bool has_names = false;
        for (const auto& color : colors) {
            if (!color.name.empty()) {
                has_names = true;
                break;
            }
        }
        if (has_names) {
            write_uint16(f, 2);
            write_uint16(f, static_cast<uint16_t>(colors.size()));
            for (const auto& color : colors) {
                write_uint16(f, color.space);
                for (int j = 0; j < 4; ++j) {
                    write_uint16(f, color.components[j]);
                }
                write_uint16(f, 0); // Separator
                uint16_t name_len = static_cast<uint16_t>(color.name.size() + 1);
                write_uint16(f, name_len);
                for (char c : color.name) {
                    write_uint16(f, static_cast<uint16_t>(static_cast<unsigned char>(c))); // Basic
                }
                write_uint16(f, 0); // Null
            }
        }
    }
};