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 "
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.
87 88 89 |
# File 'lib/fontisan/tables/cff.rb', line 87 def global_subr_index @global_subr_index end |
#header ⇒ Cff::Header (readonly)
Returns CFF header structure.
72 73 74 |
# File 'lib/fontisan/tables/cff.rb', line 72 def header @header end |
#name_index ⇒ Cff::Index (readonly)
Returns Name INDEX containing font names.
75 76 77 |
# File 'lib/fontisan/tables/cff.rb', line 75 def name_index @name_index end |
#raw_data ⇒ String (readonly)
Returns Raw binary data for the entire CFF table.
90 91 92 |
# File 'lib/fontisan/tables/cff.rb', line 90 def raw_data @raw_data end |
#string_index ⇒ Cff::Index (readonly)
Returns String INDEX containing string data.
84 85 86 |
# File 'lib/fontisan/tables/cff.rb', line 84 def string_index @string_index end |
#top_dict_index ⇒ Cff::Index (readonly)
Returns Top DICT INDEX containing font-level data.
78 79 80 |
# File 'lib/fontisan/tables/cff.rb', line 78 def top_dict_index @top_dict_index end |
#top_dicts ⇒ Array<TopDict> (readonly)
Returns Parsed Top DICT objects.
81 82 83 |
# File 'lib/fontisan/tables/cff.rb', line 81 def top_dicts @top_dicts end |
Class Method Details
.read(io) ⇒ Cff
Override read to parse CFF structure
96 97 98 99 100 101 102 103 |
# File 'lib/fontisan/tables/cff.rb', line 96 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)
212 213 214 |
# File 'lib/fontisan/tables/cff.rb', line 212 def cff2? @header&.cff2? || false end |
#cff? ⇒ Boolean
Check if this is a standard CFF table (non-variable)
219 220 221 |
# File 'lib/fontisan/tables/cff.rb', line 219 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
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/fontisan/tables/cff.rb', line 367 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
336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/fontisan/tables/cff.rb', line 336 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)
253 254 255 |
# File 'lib/fontisan/tables/cff.rb', line 253 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
186 187 188 |
# File 'lib/fontisan/tables/cff.rb', line 186 def font_count @name_index&.count || 0 end |
#font_name(index = 0) ⇒ String?
Get the PostScript name of a font by index
194 195 196 197 198 199 200 |
# File 'lib/fontisan/tables/cff.rb', line 194 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
205 206 207 |
# File 'lib/fontisan/tables/cff.rb', line 205 def font_names @name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") } end |
#global_subr_count ⇒ Integer
Get count of global subroutines
260 261 262 |
# File 'lib/fontisan/tables/cff.rb', line 260 def global_subr_count @global_subr_index&.count || 0 end |
#glyph_count(index = 0) ⇒ Integer
Get the number of glyphs in a font
391 392 393 394 |
# File 'lib/fontisan/tables/cff.rb', line 391 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
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/fontisan/tables/cff.rb', line 303 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.
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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/fontisan/tables/cff.rb', line 115 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
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/fontisan/tables/cff.rb', line 278 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.
237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/fontisan/tables/cff.rb', line 237 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
268 269 270 |
# File 'lib/fontisan/tables/cff.rb', line 268 def top_dict(index = 0) @top_dicts&.[](index) end |
#valid? ⇒ Boolean
Validate the CFF table structure
399 400 401 402 403 404 405 406 407 408 |
# File 'lib/fontisan/tables/cff.rb', line 399 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
226 227 228 |
# File 'lib/fontisan/tables/cff.rb', line 226 def version @header&.version || "unknown" end |