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/table_builder.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/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
Defined Under Namespace
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 "- STANDARD_STRINGS_ASCII =
Adobe Standard Encoding glyph names that map 1:1 to CFF SIDs 0..95. Source: Adobe CFF Specification 1.0 (TN 5176), Appendix A. These cover the printable ASCII range plus .notdef — the most common glyph names encountered in CFF fonts.
[ ".notdef", # SID 0 "space", # SID 1 (code 32) "exclam", # SID 2 (code 33) "quotedbl", # SID 3 (code 34) "numbersign", # SID 4 (code 35) "dollar", # SID 5 (code 36) "percent", # SID 6 (code 37) "ampersand", # SID 7 (code 38) "quoteright", # SID 8 (code 39) "parenleft", # SID 9 (code 40) "parenright", # SID 10 (code 41) "asterisk", # SID 11 (code 42) "plus", # SID 12 (code 43) "comma", # SID 13 (code 44) "hyphen", # SID 14 (code 45) "period", # SID 15 (code 46) "slash", # SID 16 (code 47) "zero", # SID 17 (code 48) "one", # SID 18 (code 49) "two", # SID 19 (code 50) "three", # SID 20 (code 51) "four", # SID 21 (code 52) "five", # SID 22 (code 53) "six", # SID 23 (code 54) "seven", # SID 24 (code 55) "eight", # SID 25 (code 56) "nine", # SID 26 (code 57) "colon", # SID 27 (code 58) "semicolon", # SID 28 (code 59) "less", # SID 29 (code 60) "equal", # SID 30 (code 61) "greater", # SID 31 (code 62) "question", # SID 32 (code 63) "at", # SID 33 (code 64) "A", # SID 34 (code 65) "B", # SID 35 (code 66) "C", # SID 36 (code 67) "D", # SID 37 (code 68) "E", # SID 38 (code 69) "F", # SID 39 (code 70) "G", # SID 40 (code 71) "H", # SID 41 (code 72) "I", # SID 42 (code 73) "J", # SID 43 (code 74) "K", # SID 44 (code 75) "L", # SID 45 (code 76) "M", # SID 46 (code 77) "N", # SID 47 (code 78) "O", # SID 48 (code 79) "P", # SID 49 (code 80) "Q", # SID 50 (code 81) "R", # SID 51 (code 82) "S", # SID 52 (code 83) "T", # SID 53 (code 84) "U", # SID 54 (code 85) "V", # SID 55 (code 86) "W", # SID 56 (code 87) "X", # SID 57 (code 88) "Y", # SID 58 (code 89) "Z", # SID 59 (code 90) "bracketleft", # SID 60 (code 91) "backslash", # SID 61 (code 92) "bracketright", # SID 62 (code 93) "asciicircum", # SID 63 (code 94) "underscore", # SID 64 (code 95) "quoteleft", # SID 65 (code 96) "a", # SID 66 (code 97) "b", # SID 67 (code 98) "c", # SID 68 (code 99) "d", # SID 69 (code 100) "e", # SID 70 (code 101) "f", # SID 71 (code 102) "g", # SID 72 (code 103) "h", # SID 73 (code 104) "i", # SID 74 (code 105) "j", # SID 75 (code 106) "k", # SID 76 (code 107) "l", # SID 77 (code 108) "m", # SID 78 (code 109) "n", # SID 79 (code 110) "o", # SID 80 (code 111) "p", # SID 81 (code 112) "q", # SID 82 (code 113) "r", # SID 83 (code 114) "s", # SID 84 (code 115) "t", # SID 85 (code 116) "u", # SID 86 (code 117) "v", # SID 87 (code 118) "w", # SID 88 (code 119) "x", # SID 89 (code 120) "y", # SID 90 (code 121) "z", # SID 91 (code 122) "braceleft", # SID 92 (code 123) "bar", # SID 93 (code 124) "braceright", # SID 94 (code 125) "asciitilde", # SID 95 (code 126) ].freeze
- EXTENDED_STRINGS_START =
First SID outside the ASCII subset. SIDs in EXTENDED_STRINGS_START..390 cover ligatures, currency symbols, Cyrillic (afii*), etc. Resolving those requires the full Adobe CFF spec Appendix A table; tracked as a follow-up to avoid embedding unverified data.
STANDARD_STRINGS_ASCII.length
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.
88 89 90 |
# File 'lib/fontisan/tables/cff.rb', line 88 def global_subr_index @global_subr_index end |
#header ⇒ Cff::Header (readonly)
Returns CFF header structure.
73 74 75 |
# File 'lib/fontisan/tables/cff.rb', line 73 def header @header end |
#name_index ⇒ Cff::Index (readonly)
Returns Name INDEX containing font names.
76 77 78 |
# File 'lib/fontisan/tables/cff.rb', line 76 def name_index @name_index end |
#raw_data ⇒ String (readonly)
Returns Raw binary data for the entire CFF table.
91 92 93 |
# File 'lib/fontisan/tables/cff.rb', line 91 def raw_data @raw_data end |
#string_index ⇒ Cff::Index (readonly)
Returns String INDEX containing string data.
85 86 87 |
# File 'lib/fontisan/tables/cff.rb', line 85 def string_index @string_index end |
#top_dict_index ⇒ Cff::Index (readonly)
Returns Top DICT INDEX containing font-level data.
79 80 81 |
# File 'lib/fontisan/tables/cff.rb', line 79 def top_dict_index @top_dict_index end |
#top_dicts ⇒ Array<TopDict> (readonly)
Returns Parsed Top DICT objects.
82 83 84 |
# File 'lib/fontisan/tables/cff.rb', line 82 def top_dicts @top_dicts end |
Class Method Details
.read(io) ⇒ Cff
Override read to parse CFF structure
97 98 99 100 101 102 103 104 |
# File 'lib/fontisan/tables/cff.rb', line 97 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)
213 214 215 |
# File 'lib/fontisan/tables/cff.rb', line 213 def cff2? @header&.cff2? || false end |
#cff? ⇒ Boolean
Check if this is a standard CFF table (non-variable)
220 221 222 |
# File 'lib/fontisan/tables/cff.rb', line 220 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
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/fontisan/tables/cff.rb', line 368 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.}" end |
#charstrings_index(index = 0) ⇒ CharstringsIndex?
Get the CharStrings INDEX for a specific font
The CharStrings INDEX contains glyph outline programs
337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/fontisan/tables/cff.rb', line 337 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.}" end |
#custom_string_count ⇒ Integer
Get count of custom strings (beyond standard strings)
254 255 256 |
# File 'lib/fontisan/tables/cff.rb', line 254 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
187 188 189 |
# File 'lib/fontisan/tables/cff.rb', line 187 def font_count @name_index&.count || 0 end |
#font_name(index = 0) ⇒ String?
Get the PostScript name of a font by index
195 196 197 198 199 200 201 |
# File 'lib/fontisan/tables/cff.rb', line 195 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
206 207 208 |
# File 'lib/fontisan/tables/cff.rb', line 206 def font_names @name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") } end |
#global_subr_count ⇒ Integer
Get count of global subroutines
261 262 263 |
# File 'lib/fontisan/tables/cff.rb', line 261 def global_subr_count @global_subr_index&.count || 0 end |
#glyph_count(index = 0) ⇒ Integer
Get the number of glyphs in a font
392 393 394 395 |
# File 'lib/fontisan/tables/cff.rb', line 392 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
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/fontisan/tables/cff.rb', line 304 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.}" 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.
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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/fontisan/tables/cff.rb', line 116 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
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/fontisan/tables/cff.rb', line 279 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.}" 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.
238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/fontisan/tables/cff.rb', line 238 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
269 270 271 |
# File 'lib/fontisan/tables/cff.rb', line 269 def top_dict(index = 0) @top_dicts&.[](index) end |
#valid? ⇒ Boolean
Validate the CFF table structure
400 401 402 403 404 405 406 407 408 409 |
# File 'lib/fontisan/tables/cff.rb', line 400 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
227 228 229 |
# File 'lib/fontisan/tables/cff.rb', line 227 def version @header&.version || "unknown" end |