Class: Fontisan::Tables::Cpal

Inherits:
Binary::BaseRecord show all
Defined in:
lib/fontisan/tables/cpal.rb

Overview

CPAL (Color Palette) table parser

The CPAL table defines color palettes used by COLR layers. Each palette contains an array of RGBA color values that can be referenced by the COLR table’s palette indices.

CPAL Table Structure: “‘ CPAL Table = Header

+ Palette Indices Array
+ Color Records Array
+ [Palette Types Array] (version 1)
+ [Palette Labels Array] (version 1)
+ [Palette Entry Labels Array] (version 1)

“‘

Version 0 Header (12 bytes):

  • version (uint16): Table version (0 or 1)

  • numPaletteEntries (uint16): Number of colors per palette

  • numPalettes (uint16): Number of palettes

  • numColorRecords (uint16): Total number of color records

  • colorRecordsArrayOffset (uint32): Offset to color records array

Version 1 adds optional metadata for palette types and labels.

Color Record Structure (4 bytes, BGRA format):

  • blue (uint8)

  • green (uint8)

  • red (uint8)

  • alpha (uint8)

Reference: OpenType CPAL specification docs.microsoft.com/en-us/typography/opentype/spec/cpal

Examples:

Reading a CPAL table

data = font.table_data['CPAL']
cpal = Fontisan::Tables::Cpal.read(data)
palette = cpal.palette(0)  # Get first palette
puts palette.colors.first  # => "#RRGGBBAA"

Constant Summary collapse

TAG =

OpenType table tag for CPAL

"CPAL"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#color_recordsArray<Hash> (readonly)

Returns Parsed color records (RGBA hashes).

Returns:

  • (Array<Hash>)

    Parsed color records (RGBA hashes)



73
74
75
# File 'lib/fontisan/tables/cpal.rb', line 73

def color_records
  @color_records
end

#color_records_array_offsetInteger (readonly)

Returns Offset to color records array.

Returns:

  • (Integer)

    Offset to color records array



64
65
66
# File 'lib/fontisan/tables/cpal.rb', line 64

def color_records_array_offset
  @color_records_array_offset
end

#num_color_recordsInteger (readonly)

Returns Total number of color records.

Returns:

  • (Integer)

    Total number of color records



61
62
63
# File 'lib/fontisan/tables/cpal.rb', line 61

def num_color_records
  @num_color_records
end

#num_palette_entriesInteger (readonly)

Returns Number of color entries per palette.

Returns:

  • (Integer)

    Number of color entries per palette



55
56
57
# File 'lib/fontisan/tables/cpal.rb', line 55

def num_palette_entries
  @num_palette_entries
end

#num_palettesInteger (readonly)

Returns Number of palettes in this table.

Returns:

  • (Integer)

    Number of palettes in this table



58
59
60
# File 'lib/fontisan/tables/cpal.rb', line 58

def num_palettes
  @num_palettes
end

#palette_indicesArray<Integer> (readonly)

Returns Palette indices (start index for each palette).

Returns:

  • (Array<Integer>)

    Palette indices (start index for each palette)



70
71
72
# File 'lib/fontisan/tables/cpal.rb', line 70

def palette_indices
  @palette_indices
end

#raw_dataString (readonly)

Returns Raw binary data for the entire CPAL table.

Returns:

  • (String)

    Raw binary data for the entire CPAL table



67
68
69
# File 'lib/fontisan/tables/cpal.rb', line 67

def raw_data
  @raw_data
end

#versionInteger (readonly)

Returns CPAL version (0 or 1).

Returns:

  • (Integer)

    CPAL version (0 or 1)



52
53
54
# File 'lib/fontisan/tables/cpal.rb', line 52

def version
  @version
end

Class Method Details

.read(io) ⇒ Cpal

Override read to parse CPAL structure

Parameters:

  • io (IO, String)

    Binary data to read

Returns:

  • (Cpal)

    Parsed CPAL table



79
80
81
82
83
84
85
86
# File 'lib/fontisan/tables/cpal.rb', line 79

def self.read(io)
  cpal = new
  return cpal if io.nil?

  data = io.is_a?(String) ? io : io.read
  cpal.parse!(data)
  cpal
end

Instance Method Details

#all_palettesArray<Array<String>>

Get all palettes

Returns:

  • (Array<Array<String>>)

    Array of palettes, each an array of hex colors



138
139
140
# File 'lib/fontisan/tables/cpal.rb', line 138

def all_palettes
  (0...num_palettes).map { |i| palette(i) }
end

#color_at(palette_index, entry_index) ⇒ String?

Get color at specific palette and entry index

Parameters:

  • palette_index (Integer)

    Palette index

  • entry_index (Integer)

    Entry index within palette

Returns:

  • (String, nil)

    Hex color string or nil



147
148
149
150
151
152
153
154
# File 'lib/fontisan/tables/cpal.rb', line 147

def color_at(palette_index, entry_index)
  return nil if palette_index.negative? || palette_index >= num_palettes
  return nil if entry_index.negative? || entry_index >= num_palette_entries

  start_index = palette_indices[palette_index]
  color_record = color_records[start_index + entry_index]
  color_record ? color_to_hex(color_record) : nil
end

#palette(index) ⇒ Array<String>?

Get a specific palette by index

Returns an array of color strings in hex format (#RRGGBBAA). Each palette contains num_palette_entries colors.

Parameters:

  • index (Integer)

    Palette index (0-based)

Returns:

  • (Array<String>, nil)

    Array of hex color strings, or nil if invalid



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/fontisan/tables/cpal.rb', line 119

def palette(index)
  return nil if index.negative? || index >= num_palettes

  # Get starting index for this palette
  start_index = palette_indices[index]

  # Extract colors for this palette
  colors = []
  num_palette_entries.times do |i|
    color_record = color_records[start_index + i]
    colors << color_to_hex(color_record) if color_record
  end

  colors
end

#parse!(data) ⇒ Object

Parse the CPAL table structure

Parameters:

  • data (String)

    Binary data for the CPAL table

Raises:



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/fontisan/tables/cpal.rb', line 92

def parse!(data)
  @raw_data = data
  io = StringIO.new(data)

  # Parse CPAL header
  parse_header(io)
  validate_header!

  # Parse palette indices array
  parse_palette_indices(io)

  # Parse color records
  parse_color_records(io)

  # Version 1 features (palette types, labels) not implemented yet
  # TODO: Add version 1 features in follow-up task
rescue StandardError => e
  raise CorruptedTableError, "Failed to parse CPAL table: #{e.message}"
end

#valid?Boolean

Validate the CPAL table structure

Returns:

  • (Boolean)

    True if valid



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/fontisan/tables/cpal.rb', line 159

def valid?
  return false if version.nil?
  return false unless [0, 1].include?(version)
  return false if num_palette_entries.nil? || num_palette_entries.negative?
  return false if num_palettes.nil? || num_palettes.negative?
  return false if num_color_records.nil? || num_color_records.negative?
  return false unless palette_indices
  return false unless color_records

  true
end