Class: Fontisan::Tables::Cff

Inherits:
Binary::BaseRecord show all
Extended by:
Registered
Defined in:
lib/fontisan/tables/cff.rb,
lib/fontisan/tables/cff/dict.rb,
lib/fontisan/tables/cff/index.rb,
lib/fontisan/tables/cff/header.rb,
lib/fontisan/tables/cff/charset.rb,
lib/fontisan/tables/cff/encoding.rb,
lib/fontisan/tables/cff/top_dict.rb,
lib/fontisan/tables/cff/cff_glyph.rb,
lib/fontisan/tables/cff/charstring.rb,
lib/fontisan/tables/cff/dict_builder.rb,
lib/fontisan/tables/cff/private_dict.rb,
lib/fontisan/tables/cff/index_builder.rb,
lib/fontisan/tables/cff/table_builder.rb,
lib/fontisan/tables/cff/expert_charsets.rb,
lib/fontisan/tables/cff/charstring_parser.rb,
lib/fontisan/tables/cff/charstrings_index.rb,
lib/fontisan/tables/cff/charstring_builder.rb,
lib/fontisan/tables/cff/offset_recalculator.rb,
lib/fontisan/tables/cff/private_dict_writer.rb,
lib/fontisan/tables/cff/charstring_rebuilder.rb,
lib/fontisan/tables/cff/standard_strings_data.rb,
lib/fontisan/tables/cff/cff2_charstring_builder.rb,
lib/fontisan/tables/cff/hint_operation_injector.rb

Overview

CFF (Compact Font Format) table parser

The CFF table contains PostScript-based glyph outline data for OpenType fonts with CFF outlines (as opposed to TrueType glyf/loca outlines). CFF is identified by the 'OTTO' signature in the font's sfnt version.

CFF Table Structure:

CFF Table = Header
          + Name INDEX
          + Top DICT INDEX
          + String INDEX
          + Global Subr INDEX
          + [Encodings]
          + [Charsets]
          + [FDSelect]
          + [CharStrings INDEX]
          + [Font DICT INDEX]
          + [Private DICT]
          + [Local Subr INDEX]

This implementation focuses on the foundational structures (Header and INDEX) which are used throughout CFF. Additional structures like DICT, CharStrings, Charset, and Encoding require separate implementations.

Reference: Adobe CFF specification https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf

Reference: docs/ttfunk-feature-analysis.md lines 2607-2648

Examples:

Reading a CFF table

data = font.table_data['CFF ']
cff = Fontisan::Tables::Cff.read(data)
puts cff.font_count  # => 1
puts cff.header.version  # => "1.0"

Defined Under Namespace

Modules: ExpertCharsets, StandardStrings Classes: CFFGlyph, Cff2CharStringBuilder, CharString, CharStringBuilder, CharStringParser, CharStringRebuilder, Charset, CharstringsIndex, Dict, DictBuilder, Encoding, Header, HintOperationInjector, Index, IndexBuilder, OffsetRecalculator, PrivateDict, PrivateDictWriter, TableBuilder, TopDict

Constant Summary collapse

TAG =

OpenType table tag for CFF

"CFF "

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registered

register_tag

Instance Attribute Details

#global_subr_indexCff::Index (readonly)

Returns Global Subr INDEX containing global subroutines.

Returns:

  • (Cff::Index)

    Global Subr INDEX containing global subroutines



93
94
95
# File 'lib/fontisan/tables/cff.rb', line 93

def global_subr_index
  @global_subr_index
end

#headerCff::Header (readonly)

Returns CFF header structure.

Returns:



78
79
80
# File 'lib/fontisan/tables/cff.rb', line 78

def header
  @header
end

#name_indexCff::Index (readonly)

Returns Name INDEX containing font names.

Returns:

  • (Cff::Index)

    Name INDEX containing font names



81
82
83
# File 'lib/fontisan/tables/cff.rb', line 81

def name_index
  @name_index
end

#raw_dataString (readonly)

Returns Raw binary data for the entire CFF table.

Returns:

  • (String)

    Raw binary data for the entire CFF table



96
97
98
# File 'lib/fontisan/tables/cff.rb', line 96

def raw_data
  @raw_data
end

#string_indexCff::Index (readonly)

Returns String INDEX containing string data.

Returns:

  • (Cff::Index)

    String INDEX containing string data



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

def string_index
  @string_index
end

#top_dict_indexCff::Index (readonly)

Returns Top DICT INDEX containing font-level data.

Returns:

  • (Cff::Index)

    Top DICT INDEX containing font-level data



84
85
86
# File 'lib/fontisan/tables/cff.rb', line 84

def top_dict_index
  @top_dict_index
end

#top_dictsArray<TopDict> (readonly)

Returns Parsed Top DICT objects.

Returns:

  • (Array<TopDict>)

    Parsed Top DICT objects



87
88
89
# File 'lib/fontisan/tables/cff.rb', line 87

def top_dicts
  @top_dicts
end

Class Method Details

.read(io) ⇒ Cff

Override read to parse CFF structure

Parameters:

  • io (IO, String)

    Binary data to read

Returns:

  • (Cff)

    Parsed CFF table



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

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

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

Instance Method Details

#cff2?Boolean

Check if this is a CFF2 table (variable CFF)

Returns:

  • (Boolean)

    True if CFF version 2



217
218
219
# File 'lib/fontisan/tables/cff.rb', line 217

def cff2?
  @header&.cff2? || false
end

#cff?Boolean

Check if this is a standard CFF table (non-variable)

Returns:

  • (Boolean)

    True if CFF version 1



224
225
226
# File 'lib/fontisan/tables/cff.rb', line 224

def cff?
  @header&.cff? || false
end

#charset(index = 0) ⇒ Charset?

Get the Charset for a specific font

Charset maps glyph IDs to glyph names via String IDs

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (Charset, nil)

    Charset object, or nil if not present



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/fontisan/tables/cff.rb', line 434

def charset(index = 0)
  top = top_dict(index)
  return nil unless top

  charset_offset = top.charset
  return nil unless charset_offset

  # Handle predefined charsets
  if charset_offset <= 2
    num_glyphs = glyph_count(index)
    return Charset.new(charset_offset, num_glyphs, self)
  end

  # Parse custom charset from offset
  charset_data = @raw_data[charset_offset..]
  return nil unless charset_data

  num_glyphs = glyph_count(index)
  Charset.new(charset_data, num_glyphs, self)
rescue StandardError => e
  raise CorruptedTableError, "Failed to parse Charset: #{e.message}"
end

#charstring_for_glyph(glyph_index, font_index = 0) ⇒ CharString?

Get a CharString for a specific glyph

This returns an interpreted CharString object with the glyph's outline data

Examples:

Getting a glyph's CharString

cff = Fontisan::Tables::Cff.read(data)
charstring = cff.charstring_for_glyph(42)
puts charstring.width
puts charstring.bounding_box
charstring.to_commands.each { |cmd| puts cmd.inspect }

Parameters:

  • glyph_index (Integer)

    Glyph index (0-based, 0 is typically .notdef)

  • font_index (Integer) (defaults to: 0)

    Font index in CFF (default 0)

Returns:

  • (CharString, nil)

    Interpreted CharString, or nil if not found



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/fontisan/tables/cff.rb', line 372

def charstring_for_glyph(glyph_index, font_index = 0)
  charstrings = charstrings_index(font_index)
  return nil unless charstrings

  priv_dict = private_dict(font_index)
  return nil unless priv_dict

  local_subr_index = local_subrs(font_index)

  charstrings.charstring_at(
    glyph_index,
    priv_dict,
    @global_subr_index,
    local_subr_index,
  )
rescue StandardError => e
  raise CorruptedTableError,
        "Failed to get CharString for glyph #{glyph_index}: #{e.message}"
end

#charstrings_index(index = 0) ⇒ CharstringsIndex?

Get the CharStrings INDEX for a specific font

The CharStrings INDEX contains glyph outline programs

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:



341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/fontisan/tables/cff.rb', line 341

def charstrings_index(index = 0)
  top = top_dict(index)
  return nil unless top

  charstrings_offset = top.charstrings
  return nil unless charstrings_offset

  io = StringIO.new(@raw_data)
  io.seek(charstrings_offset)
  CharstringsIndex.new(io, start_offset: charstrings_offset)
rescue StandardError => e
  raise CorruptedTableError,
        "Failed to parse CharStrings INDEX: #{e.message}"
end

#custom_string_countInteger

Get count of custom strings (beyond standard strings)

Returns:

  • (Integer)

    Number of custom strings



258
259
260
# File 'lib/fontisan/tables/cff.rb', line 258

def custom_string_count
  @string_index&.count || 0
end

#encoding(index = 0) ⇒ Encoding?

Get the Encoding for a specific font

Encoding maps character codes to glyph IDs

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (Encoding, nil)

    Encoding object, or nil if not present



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/fontisan/tables/cff.rb', line 463

def encoding(index = 0)
  top = top_dict(index)
  return nil unless top

  encoding_offset = top.encoding
  return nil unless encoding_offset

  # Handle predefined encodings
  if encoding_offset <= 1
    num_glyphs = glyph_count(index)
    return Encoding.new(encoding_offset, num_glyphs)
  end

  # Parse custom encoding from offset
  encoding_data = @raw_data[encoding_offset..]
  return nil unless encoding_data

  num_glyphs = glyph_count(index)
  Encoding.new(encoding_data, num_glyphs)
rescue StandardError => e
  raise CorruptedTableError, "Failed to parse Encoding: #{e.message}"
end

#font_countInteger

Get the number of fonts in this CFF table

Typically 1 for most OpenType fonts, but CFF supports multiple fonts

Returns:

  • (Integer)

    Number of fonts



191
192
193
# File 'lib/fontisan/tables/cff.rb', line 191

def font_count
  @name_index&.count || 0
end

#font_name(index = 0) ⇒ String?

Get the PostScript name of a font by index

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (String, nil)

    PostScript font name, or nil if invalid index



199
200
201
202
203
204
205
# File 'lib/fontisan/tables/cff.rb', line 199

def font_name(index = 0)
  name_data = @name_index[index]
  return nil unless name_data

  # Font names in Name INDEX are ASCII strings
  name_data.force_encoding("ASCII-8BIT")
end

#font_namesArray<String>

Get all font names in this CFF

Returns:

  • (Array<String>)

    Array of PostScript font names



210
211
212
# File 'lib/fontisan/tables/cff.rb', line 210

def font_names
  @name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") }
end

#global_subr_countInteger

Get count of global subroutines

Returns:

  • (Integer)

    Number of global subroutines



265
266
267
# File 'lib/fontisan/tables/cff.rb', line 265

def global_subr_count
  @global_subr_index&.count || 0
end

#glyph_count(index = 0) ⇒ Integer

Get the number of glyphs in a font

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (Integer)

    Number of glyphs, or 0 if CharStrings not available



396
397
398
399
# File 'lib/fontisan/tables/cff.rb', line 396

def glyph_count(index = 0)
  charstrings = charstrings_index(index)
  charstrings&.glyph_count || 0
end

#local_subrs(index = 0) ⇒ Index?

Get the Local Subr INDEX for a specific font

Local subroutines are stored in the Private DICT area

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (Index, nil)

    Local Subr INDEX, or nil if not present



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/fontisan/tables/cff.rb', line 308

def local_subrs(index = 0)
  priv_dict = private_dict(index)
  return nil unless priv_dict

  subrs_offset = priv_dict.subrs
  return nil unless subrs_offset

  top = top_dict(index)
  return nil unless top

  private_info = top.private
  return nil unless private_info

  _size, private_offset = private_info

  # Local Subr offset is relative to Private DICT start
  absolute_offset = private_offset + subrs_offset

  io = StringIO.new(@raw_data)
  io.seek(absolute_offset)
  Index.new(io, start_offset: absolute_offset)
rescue StandardError => e
  raise CorruptedTableError,
        "Failed to parse Local Subr INDEX: #{e.message}"
end

#parse!(data) ⇒ Object

Parse the CFF table structure.

Parses: Header, Name INDEX, Top DICT INDEX, String INDEX, Global Subr INDEX. Deferred structures (CharStrings, Charset, Encoding, Private DICT) are parsed on demand via their accessor methods.

Parameters:

  • data (String)

    Binary data for the CFF table

Raises:



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/fontisan/tables/cff.rb', line 120

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

  # Parse CFF Header (4 bytes minimum)
  @header = Cff::Header.read(io)
  @header.validate!

  # Skip any additional header bytes beyond the standard 4
  # (hdr_size can be larger for extensions)
  if @header.hdr_size > 4
    io.seek(@header.hdr_size)
  end

  # Parse Name INDEX
  # Contains PostScript names of fonts in this CFF
  # Typically just one name for single-font CFF
  name_start = io.pos
  @name_index = Cff::Index.new(io, start_offset: name_start)

  # Validate that we have at least one font
  if @name_index.count.zero?
    raise CorruptedTableError, "CFF table must contain at least one font"
  end

  # Parse Top DICT INDEX
  # Contains font-level DICTs with metadata and pointers
  # Count should match name_index count (one DICT per font)
  top_dict_start = io.pos
  @top_dict_index = Cff::Index.new(io, start_offset: top_dict_start)

  # Validate Top DICT count matches Name count
  unless @top_dict_index.count == @name_index.count
    raise CorruptedTableError,
          "Top DICT count (#{@top_dict_index.count}) " \
          "must match Name count (#{@name_index.count})"
  end

  # Parse String INDEX
  # Contains additional string data beyond standard strings
  # Standard strings (SIDs 0-390) are built-in
  string_start = io.pos
  @string_index = Cff::Index.new(io, start_offset: string_start)

  # Parse Global Subr INDEX
  # Contains subroutines used across all fonts in CFF
  # Can be empty (count = 0)
  global_subr_start = io.pos
  @global_subr_index = Cff::Index.new(io, start_offset: global_subr_start)

  # Parse Top DICTs
  @top_dicts = []
  @top_dict_index.each do |dict_data|
    @top_dicts << TopDict.new(dict_data)
  end

  # Additional parsing will be added in follow-up tasks:
  # - Charset parsing
  # - Encoding parsing
  # - CharStrings parsing
  # - FDSelect parsing (for CIDFonts)
  # - Private DICT parsing (requires Top DICT offsets)
rescue StandardError => e
  raise CorruptedTableError, "Failed to parse CFF table: #{e.message}"
end

#private_dict(index = 0) ⇒ PrivateDict?

Parse the Private DICT for a specific font

The Private DICT location is specified in the Top DICT

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (PrivateDict, nil)

    Private DICT object, or nil if not present



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/fontisan/tables/cff.rb', line 283

def private_dict(index = 0)
  top = top_dict(index)
  return nil unless top

  private_info = top.private
  return nil unless private_info

  size, offset = private_info
  return nil if size <= 0 || offset.negative?

  # Extract Private DICT data from raw CFF data
  private_data = @raw_data[offset, size]
  return nil unless private_data

  PrivateDict.new(private_data)
rescue StandardError => e
  raise CorruptedTableError, "Failed to parse Private DICT: #{e.message}"
end

#standard_string(sid) ⇒ String?

Get a standard CFF string by SID (0-390).

Full 391-entry standard string table per Adobe CFF spec Appendix A. Returns nil for SID < 0 or SID > 390.

Parameters:

  • sid (Integer)

    String ID (0-390)

Returns:

  • (String, nil)

    standard string, or nil for out-of-range SIDs



422
423
424
425
426
# File 'lib/fontisan/tables/cff.rb', line 422

def standard_string(sid)
  return nil if sid.negative?

  StandardStrings::LIST[sid]
end

#string_for_sid(sid) ⇒ String?

Get a string by String ID (SID)

CFF has 391 predefined standard strings (SIDs 0-390). Additional strings are stored in the String INDEX.

Parameters:

  • sid (Integer)

    String ID

Returns:

  • (String, nil)

    String data, or nil if invalid SID



242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/fontisan/tables/cff.rb', line 242

def string_for_sid(sid)
  # Standard strings (SIDs 0-390) are predefined
  # See CFF spec Appendix A for the complete list
  if sid <= 390
    standard_string(sid)
  else
    # Custom strings start at SID 391
    string_index_offset = sid - 391
    string_data = @string_index[string_index_offset]
    string_data&.force_encoding("ASCII-8BIT")
  end
end

#top_dict(index = 0) ⇒ TopDict?

Get the Top DICT for a specific font

Parameters:

  • index (Integer) (defaults to: 0)

    Font index (0-based)

Returns:

  • (TopDict, nil)

    Top DICT object, or nil if invalid index



273
274
275
# File 'lib/fontisan/tables/cff.rb', line 273

def top_dict(index = 0)
  @top_dicts&.[](index)
end

#valid?Boolean

Validate the CFF table structure

Returns:

  • (Boolean)

    True if valid



404
405
406
407
408
409
410
411
412
413
# File 'lib/fontisan/tables/cff.rb', line 404

def valid?
  return false unless @header&.valid?
  return false unless @name_index&.count&.positive?
  return false unless @top_dict_index
  return false unless @top_dict_index.count == @name_index.count
  return false unless @string_index
  return false unless @global_subr_index

  true
end

#versionString

Get the CFF version string

Returns:

  • (String)

    Version in "major.minor" format



231
232
233
# File 'lib/fontisan/tables/cff.rb', line 231

def version
  @header&.version || "unknown"
end