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 Header (24 bytes) adds:

  • paletteTypesArrayOffset (uint32): Offset to palette types array
  • paletteLabelsArrayOffset (uint32): Offset to palette labels array
  • paletteEntryLabelsArrayOffset (uint32): Offset to palette entry labels

Color Record Structure (4 bytes, BGRA format):

  • blue (uint8)
  • green (uint8)
  • red (uint8)
  • alpha (uint8)

Reference: OpenType CPAL specification https://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"
PALETTE_TYPE_LIGHT_ON_DARK =

Palette type bit flags (CPAL v1, paletteTypesArray entries).

0x01
PALETTE_TYPE_DARK_ON_LIGHT =
0x02
NULL_OFFSET =

Offset that means "no table present" in CPAL v1.

0

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)



82
83
84
# File 'lib/fontisan/tables/cpal.rb', line 82

def color_records
  @color_records
end

#color_records_array_offsetInteger (readonly)

Returns Offset to color records array.

Returns:

  • (Integer)

    Offset to color records array



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

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



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

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



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

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



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

def num_palettes
  @num_palettes
end

#palette_entry_labelsArray<Integer>? (readonly)

Returns Name-record IDs (name table) for each palette entry index (shared across palettes). nil for CPAL v0 or when v1 has no entry-labels array.

Returns:

  • (Array<Integer>, nil)

    Name-record IDs (name table) for each palette entry index (shared across palettes). nil for CPAL v0 or when v1 has no entry-labels array.



95
96
97
# File 'lib/fontisan/tables/cpal.rb', line 95

def 
  @palette_entry_labels
end

#palette_indicesArray<Integer> (readonly)

Returns Palette indices (start index for each palette).

Returns:

  • (Array<Integer>)

    Palette indices (start index for each palette)



79
80
81
# File 'lib/fontisan/tables/cpal.rb', line 79

def palette_indices
  @palette_indices
end

#palette_labelsArray<Integer>? (readonly)

Returns Name-record IDs (name table) for each palette. nil for CPAL v0 or when v1 has no labels array.

Returns:

  • (Array<Integer>, nil)

    Name-record IDs (name table) for each palette. nil for CPAL v0 or when v1 has no labels array.



90
91
92
# File 'lib/fontisan/tables/cpal.rb', line 90

def palette_labels
  @palette_labels
end

#palette_typesArray<Integer>? (readonly)

Returns Palette type bit flags, one per palette. nil for CPAL v0 or when v1 has no palette types array.

Returns:

  • (Array<Integer>, nil)

    Palette type bit flags, one per palette. nil for CPAL v0 or when v1 has no palette types array.



86
87
88
# File 'lib/fontisan/tables/cpal.rb', line 86

def palette_types
  @palette_types
end

#raw_dataString (readonly)

Returns Raw binary data for the entire CPAL table.

Returns:

  • (String)

    Raw binary data for the entire CPAL table



76
77
78
# File 'lib/fontisan/tables/cpal.rb', line 76

def raw_data
  @raw_data
end

#versionInteger (readonly)

Returns CPAL version (0 or 1).

Returns:

  • (Integer)

    CPAL version (0 or 1)



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

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



101
102
103
104
105
106
107
108
# File 'lib/fontisan/tables/cpal.rb', line 101

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



160
161
162
# File 'lib/fontisan/tables/cpal.rb', line 160

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



169
170
171
172
173
174
175
176
# File 'lib/fontisan/tables/cpal.rb', line 169

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

#dark_on_light?(palette_index) ⇒ Boolean

Whether a palette is intended for dark-on-light use (CPAL v1).

Parameters:

  • palette_index (Integer)

Returns:

  • (Boolean)

    true if the palette type bit is set; false otherwise



212
213
214
# File 'lib/fontisan/tables/cpal.rb', line 212

def dark_on_light?(palette_index)
  palette_type_set?(palette_index, PALETTE_TYPE_DARK_ON_LIGHT)
end

#light_on_dark?(palette_index) ⇒ Boolean

Whether a palette is intended for light-on-dark use (CPAL v1).

Parameters:

  • palette_index (Integer)

Returns:

  • (Boolean)

    true if the palette type bit is set; false otherwise



204
205
206
# File 'lib/fontisan/tables/cpal.rb', line 204

def light_on_dark?(palette_index)
  palette_type_set?(palette_index, PALETTE_TYPE_LIGHT_ON_DARK)
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



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/fontisan/tables/cpal.rb', line 141

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

#palette_entry_label(entry_index) ⇒ Integer?

Get the name-record ID for a palette entry's human-readable label.

Parameters:

  • entry_index (Integer)

Returns:

  • (Integer, nil)

    nameID for the entry, or nil if v0 / no labels



193
194
195
196
197
198
# File 'lib/fontisan/tables/cpal.rb', line 193

def (entry_index)
  return nil unless 
  return nil if entry_index.negative? || entry_index >= .length

  [entry_index]
end

#palette_label(palette_index) ⇒ Integer?

Get the name-record ID for a palette's human-readable label.

Parameters:

  • palette_index (Integer)

Returns:

  • (Integer, nil)

    nameID for the palette, or nil if v0 / no labels



182
183
184
185
186
187
# File 'lib/fontisan/tables/cpal.rb', line 182

def palette_label(palette_index)
  return nil unless palette_labels
  return nil if palette_index.negative? || palette_index >= palette_labels.length

  palette_labels[palette_index]
end

#parse!(data) ⇒ Object

Parse the CPAL table structure

Parameters:

  • data (String)

    Binary data for the CPAL table

Raises:



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

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: palette types + labels (lazily walked via offsets)
   if version == 1
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



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/fontisan/tables/cpal.rb', line 219

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