Expanding Nippon Colors: Supporting Adobe ASE Export

In our quest to make Nippon Colors, our iOS app showcasing Japanese traditional colors, more versatile for designers, we added support for exporting palettes as Adobe ASE (Adobe Swatch Exchange) files. ASE files store color data in a binary format, enabling seamless integration with professional tools like Photoshop and Illustrator. This feature allows users to bring the elegance of Japanese aesthetics into their creative workflows. However, implementing ASE export introduced us to the complexities of binary file formats and taught us valuable lessons about data encoding. This post chronicles the challenges we faced, how we analyzed them, and the solution we crafted to deliver a robust ASE export feature.

Advertisement

The Initial Challenge: Why ASE Files?

Our app already supported exporting palettes as Procreate® .swatches files, which was great for digital artists. However, many users requested compatibility with Adobe’s ecosystem, a staple for graphic designers. ASE files were the perfect solution—they’re widely supported across Adobe tools and can store color data in a structured binary format. Unlike the JSON-based Procreate® export, ASE required constructing a binary file, which presented unexpected hurdles.

The first version of our export crashed silently or produced files that Adobe tools rejected. The error messages were cryptic, and the files either failed to load or displayed incorrect colors. Clearly, something was amiss in how we were generating the ASE data.

Unpacking the Problem: What Went Wrong?

The crashes and corrupted files pointed to issues in our binary data construction. Here’s what we observed:

1. Silent Failures in File Generation

Our initial implementation wrote color data directly to a Data buffer, but Adobe tools either refused to open the resulting .ase file or crashed when loading it. There were no obvious runtime errors in Xcode, making it hard to pinpoint the issue.

2. Incorrect Colors in Adobe Tools

When we generated a file that opened, the colors were wrong—Japanese “Sakura” pink appeared as a muddy green! This suggested a problem with how we encoded RGB values or color spaces.

3. File Size Discrepancies

The generated ASE files were larger than expected compared to sample ASE files from Photoshop. This hinted at incorrect block lengths or redundant data.

To diagnose these issues, we used a hex editor (Hex Fiend) to compare our output against Photoshop-exported files and added detailed debug logs to track byte sequences.

Digging Deeper: Analyzing the ASE Format

To resolve these issues, we studied the ASE format’s specification (referenced from Adobe’s community forums and sample files, as no official public spec exists). ASE files are binary, with a strict structure:

Section Description Example Data
Header Signature “ASEF” + version (1.0) ASEF 0x0001 0x0000
Block Count 32-bit integer for total blocks 0x00000003
Group Start 0xc001 + name length + UTF-16BE name 0xc001 0x0000000E ...
Color Block 0x0001 + name + color space + values + type 0x0001 ... RGB ...
Group End 0xc002 + zero length 0xc002 0x00000000

Our initial assumptions were overly simplistic. We identified three key issues:

1. Improper Byte Order

Swift’s Data type doesn’t handle endianness automatically. We appended integers in the host’s native byte order, but ASE requires big-endian for all multi-byte values (e.g., UInt16, UInt32, Float32). This caused misaligned data, leading to corrupted files.

2. UTF-16 Encoding for Names

Color and group names must be encoded in UTF-16 big-endian, with a leading character count. We initially used UTF-8, which broke the file structure and caused Adobe tools to misread names, resulting in crashes or blank palettes.

3. Incorrect Block Length Calculations

Each block requires a precise length field. We miscalculated these by omitting the character count or including extra bytes, which threw off parsing and caused Adobe tools to reject the file.

Crafting the Solution: Building a Robust ASE Export

Fixing these issues required a methodical approach. Here’s how we built a reliable ASE export:

1. Mastering Big-Endian Encoding

We ensured all multi-byte values were converted to big-endian before appending to the Data buffer:

let majorVersion = UInt16(1).bigEndian
data.append(withUnsafeBytes(of: majorVersion) { Data($0) })

This aligned integers and floats with the ASE specification.

2. Proper UTF-16 Name Encoding

We switched to UTF-16 big-endian for names, carefully calculating character counts and block lengths:

let safeName = groupName.isEmpty ? "Untitled Palette" : groupName
guard let nameData = safeName.data(using: .utf16BigEndian) else { return Data() }
let nameCharCount = UInt16(safeName.utf16.count)

This ensured names were correctly interpreted, preventing crashes.

3. Precise Block Structure

We constructed each block—group start, color entries, and group end—verifying lengths at every step:

  • Group Start: 0xc001 + name length + name data.
  • Color Entry: 0x0001 + name length + name data + “RGB “ + RGB floats + color type.
  • Group End: 0xc002 + zero length.

Debug logs tracked byte counts:

print("ASE generated, bytes: \(data.count), preview: \(data.prefix(64).hexString)")

4. Validating with Sample Files

We exported a test palette from Photoshop, analyzed it in a hex editor, and compared it byte-for-byte with our output. This revealed issues like missing null terminators, which we corrected.

The Final ASE Generation Code

Below is the generateASEData method from our PalettesView.swift. It assumes a PaletteColor struct with properties like japaneseName (e.g., “Sakura”) and rgbHexString (e.g., “#F5A9B8”):

/// Generates ASE data for a list of colors.
/// - Parameters:
///   - colors: The colors to include.
///   - groupName: The name of the color group.
/// - Returns: Binary data in ASE format.
func generateASEData(colors: [PaletteColor], groupName: String) -> Data {
    var data = Data()
    print("Generating ASE for group: \(groupName), colors: \(colors.count)")
    
    // Header
    data.append("ASEF".data(using: .ascii)!)
    
    // Version 1.0
    let majorVersion = UInt16(1).bigEndian
    let minorVersion = UInt16(0).bigEndian
    data.append(withUnsafeBytes(of: majorVersion) { Data($0) })
    data.append(withUnsafeBytes(of: minorVersion) { Data($0) })
    
    // Block count
    let blockCount = UInt32(colors.count + 2).bigEndian
    data.append(withUnsafeBytes(of: blockCount) { Data($0) })
    
    // Group start
    let groupStart = UInt16(0xc001).bigEndian
    data.append(withUnsafeBytes(of: groupStart) { Data($0) })
    
    let safeName = groupName.isEmpty ? "Untitled Palette" : groupName
    guard let nameData = safeName.data(using: .utf16BigEndian) else {
        print("Error: Failed to encode group name: \(safeName)")
        return Data()
    }
    let nameCharCount = UInt16(safeName.utf16.count)
    let groupBlockLength = UInt32(2 + nameData.count).bigEndian
    data.append(withUnsafeBytes(of: groupBlockLength) { Data($0) })
    data.append(withUnsafeBytes(of: nameCharCount.bigEndian) { Data($0) })
    data.append(nameData)
    print("Group start: name=\(safeName), chars=\(nameCharCount), length=\(groupBlockLength)")
    
    // Colors
    for (index, color) in colors.enumerated() {
        guard let hex = color.rgbHexString,
              let name = color.japaneseName,
              !name.isEmpty,
              let nameData = name.data(using: .utf16BigEndian) else {
            print("Skipping color \(index): invalid hex or name")
            continue
        }
        
        print("Processing color \(index): name=\(name), hex=\(hex)")
        
        let uiColor = UIColor(hex: hex)
        var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
        
        let nameCharCount = UInt16(name.utf16.count)
        let blockLength = UInt32(2 + nameData.count + 4 + 12 + 2).bigEndian
        
        let colorEntry = UInt16(0x0001).bigEndian
        data.append(withUnsafeBytes(of: colorEntry) { Data($0) })
        data.append(withUnsafeBytes(of: blockLength) { Data($0) })
        data.append(withUnsafeBytes(of: nameCharCount.bigEndian) { Data($0) })
        data.append(nameData)
        data.append("RGB ".data(using: .ascii)!)
        
        let redFloat = Float32(r).bitPattern.bigEndian
        let greenFloat = Float32(g).bitPattern.bigEndian
        let blueFloat = Float32(b).bitPattern.bigEndian
        data.append(withUnsafeBytes(of: redFloat) { Data($0) })
        data.append(withUnsafeBytes(of: greenFloat) { Data($0) })
        data.append(withUnsafeBytes(of: blueFloat) { Data($0) })
        
        let colorType = UInt16(0).bigEndian
        data.append(withUnsafeBytes(of: colorType) { Data($0) })
        print("Color \(index): r=\(r), g=\(g), b=\(b), length=\(blockLength)")
    }
    
    // Group end
    let groupEnd = UInt16(0xc002).bigEndian
    let zeroLength = UInt32(0)
    data.append(withUnsafeBytes(of: groupEnd) { Data($0) })
    data.append(withUnsafeBytes(of: zeroLength) { Data($0) })
    
    print("ASE generated, bytes: \(data.count), preview: \(data.prefix(64).hexString)")
    return data
}

The exported colors, like “Sakura” (#F5A9B8), now appear correctly in Photoshop and Illustrator, preserving their delicate Japanese aesthetic.

Enhancing the User Experience

Beyond the core generation, we integrated the ASE export with user-friendly features, tailored for our Japanese audience:

1. Temporary Directory for Debugging

To avoid file conflicts during testing, we created a temporary directory:

private func setupTempDirectory() throws -> URL {
    let fileManager = FileManager.default
    guard let tempBaseURL = fileManager.temporaryDirectory else {
        throw NSError(domain: "NipponColors", code: -1, userInfo: [NSLocalizedDescriptionKey: "Temporary directory not found".localized()])
    }
    let tempDir = tempBaseURL.appendingPathComponent("exportedPalettes")
    try? fileManager.removeItem(at: tempDir)
    try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true)
    return tempDir
}

2. Error Handling

We wrapped the export in a do-catch block, with localized alerts in Japanese and English:

do {
    let exportDir = try setupTempDirectory()
    let exportURL = exportDir.appendingPathComponent(fileName)
    try aseData.write(to: exportURL)
    // Share file
} catch {
    presentErrorAlert("Failed to export ASE: \(error.localizedDescription)".localized())
}

3. Seamless Sharing

We used UIActivityViewController for sharing, with a localized success message:

if completed {
    let alert = UIAlertController(
        title: "Success".localized(),
        message: "Palette exported successfully!".localized(),
        preferredStyle: .alert
    )
    alert.addAction(UIAlertAction(title: "OK".localized(), style: .default))
    topViewController.present(alert, animated: true)
}

We ensured color names (e.g., “桜” for Sakura) and error messages were localized in Japanese to match our users’ cultural expectations.

Best Practices for Binary File Exports

This experience taught us key lessons for binary formats:

1. Respect the Specification

Study the format documentation or reverse-engineer sample files. Small deviations, like wrong endianness, break compatibility.

2. Use Big-Endian Consistently

Binary formats often require big-endian encoding. Use Swift’s .bigEndian property to avoid errors.

3. Validate String Encoding

Confirm the expected encoding (UTF-16 vs. UTF-8) and include correct length prefixes. Test with non-ASCII characters like Japanese.

4. Debug with Hex Editors

Use tools like Hex Fiend to compare outputs. Log byte sequences to catch mismatches early.

5. Handle Errors Gracefully

Wrap file operations in do-catch blocks and provide localized feedback to avoid frustrating users.

6. Test Across Tools

Verify files in multiple tools (Photoshop, Illustrator, Affinity Designer) and versions (e.g., Photoshop 2023 vs. 2025) to ensure compatibility.

Conclusion

Adding Adobe ASE export to Nippon Colors was a challenging but rewarding journey. What started as a feature request turned into a deep dive into binary file formats, endianness, and string encoding. By analyzing the ASE specification, fixing byte-level errors, and building a robust implementation, we empowered designers to use Japanese colors in their favorite tools.

The generateASEData method is the heart of this feature, and we hope sharing it helps other developers tackle similar challenges. Binary formats may seem daunting, but with patience and precision, they unlock powerful possibilities for creative apps like Nippon Colors.

If you’re building a color management app or exploring file exports, remember: every byte matters, and a well-placed debug log can save hours of frustration. Happy coding!