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/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
Defined Under Namespace
Modules: 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
-
#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).
-
#charset(index = 0) ⇒ Charset?
Get the Charset for a specific font.
-
#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).
-
#encoding(index = 0) ⇒ Encoding?
Get the Encoding for a specific font.
-
#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.
-
#standard_string(sid) ⇒ String?
Get a standard CFF string by SID (0-390).
-
#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.
89 90 91 |
# File 'lib/fontisan/tables/cff.rb', line 89 def global_subr_index @global_subr_index end |
#header ⇒ Cff::Header (readonly)
Returns CFF header structure.
74 75 76 |
# File 'lib/fontisan/tables/cff.rb', line 74 def header @header end |
#name_index ⇒ Cff::Index (readonly)
Returns Name INDEX containing font names.
77 78 79 |
# File 'lib/fontisan/tables/cff.rb', line 77 def name_index @name_index end |
#raw_data ⇒ String (readonly)
Returns Raw binary data for the entire CFF table.
92 93 94 |
# File 'lib/fontisan/tables/cff.rb', line 92 def raw_data @raw_data end |
#string_index ⇒ Cff::Index (readonly)
Returns String INDEX containing string data.
86 87 88 |
# File 'lib/fontisan/tables/cff.rb', line 86 def string_index @string_index end |
#top_dict_index ⇒ Cff::Index (readonly)
Returns Top DICT INDEX containing font-level data.
80 81 82 |
# File 'lib/fontisan/tables/cff.rb', line 80 def top_dict_index @top_dict_index end |
#top_dicts ⇒ Array<TopDict> (readonly)
Returns Parsed Top DICT objects.
83 84 85 |
# File 'lib/fontisan/tables/cff.rb', line 83 def top_dicts @top_dicts end |
Class Method Details
.read(io) ⇒ Cff
Override read to parse CFF structure
98 99 100 101 102 103 104 105 |
# File 'lib/fontisan/tables/cff.rb', line 98 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)
214 215 216 |
# File 'lib/fontisan/tables/cff.rb', line 214 def cff2? @header&.cff2? || false end |
#cff? ⇒ Boolean
Check if this is a standard CFF table (non-variable)
221 222 223 |
# File 'lib/fontisan/tables/cff.rb', line 221 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
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/fontisan/tables/cff.rb', line 431 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.}" 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
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/fontisan/tables/cff.rb', line 369 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
338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/fontisan/tables/cff.rb', line 338 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)
255 256 257 |
# File 'lib/fontisan/tables/cff.rb', line 255 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
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 |
# File 'lib/fontisan/tables/cff.rb', line 460 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.}" end |
#font_count ⇒ Integer
Get the number of fonts in this CFF table
Typically 1 for most OpenType fonts, but CFF supports multiple fonts
188 189 190 |
# File 'lib/fontisan/tables/cff.rb', line 188 def font_count @name_index&.count || 0 end |
#font_name(index = 0) ⇒ String?
Get the PostScript name of a font by index
196 197 198 199 200 201 202 |
# File 'lib/fontisan/tables/cff.rb', line 196 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
207 208 209 |
# File 'lib/fontisan/tables/cff.rb', line 207 def font_names @name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") } end |
#global_subr_count ⇒ Integer
Get count of global subroutines
262 263 264 |
# File 'lib/fontisan/tables/cff.rb', line 262 def global_subr_count @global_subr_index&.count || 0 end |
#glyph_count(index = 0) ⇒ Integer
Get the number of glyphs in a font
393 394 395 396 |
# File 'lib/fontisan/tables/cff.rb', line 393 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
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/fontisan/tables/cff.rb', line 305 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.
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 181 |
# File 'lib/fontisan/tables/cff.rb', line 117 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
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/fontisan/tables/cff.rb', line 280 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 |
#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.
419 420 421 422 423 |
# File 'lib/fontisan/tables/cff.rb', line 419 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.
239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/fontisan/tables/cff.rb', line 239 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
270 271 272 |
# File 'lib/fontisan/tables/cff.rb', line 270 def top_dict(index = 0) @top_dicts&.[](index) end |
#valid? ⇒ Boolean
Validate the CFF table structure
401 402 403 404 405 406 407 408 409 410 |
# File 'lib/fontisan/tables/cff.rb', line 401 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
228 229 230 |
# File 'lib/fontisan/tables/cff.rb', line 228 def version @header&.version || "unknown" end |