Class: Fontisan::Tables::Cff
- Inherits:
-
Binary::BaseRecord
- Object
- BinData::Record
- Binary::BaseRecord
- Fontisan::Tables::Cff
- 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/charstrings_index.rb,
lib/fontisan/tables/cff/charstring_builder.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 adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
Reference: docs/ttfunk-feature-analysis.md lines 2607-2648
Defined Under Namespace
Classes: CFFGlyph, CharString, CharStringBuilder, Charset, CharstringsIndex, Dict, DictBuilder, Encoding, Header, Index, IndexBuilder, PrivateDict, TopDict
Constant Summary collapse
- TAG =
OpenType table tag for CFF
"CFF "
Instance Attribute Summary collapse
-
#global_subr_index ⇒ Cff::Index
readonly
Global Subr INDEX containing global subroutines.
-
#header ⇒ Cff::Header
readonly
CFF header structure.
-
#name_index ⇒ Cff::Index
readonly
Name INDEX containing font names.
-
#raw_data ⇒ String
readonly
Raw binary data for the entire CFF table.
-
#string_index ⇒ Cff::Index
readonly
String INDEX containing string data.
-
#top_dict_index ⇒ Cff::Index
readonly
Top DICT INDEX containing font-level data.
-
#top_dicts ⇒ Array<TopDict>
readonly
Parsed Top DICT objects.
Class Method Summary collapse
-
.read(io) ⇒ Cff
Override read to parse CFF structure.
Instance Method Summary collapse
-
#cff2? ⇒ Boolean
Check if this is a CFF2 table (variable CFF).
-
#cff? ⇒ Boolean
Check if this is a standard CFF table (non-variable).
-
#charstring_for_glyph(glyph_index, font_index = 0) ⇒ CharString?
Get a CharString for a specific glyph.
-
#charstrings_index(index = 0) ⇒ CharstringsIndex?
Get the CharStrings INDEX for a specific font.
-
#custom_string_count ⇒ Integer
Get count of custom strings (beyond standard strings).
-
#font_count ⇒ Integer
Get the number of fonts in this CFF table.
-
#font_name(index = 0) ⇒ String?
Get the PostScript name of a font by index.
-
#font_names ⇒ Array<String>
Get all font names in this CFF.
-
#global_subr_count ⇒ Integer
Get count of global subroutines.
-
#glyph_count(index = 0) ⇒ Integer
Get the number of glyphs in a font.
-
#local_subrs(index = 0) ⇒ Index?
Get the Local Subr INDEX for a specific font.
-
#parse!(data) ⇒ Object
Parse the CFF table structure.
-
#private_dict(index = 0) ⇒ PrivateDict?
Parse the Private DICT for a specific font.
-
#string_for_sid(sid) ⇒ String?
Get a string by String ID (SID).
-
#top_dict(index = 0) ⇒ TopDict?
Get the Top DICT for a specific font.
-
#valid? ⇒ Boolean
Validate the CFF table structure.
-
#version ⇒ String
Get the CFF version string.
Instance Attribute Details
#global_subr_index ⇒ Cff::Index (readonly)
Returns Global Subr INDEX containing global subroutines.
64 65 66 |
# File 'lib/fontisan/tables/cff.rb', line 64 def global_subr_index @global_subr_index end |
#header ⇒ Cff::Header (readonly)
Returns CFF header structure.
49 50 51 |
# File 'lib/fontisan/tables/cff.rb', line 49 def header @header end |
#name_index ⇒ Cff::Index (readonly)
Returns Name INDEX containing font names.
52 53 54 |
# File 'lib/fontisan/tables/cff.rb', line 52 def name_index @name_index end |
#raw_data ⇒ String (readonly)
Returns Raw binary data for the entire CFF table.
67 68 69 |
# File 'lib/fontisan/tables/cff.rb', line 67 def raw_data @raw_data end |
#string_index ⇒ Cff::Index (readonly)
Returns String INDEX containing string data.
61 62 63 |
# File 'lib/fontisan/tables/cff.rb', line 61 def string_index @string_index end |
#top_dict_index ⇒ Cff::Index (readonly)
Returns Top DICT INDEX containing font-level data.
55 56 57 |
# File 'lib/fontisan/tables/cff.rb', line 55 def top_dict_index @top_dict_index end |
#top_dicts ⇒ Array<TopDict> (readonly)
Returns Parsed Top DICT objects.
58 59 60 |
# File 'lib/fontisan/tables/cff.rb', line 58 def top_dicts @top_dicts end |
Class Method Details
.read(io) ⇒ Cff
Override read to parse CFF structure
73 74 75 76 77 78 79 80 |
# File 'lib/fontisan/tables/cff.rb', line 73 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)
189 190 191 |
# File 'lib/fontisan/tables/cff.rb', line 189 def cff2? @header&.cff2? || false end |
#cff? ⇒ Boolean
Check if this is a standard CFF table (non-variable)
196 197 198 |
# File 'lib/fontisan/tables/cff.rb', line 196 def cff? @header&.cff? || false 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
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/fontisan/tables/cff.rb', line 345 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 warn "Failed to get CharString for glyph #{glyph_index}: #{e.}" nil end |
#charstrings_index(index = 0) ⇒ CharstringsIndex?
Get the CharStrings INDEX for a specific font
The CharStrings INDEX contains glyph outline programs
314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/fontisan/tables/cff.rb', line 314 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 warn "Failed to parse CharStrings INDEX: #{e.}" nil end |
#custom_string_count ⇒ Integer
Get count of custom strings (beyond standard strings)
230 231 232 |
# File 'lib/fontisan/tables/cff.rb', line 230 def custom_string_count @string_index&.count || 0 end |
#font_count ⇒ Integer
Get the number of fonts in this CFF table
Typically 1 for most OpenType fonts, but CFF supports multiple fonts
163 164 165 |
# File 'lib/fontisan/tables/cff.rb', line 163 def font_count @name_index&.count || 0 end |
#font_name(index = 0) ⇒ String?
Get the PostScript name of a font by index
171 172 173 174 175 176 177 |
# File 'lib/fontisan/tables/cff.rb', line 171 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_names ⇒ Array<String>
Get all font names in this CFF
182 183 184 |
# File 'lib/fontisan/tables/cff.rb', line 182 def font_names @name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") } end |
#global_subr_count ⇒ Integer
Get count of global subroutines
237 238 239 |
# File 'lib/fontisan/tables/cff.rb', line 237 def global_subr_count @global_subr_index&.count || 0 end |
#glyph_count(index = 0) ⇒ Integer
Get the number of glyphs in a font
369 370 371 372 |
# File 'lib/fontisan/tables/cff.rb', line 369 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
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/fontisan/tables/cff.rb', line 281 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 warn "Failed to parse Local Subr INDEX: #{e.}" nil end |
#parse!(data) ⇒ Object
Parse the CFF table structure
This parses the foundational CFF structures: Header, Name INDEX, Top DICT INDEX, String INDEX, and Global Subr INDEX.
Additional structures (CharStrings, Charset, Encoding, Private DICT) will be implemented in follow-up tasks.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 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 |
# File 'lib/fontisan/tables/cff.rb', line 92 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.}" end |
#private_dict(index = 0) ⇒ PrivateDict?
Parse the Private DICT for a specific font
The Private DICT location is specified in the Top DICT
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/fontisan/tables/cff.rb', line 255 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 warn "Failed to parse Private DICT: #{e.}" nil 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.
214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/fontisan/tables/cff.rb', line 214 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
245 246 247 |
# File 'lib/fontisan/tables/cff.rb', line 245 def top_dict(index = 0) @top_dicts&.[](index) end |
#valid? ⇒ Boolean
Validate the CFF table structure
377 378 379 380 381 382 383 384 385 386 |
# File 'lib/fontisan/tables/cff.rb', line 377 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 |
#version ⇒ String
Get the CFF version string
203 204 205 |
# File 'lib/fontisan/tables/cff.rb', line 203 def version @header&.version || "unknown" end |