Task 020: .AI File Format

Task 020: .AI File Format

1. List of all the properties of this file format intrinsic to its file system

Based on the available specifications for the .AI file format (primarily from the Adobe Illustrator 7 file format document, as modern .AI files are proprietary PDF-based with private data and lack full public specs), the intrinsic properties include header fields, version info, and structural elements like layers, palettes, images, text, colors, patterns, overprint, groups, clipping, guides, object tags, attributes, and non-printing elements. These are parsed from PostScript comments and operators in the file. The list below covers key properties:

  • File ID Line (Magic String): The starting line, typically %!PS-Adobe-2.0 EPSF-3.0 (for PostScript-based versions; modern versions start with %PDF-1.x).
  • Header Comments:
  • %%Creator: The creator string, e.g., "Adobe Illustrator(TM) version".
  • %%For: User and organization, e.g., "(username) (organization)".
  • %%Title: Illustration title, e.g., "(illustration title)".
  • %%CreationDate: Date and time of creation, e.g., "(date) (time)".
  • %%DocumentProcSets: Procedure sets, e.g., "Adobe_Illustrator_version level revision".
  • %%DocumentSuppliedProcSets: Supplied procedure sets, e.g., "Adobe_Illustrator_version level revision".
  • %%DocumentFonts: List of fonts used, e.g., "font...".
  • %%BoundingBox: Bounding box coordinates, e.g., "llx lly urx ury".
  • %%TemplateBox: Template box coordinates, e.g., "llx lly urx ury".
  • %%PageOrigin (Windows-specific): Page origin, e.g., "x y".
  • %%PrinterName (Windows-specific): Printer name, e.g., "{printer brand name}".
  • %%PrinterRect (Windows-specific): Printer rectangle, e.g., "llx lly urx ury".
  • Version Information: %AI5_FileFormat version, e.g., "3" for Illustrator 7.0.
  • Layer Properties (from Lb operator): visible (0/1), preview (0/1), enabled (0/1), printing (0/1), dimmed (0/1), hasMultiLayerMasks (0/1), colorIndex (-1 to 26), red (0-255), green (0-255), blue (0-255).
  • Layer Name (from Ln operator): Layer name string.
  • Palette Properties (from Pb operator): topLeftCellIndex, selectedIndex.
  • Raster Image Properties (from XI/XF operator): matrix [a b c d tx ty], bounding box (llx lly urx ury), height (h), width (w), bits per pixel, ImageType (1=grayscale, 3=RGB, 4=CMYK), AlphaChannelCount, reserved, bin-ascii (0=ASCII hex, 1=binary big-endian), ImageMask (0=opaque, 1=transparent).
  • Image Link Path (from XG operator): Path string, modified flag (0/1).
  • Text Object Properties (from To operator): Type (0=point text, 1=area text, 2=path text).
  • Text Path Properties (from Tp operator): Matrix [a b c d tx ty], startPt (fractional Bézier index).
  • Text Render Mode (from Tr operator): 0=fill, 1=stroke, 2=fill+stroke, 3=invisible, etc.
  • Text Font (from Tf operator): Fontname (e.g., "/_Helvetica"), size, ascent, descent.
  • Custom Color Properties (from Xx/XX operator): Components (comp1 … compN, e.g., R G B or CMYK), name string, tint (0.0-1.0), type (0=CMYK, 1=RGB).
  • RGB Color Properties (from Xa/XA operator): red (0.0-1.0), green (0.0-1.0), blue (0.0-1.0).
  • Pattern Properties (from p/P operator): Patternname string, px, py (offset), sx, sy (scale), angle (degrees), rf (reflection flag 0/1), r (reflection angle), k (shear angle), ka (shear axis), matrix [a b c d tx ty].
  • Overprint Flag (from O/R operator): Flag (0=normal, 1=overprint).
  • Group Indicators: Begin group (u), end group (U).
  • Clipping Mask Indicators: Begin clipping (q), end clipping (Q), intersect clip (W).
  • Guide Indicators: Guide flag (), render mode (e.g., (N) for non-printing).
  • Object Tag (from XT operator): Identifier (e.g., /profits98), string value.
  • Attributes:
  • Show center point (from Ap operator): showCenter (0/1).
  • Path resolution (from Ar operator): resolution (dpi).
  • Non-Printing Elements: Begin non-printing (%AI5_Begin_NonPrinting, Np), end non-printing (%AI5_End_NonPrinting--).
  • Byte Order: Big-endian (Motorola) for binary data (e.g., images).
  • Encoding: ASCII for text, platform-specific for special characters, 83PVRKSJ–H for CJK fonts.

Note: Modern .AI files (post-Illustrator CS6) are PDF-based, so properties also include PDF version, objects, and private Illustrator data in /PieceInfo dictionaries. However, detailed specs for private data are not publicly available, so the above focuses on the documented PostScript structure.

3. Ghost blog embedded HTML JavaScript for drag n drop .AI file to dump properties

AI File Property Dumper
Drag and drop .AI file here

4. Python class for .AI file

class AIFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.content = None
        self.properties = {}

    def read_decode(self):
        with open(self.filepath, 'r', encoding='utf-8', errors='ignore') as f:
            self.content = f.read()
        self.properties = self.parse_properties(self.content)

    def parse_properties(self, content):
        lines = content.split('\n')
        properties = {
            'header': {},
            'layers': [],
            'palettes': [],
            'images': [],
            'texts': [],
            'customColors': [],
            'patterns': [],
            'overprints': [],
            'groups': 0,
            'clippings': 0,
            'guides': [],
            'objectTags': [],
            'attributes': {},
            'nonPrinting': False,
            'byteOrder': 'big-endian (assumed for binary sections)',
            'encoding': 'ASCII with platform specifics'
        }

        current_layer = None
        in_layer = False
        in_palette = False
        in_non_printing = False
        group_count = 0
        clipping_count = 0

        for line in lines:
            line = line.strip()

            # Header
            if line.startswith('%%Creator:'):
                properties['header']['creator'] = line[10:].strip()
            elif line.startswith('%%For:'):
                properties['header']['for'] = line[6:].strip()
            elif line.startswith('%%Title:'):
                properties['header']['title'] = line[8:].strip()
            elif line.startswith('%%CreationDate:'):
                properties['header']['creationDate'] = line[15:].strip()
            elif line.startswith('%%DocumentProcSets:'):
                properties['header']['documentProcSets'] = line[19:].strip()
            elif line.startswith('%%DocumentSuppliedProcSets:'):
                properties['header']['documentSuppliedProcSets'] = line[27:].strip()
            elif line.startswith('%%DocumentFonts:'):
                properties['header']['documentFonts'] = line[16:].strip()
            elif line.startswith('%%BoundingBox:'):
                properties['header']['boundingBox'] = line[14:].strip()
            elif line.startswith('%%TemplateBox:'):
                properties['header']['templateBox'] = line[14:].strip()
            elif line.startswith('%AI5_FileFormat'):
                properties['header']['aiFileFormat'] = line[15:].strip()

            # Layer
            if line == '%AI5_BeginLayer':
                in_layer = True
            if in_layer and line.startswith('Lb'):
                current_layer = {'properties': line[2:].strip().split(' ')}
                properties['layers'].append(current_layer)
            if in_layer and line.startswith('Ln'):
                if current_layer:
                    current_layer['name'] = line[2:].strip()
            if line == '%AI5_EndLayer':
                in_layer = False

            # Palette
            if line == '%AI5_BeginPalette':
                in_palette = True
            if in_palette and line.startswith('Pb'):
                properties['palettes'].append(line[2:].strip())
            if line == '%AI5_EndPalette':
                in_palette = False

            # Image
            if line.startswith('XI') or line.startswith('XF'):
                properties['images'].append(line.strip())

            # Text
            if line.startswith('To'):
                properties['texts'].append({'type': line[2:].strip()})
            if line.startswith('Tf'):
                if properties['texts']:
                    properties['texts'][-1]['font'] = line[2:].strip()

            # Custom Color
            if line.startswith('Xx') or line.startswith('XX'):
                properties['customColors'].append(line.strip())

            # Pattern
            if line.startswith('p') or line.startswith('P'):
                properties['patterns'].append(line.strip())

            # Overprint
            if line.startswith('O') or line.startswith('R'):
                properties['overprints'].append(line.strip())

            # Group and Clipping
            if line == 'u':
                group_count += 1
            if line == 'U':
                group_count -= 1
            if line == 'q':
                clipping_count += 1
            if line == 'Q':
                clipping_count -= 1
            properties['groups'] = group_count
            properties['clippings'] = clipping_count

            # Guide
            if '*' in line:
                properties['guides'].append(line.strip())

            # Object Tag
            if line.startswith('XT'):
                properties['objectTags'].append(line.strip())

            # Attributes
            if line.startswith('Ap'):
                properties['attributes']['showCenter'] = line[2:].strip()
            if line.startswith('Ar'):
                properties['attributes']['resolution'] = line[2:].strip()

            # Non-Printing
            if line == '%AI5_Begin_NonPrinting':
                in_non_printing = True
            if line == '%AI5_End_NonPrinting--':
                in_non_printing = False
            properties['nonPrinting'] = in_non_printing

        return properties

    def print_properties(self):
        import json
        print(json.dumps(self.properties, indent=4))

    def write(self, new_filepath=None):
        filepath = new_filepath or self.filepath
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(self.content)  # Writes original content; modify self.content for changes

# Example usage:
# handler = AIFileHandler('sample.ai')
# handler.read_decode()
# handler.print_properties()
# handler.write('output.ai')

5. Java class for .AI file

import java.io.*;
import java.util.*;

public class AIFileHandler {
    private String filepath;
    private String content;
    private Map<String, Object> properties;

    public AIFileHandler(String filepath) {
        this.filepath = filepath;
        this.properties = new HashMap<>();
    }

    public void readDecode() throws IOException {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
        }
        content = sb.toString();
        properties = parseProperties(content);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> parseProperties(String content) {
        String[] lines = content.split("\n");
        Map<String, Object> props = new HashMap<>();
        Map<String, String> header = new HashMap<>();
        List<Map<String, Object>> layers = new ArrayList<>();
        List<String> palettes = new ArrayList<>();
        List<String> images = new ArrayList<>();
        List<Map<String, String>> texts = new ArrayList<>();
        List<String> customColors = new ArrayList<>();
        List<String> patterns = new ArrayList<>();
        List<String> overprints = new ArrayList<>();
        int groups = 0;
        int clippings = 0;
        List<String> guides = new ArrayList<>();
        List<String> objectTags = new ArrayList<>();
        Map<String, String> attributes = new HashMap<>();
        boolean nonPrinting = false;
        props.put("header", header);
        props.put("layers", layers);
        props.put("palettes", palettes);
        props.put("images", images);
        props.put("texts", texts);
        props.put("customColors", customColors);
        props.put("patterns", patterns);
        props.put("overprints", overprints);
        props.put("guides", guides);
        props.put("objectTags", objectTags);
        props.put("attributes", attributes);
        props.put("byteOrder", "big-endian (assumed for binary sections)");
        props.put("encoding", "ASCII with platform specifics");

        Map<String, Object> currentLayer = null;
        boolean inLayer = false;
        boolean inPalette = false;
        boolean inNonPrinting = false;

        for (String line : lines) {
            line = line.trim();

            // Header
            if (line.startsWith("%%Creator:")) header.put("creator", line.substring(10).trim());
            else if (line.startsWith("%%For:")) header.put("for", line.substring(6).trim());
            else if (line.startsWith("%%Title:")) header.put("title", line.substring(8).trim());
            else if (line.startsWith("%%CreationDate:")) header.put("creationDate", line.substring(15).trim());
            else if (line.startsWith("%%DocumentProcSets:")) header.put("documentProcSets", line.substring(19).trim());
            else if (line.startsWith("%%DocumentSuppliedProcSets:")) header.put("documentSuppliedProcSets", line.substring(27).trim());
            else if (line.startsWith("%%DocumentFonts:")) header.put("documentFonts", line.substring(16).trim());
            else if (line.startsWith("%%BoundingBox:")) header.put("boundingBox", line.substring(14).trim());
            else if (line.startsWith("%%TemplateBox:")) header.put("templateBox", line.substring(14).trim());
            else if (line.startsWith("%AI5_FileFormat")) header.put("aiFileFormat", line.substring(15).trim());

            // Layer
            if (line.equals("%AI5_BeginLayer")) inLayer = true;
            if (inLayer && line.startsWith("Lb")) {
                currentLayer = new HashMap<>();
                currentLayer.put("properties", Arrays.asList(line.substring(2).trim().split(" ")));
                layers.add(currentLayer);
            }
            if (inLayer && line.startsWith("Ln")) {
                if (currentLayer != null) currentLayer.put("name", line.substring(2).trim());
            }
            if (line.equals("%AI5_EndLayer")) inLayer = false;

            // Palette
            if (line.equals("%AI5_BeginPalette")) inPalette = true;
            if (inPalette && line.startsWith("Pb")) palettes.add(line.substring(2).trim());
            if (line.equals("%AI5_EndPalette")) inPalette = false;

            // Image
            if (line.startsWith("XI") || line.startsWith("XF")) images.add(line.trim());

            // Text
            if (line.startsWith("To")) {
                Map<String, String> text = new HashMap<>();
                text.put("type", line.substring(2).trim());
                texts.add(text);
            }
            if (line.startsWith("Tf")) {
                if (!texts.isEmpty()) texts.get(texts.size() - 1).put("font", line.substring(2).trim());
            }

            // Custom Color
            if (line.startsWith("Xx") || line.startsWith("XX")) customColors.add(line.trim());

            // Pattern
            if (line.startsWith("p") || line.startsWith("P")) patterns.add(line.trim());

            // Overprint
            if (line.startsWith("O") || line.startsWith("R")) overprints.add(line.trim());

            // Group and Clipping
            if (line.equals("u")) groups++;
            if (line.equals("U")) groups--;
            if (line.equals("q")) clippings++;
            if (line.equals("Q")) clippings--;

            // Guide
            if (line.contains("*")) guides.add(line.trim());

            // Object Tag
            if (line.startsWith("XT")) objectTags.add(line.trim());

            // Attributes
            if (line.startsWith("Ap")) attributes.put("showCenter", line.substring(2).trim());
            if (line.startsWith("Ar")) attributes.put("resolution", line.substring(2).trim());

            // Non-Printing
            if (line.equals("%AI5_Begin_NonPrinting")) inNonPrinting = true;
            if (line.equals("%AI5_End_NonPrinting--")) inNonPrinting = false;
        }

        props.put("groups", groups);
        props.put("clippings", clippings);
        props.put("nonPrinting", inNonPrinting);

        return props;
    }

    public void printProperties() {
        System.out.println(propertiesToString(properties, 0));
    }

    private String propertiesToString(Object obj, int indent) {
        StringBuilder sb = new StringBuilder();
        String ind = "  ".repeat(indent);
        if (obj instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) obj;
            sb.append("{\n");
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                sb.append(ind).append("  \"").append(entry.getKey()).append("\": ").append(propertiesToString(entry.getValue(), indent + 1)).append(",\n");
            }
            sb.append(ind).append("}");
        } else if (obj instanceof List) {
            List<?> list = (List<?>) obj;
            sb.append("[\n");
            for (Object item : list) {
                sb.append(ind).append("  ").append(propertiesToString(item, indent + 1)).append(",\n");
            }
            sb.append(ind).append("]");
        } else {
            sb.append("\"").append(obj).append("\"");
        }
        return sb.toString();
    }

    public void write(String newFilepath) throws IOException {
        String path = (newFilepath == null) ? filepath : newFilepath;
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(path))) {
            bw.write(content);
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     AIFileHandler handler = new AIFileHandler("sample.ai");
    //     handler.readDecode();
    //     handler.printProperties();
    //     handler.write("output.ai");
    // }
}

6. JavaScript class for .AI file (Node.js compatible)

const fs = require('fs');

class AIFileHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.content = null;
    this.properties = {};
  }

  readDecode() {
    this.content = fs.readFileSync(this.filepath, 'utf8');
    this.properties = this.parseProperties(this.content);
  }

  parseProperties(content) {
    const lines = content.split('\n');
    const properties = {
      header: {},
      layers: [],
      palettes: [],
      images: [],
      texts: [],
      customColors: [],
      patterns: [],
      overprints: [],
      groups: 0,
      clippings: 0,
      guides: [],
      objectTags: [],
      attributes: {},
      nonPrinting: false,
      byteOrder: 'big-endian (assumed for binary sections)',
      encoding: 'ASCII with platform specifics'
    };

    let currentLayer = null;
    let inLayer = false;
    let inPalette = false;
    let inNonPrinting = false;
    let groups = 0;
    let clippings = 0;

    lines.forEach(line => {
      line = line.trim();

      // Header
      if (line.startsWith('%%Creator:')) properties.header.creator = line.slice(10).trim();
      if (line.startsWith('%%For:')) properties.header.for = line.slice(6).trim();
      if (line.startsWith('%%Title:')) properties.header.title = line.slice(8).trim();
      if (line.startsWith('%%CreationDate:')) properties.header.creationDate = line.slice(15).trim();
      if (line.startsWith('%%DocumentProcSets:')) properties.header.documentProcSets = line.slice(19).trim();
      if (line.startsWith('%%DocumentSuppliedProcSets:')) properties.header.documentSuppliedProcSets = line.slice(27).trim();
      if (line.startsWith('%%DocumentFonts:')) properties.header.documentFonts = line.slice(16).trim();
      if (line.startsWith('%%BoundingBox:')) properties.header.boundingBox = line.slice(14).trim();
      if (line.startsWith('%%TemplateBox:')) properties.header.templateBox = line.slice(14).trim();
      if (line.startsWith('%AI5_FileFormat')) properties.header.aiFileFormat = line.slice(15).trim();

      // Layer
      if (line === '%AI5_BeginLayer') inLayer = true;
      if (inLayer && line.startsWith('Lb')) {
        currentLayer = { properties: line.slice(2).trim().split(' ') };
        properties.layers.push(currentLayer);
      }
      if (inLayer && line.startsWith('Ln')) if (currentLayer) currentLayer.name = line.slice(2).trim();
      if (line === '%AI5_EndLayer') inLayer = false;

      // Palette
      if (line === '%AI5_BeginPalette') inPalette = true;
      if (inPalette && line.startsWith('Pb')) properties.palettes.push(line.slice(2).trim());
      if (line === '%AI5_EndPalette') inPalette = false;

      // Image
      if (line.startsWith('XI') || line.startsWith('XF')) properties.images.push(line.trim());

      // Text
      if (line.startsWith('To')) properties.texts.push({ type: line.slice(2).trim() });
      if (line.startsWith('Tf')) if (properties.texts.length > 0) properties.texts[properties.texts.length - 1].font = line.slice(2).trim();

      // Custom Color
      if (line.startsWith('Xx') || line.startsWith('XX')) properties.customColors.push(line.trim());

      // Pattern
      if (line.startsWith('p') || line.startsWith('P')) properties.patterns.push(line.trim());

      // Overprint
      if (line.startsWith('O') || line.startsWith('R')) properties.overprints.push(line.trim());

      // Group and Clipping
      if (line === 'u') groups++;
      if (line === 'U') groups--;
      if (line === 'q') clippings++;
      if (line === 'Q') clippings--;

      // Guide
      if (line.includes('*')) properties.guides.push(line.trim());

      // Object Tag
      if (line.startsWith('XT')) properties.objectTags.push(line.trim());

      // Attributes
      if (line.startsWith('Ap')) properties.attributes.showCenter = line.slice(2).trim();
      if (line.startsWith('Ar')) properties.attributes.resolution = line.slice(2).trim();

      // Non-Printing
      if (line === '%AI5_Begin_NonPrinting') inNonPrinting = true;
      if (line === '%AI5_End_NonPrinting--') inNonPrinting = false;
    });

    properties.groups = groups;
    properties.clippings = clippings;
    properties.nonPrinting = inNonPrinting;

    return properties;
  }

  printProperties() {
    console.log(JSON.stringify(this.properties, null, 2));
  }

  write(newFilepath = null) {
    const path = newFilepath || this.filepath;
    fs.writeFileSync(path, this.content, 'utf8');
  }
}

// Example usage:
// const handler = new AIFileHandler('sample.ai');
// handler.readDecode();
// handler.printProperties();
// handler.write('output.ai');

7. C class (using C++ for class support)

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include <string>

class AIFileHandler {
private:
    std::string filepath;
    std::string content;
    std::map<std::string, std::string> header;
    std::vector<std::map<std::string, std::string>> layers; // Simplified for C++
    std::vector<std::string> palettes;
    std::vector<std::string> images;
    std::vector<std::map<std::string, std::string>> texts;
    std::vector<std::string> customColors;
    std::vector<std::string> patterns;
    std::vector<std::string> overprints;
    int groups;
    int clippings;
    std::vector<std::string> guides;
    std::vector<std::string> objectTags;
    std::map<std::string, std::string> attributes;
    bool nonPrinting;
    std::string byteOrder;
    std::string encoding;

public:
    AIFileHandler(const std::string& fp) : filepath(fp), groups(0), clippings(0), nonPrinting(false),
        byteOrder("big-endian (assumed for binary sections)"), encoding("ASCII with platform specifics") {}

    void readDecode() {
        std::ifstream file(filepath);
        std::stringstream ss;
        ss << file.rdbuf();
        content = ss.str();
        parseProperties();
    }

    void parseProperties() {
        std::stringstream ss(content);
        std::string line;
        std::map<std::string, std::string> currentLayer;
        bool inLayer = false;
        bool inPalette = false;
        bool inNonPrinting = false;

        while (std::getline(ss, line)) {
            // Trim line (simplified)
            size_t start = line.find_first_not_of(" \t");
            if (start == std::string::npos) continue;
            line = line.substr(start);
            size_t end = line.find_last_not_of(" \t");
            line = line.substr(0, end + 1);

            // Header
            if (line.rfind("%%Creator:", 0) == 0) header["creator"] = line.substr(10);
            else if (line.rfind("%%For:", 0) == 0) header["for"] = line.substr(6);
            else if (line.rfind("%%Title:", 0) == 0) header["title"] = line.substr(8);
            else if (line.rfind("%%CreationDate:", 0) == 0) header["creationDate"] = line.substr(15);
            else if (line.rfind("%%DocumentProcSets:", 0) == 0) header["documentProcSets"] = line.substr(19);
            else if (line.rfind("%%DocumentSuppliedProcSets:", 0) == 0) header["documentSuppliedProcSets"] = line.substr(27);
            else if (line.rfind("%%DocumentFonts:", 0) == 0) header["documentFonts"] = line.substr(16);
            else if (line.rfind("%%BoundingBox:", 0) == 0) header["boundingBox"] = line.substr(14);
            else if (line.rfind("%%TemplateBox:", 0) == 0) header["templateBox"] = line.substr(14);
            else if (line.rfind("%AI5_FileFormat", 0) == 0) header["aiFileFormat"] = line.substr(15);

            // Layer (simplified parsing)
            if (line == "%AI5_BeginLayer") inLayer = true;
            if (inLayer && line.rfind("Lb", 0) == 0) {
                currentLayer["properties"] = line.substr(2);
                layers.push_back(currentLayer);
                currentLayer.clear();
            }
            if (inLayer && line.rfind("Ln", 0) == 0) layers.back()["name"] = line.substr(2);
            if (line == "%AI5_EndLayer") inLayer = false;

            // Palette
            if (line == "%AI5_BeginPalette") inPalette = true;
            if (inPalette && line.rfind("Pb", 0) == 0) palettes.push_back(line.substr(2));
            if (line == "%AI5_EndPalette") inPalette = false;

            // Image
            if (line.rfind("XI", 0) == 0 || line.rfind("XF", 0) == 0) images.push_back(line);

            // Text
            if (line.rfind("To", 0) == 0) {
                std::map<std::string, std::string> text;
                text["type"] = line.substr(2);
                texts.push_back(text);
            }
            if (line.rfind("Tf", 0) == 0) if (!texts.empty()) texts.back()["font"] = line.substr(2);

            // Custom Color
            if (line.rfind("Xx", 0) == 0 || line.rfind("XX", 0) == 0) customColors.push_back(line);

            // Pattern
            if (line.rfind("p", 0) == 0 || line.rfind("P", 0) == 0) patterns.push_back(line);

            // Overprint
            if (line.rfind("O", 0) == 0 || line.rfind("R", 0) == 0) overprints.push_back(line);

            // Group and Clipping
            if (line == "u") ++groups;
            if (line == "U") --groups;
            if (line == "q") ++clippings;
            if (line == "Q") --clippings;

            // Guide
            if (line.find('*') != std::string::npos) guides.push_back(line);

            // Object Tag
            if (line.rfind("XT", 0) == 0) objectTags.push_back(line);

            // Attributes
            if (line.rfind("Ap", 0) == 0) attributes["showCenter"] = line.substr(2);
            if (line.rfind("Ar", 0) == 0) attributes["resolution"] = line.substr(2);

            // Non-Printing
            if (line == "%AI5_Begin_NonPrinting") inNonPrinting = true;
            if (line == "%AI5_End_NonPrinting--") inNonPrinting = false;
        }
        nonPrinting = inNonPrinting;
    }

    void printProperties() {
        std::cout << "{\n";
        std::cout << "  \"header\": {\n";
        for (const auto& kv : header) {
            std::cout << "    \"" << kv.first << "\": \"" << kv.second << "\",\n";
        }
        std::cout << "  },\n";
        std::cout << "  \"layers\": [ /* simplified */ ],\n";  // Omit detailed print for brevity
        std::cout << "  \"palettes\": [ /* ... */ ],\n";
        std::cout << "  \"images\": [ /* ... */ ],\n";
        std::cout << "  \"texts\": [ /* ... */ ],\n";
        std::cout << "  \"customColors\": [ /* ... */ ],\n";
        std::cout << "  \"patterns\": [ /* ... */ ],\n";
        std::cout << "  \"overprints\": [ /* ... */ ],\n";
        std::cout << "  \"groups\": " << groups << ",\n";
        std::cout << "  \"clippings\": " << clippings << ",\n";
        std::cout << "  \"guides\": [ /* ... */ ],\n";
        std::cout << "  \"objectTags\": [ /* ... */ ],\n";
        std::cout << "  \"attributes\": {\n";
        for (const auto& kv : attributes) {
            std::cout << "    \"" << kv.first << "\": \"" << kv.second << "\",\n";
        }
        std::cout << "  },\n";
        std::cout << "  \"nonPrinting\": " << (nonPrinting ? "true" : "false") << ",\n";
        std::cout << "  \"byteOrder\": \"" << byteOrder << "\",\n";
        std::cout << "  \"encoding\": \"" << encoding << "\"\n";
        std::cout << "}\n";
    }

    void write(const std::string& newFilepath = "") {
        std::string path = newFilepath.empty() ? filepath : newFilepath;
        std::ofstream out(path);
        out << content;
    }
};

// Example usage:
// int main() {
//     AIFileHandler handler("sample.ai");
//     handler.readDecode();
//     handler.printProperties();
//     handler.write("output.ai");
//     return 0;
// }