Class: Fontisan::Export::TtxGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/export/ttx_generator.rb

Overview

TtxGenerator generates TTX XML format from font data

Generates fonttools-compatible TTX XML format for font debugging and interoperability. Handles various table types with appropriate XML structures per the TTX specification.

Examples:

Generating TTX from a font

generator = TtxGenerator.new(font, "font.ttf")
ttx_xml = generator.generate
File.write("font.ttx", ttx_xml)

Selective table generation

ttx_xml = generator.generate(tables: ["head", "name", "glyf"])

Constant Summary collapse

TABLE_EMITTERS =

Per-tag emitter dispatch table. Adding a new table's TTX emitter means adding one entry here, not editing generate_table.

{
  "head" => :generate_head_table,
  "hhea" => :generate_hhea_table,
  "maxp" => :generate_maxp_table,
  "post" => :generate_post_table,
  "name" => :generate_name_table,
  "cmap" => :generate_cmap_table,
  "loca" => :generate_loca_table,
  "glyf" => :generate_glyf_table,
  "CFF" => :generate_cff_table,
  "CFF " => :generate_cff_table,
  "hmtx" => :generate_hmtx_table,
  "fvar" => :generate_fvar_table,
}.freeze
VARIATION_TAGS =

Variation tags all share the same emitter shape.

%w[gvar cvar HVAR VVAR MVAR].freeze

Instance Method Summary collapse

Constructor Details

#initialize(font, source_path, options = {}) ⇒ TtxGenerator

Initialize TTX generator

Parameters:

  • font (TrueTypeFont, OpenTypeFont)

    The font to export

  • source_path (String)

    Path to source font file

  • options (Hash) (defaults to: {})

    Generation options

Options Hash (options):

  • :pretty (Boolean)

    Pretty-print XML (default: true)

  • :indent (Integer)

    Indentation spaces (default: 2)



28
29
30
31
32
33
# File 'lib/fontisan/export/ttx_generator.rb', line 28

def initialize(font, source_path, options = {})
  @font = font
  @source_path = source_path
  @pretty = options.fetch(:pretty, true)
  @indent = options.fetch(:indent, 2)
end

Instance Method Details

#add_element_with_value(parent, name, value, doc) ⇒ void

This method returns an undefined value.

Helper to add element with value attribute

Parameters:

  • parent (Nokogiri::XML::Element)

    Parent element

  • name (String)

    Element name

  • value (Object)

    Value

  • doc (Nokogiri::XML::Document)

    Document



282
283
284
285
286
# File 'lib/fontisan/export/ttx_generator.rb', line 282

def add_element_with_value(parent, name, value, doc)
  elem = doc.create_element(name)
  elem["value"] = value.to_s
  parent.add_child(elem)
end

#format_binary_flags(value, bits) ⇒ String

Format binary flags

Parameters:

  • value (Integer)

    Integer value

  • bits (Integer)

    Number of bits

Returns:

  • (String)

    Binary string with spaces every 8 bits



482
483
484
485
486
# File 'lib/fontisan/export/ttx_generator.rb', line 482

def format_binary_flags(value, bits)
  binary = value.to_s(2).rjust(bits, "0")
  # Add spaces every 8 bits from left
  binary.scan(/.{1,8}/).join(" ")
end

#format_fixed(value) ⇒ String

Format fixed-point number (16.16)

Parameters:

  • value (Integer)

    Fixed-point value

Returns:

  • (String)

    Decimal string



457
458
459
460
461
462
463
464
465
# File 'lib/fontisan/export/ttx_generator.rb', line 457

def format_fixed(value)
  result = value.to_f / 65536.0
  # Format with minimal decimal places
  if result == result.to_i
    "#{result.to_i}.0"
  else
    result.to_s
  end
end

#format_hex(value, width: 8) ⇒ String

Format hex value

Parameters:

  • value (Integer)

    Integer value

  • width (Integer) (defaults to: 8)

    Minimum hex width

Returns:

  • (String)

    Hex string (e.g., "0x1234")



472
473
474
475
# File 'lib/fontisan/export/ttx_generator.rb', line 472

def format_hex(value, width: 8)
  int_value = Integer(value)
  "0x#{int_value.to_s(16).rjust(width, '0')}"
end

#format_hex_data(data) ⇒ String

Format binary data as hex

Parameters:

  • data (String)

    Binary data

Returns:

  • (String)

    Hex string with newlines every 32 bytes



505
506
507
508
509
# File 'lib/fontisan/export/ttx_generator.rb', line 505

def format_hex_data(data)
  hex = data.unpack1("H*")
  # Format in lines of 64 hex chars (32 bytes) for readability
  hex.scan(/.{1,64}/).join("\n    ")
end

#format_output(xml) ⇒ String

Format output XML

Parameters:

  • xml (String)

    Raw XML

Returns:

  • (String)

    Formatted XML



515
516
517
518
519
520
521
522
523
# File 'lib/fontisan/export/ttx_generator.rb', line 515

def format_output(xml)
  if @pretty
    doc = Nokogiri::XML(xml)
    doc.to_xml(indent: @indent)
  else
    # Remove extra whitespace for compact format
    xml.gsub(/>\s+</, "><").gsub(/\n\s*/, "")
  end
end

#format_sfnt_version(version) ⇒ String

Format SFNT version

Parameters:

  • version (Integer)

    SFNT version

Returns:

  • (String)

    Formatted version as escaped bytes



447
448
449
450
451
# File 'lib/fontisan/export/ttx_generator.rb', line 447

def format_sfnt_version(version)
  # Format as 4 bytes for TTX compatibility
  bytes = [version].pack("N").bytes
  "\\x#{bytes.map { |b| b.to_s(16).rjust(2, '0') }.join('\\x')}"
end

#format_timestamp(timestamp) ⇒ String

Format timestamp

Parameters:

  • timestamp (Integer)

    Mac timestamp (seconds since 1904-01-01)

Returns:

  • (String)

    Human-readable date string



492
493
494
495
496
497
498
499
# File 'lib/fontisan/export/ttx_generator.rb', line 492

def format_timestamp(timestamp)
  # Mac epoch: Jan 1, 1904 00:00:00 UTC
  mac_epoch = Time.utc(1904, 1, 1)
  time = mac_epoch + timestamp
  time.strftime("%a %b %e %H:%M:%S %Y")
rescue StandardError
  "Invalid Date"
end

#generate(options = {}) ⇒ String

Generate TTX XML

Parameters:

  • options (Hash) (defaults to: {})

    Generation options

Options Hash (options):

  • :tables (Array<String>)

    Specific tables to include

Returns:

  • (String)

    TTX XML content



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/fontisan/export/ttx_generator.rb', line 40

def generate(options = {})
  table_list = options[:tables] || :all

  builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
    xml.ttFont(
      "sfntVersion" => format_sfnt_version(to_int(@font.header.sfnt_version)),
      "ttLibVersion" => "4.0",
    ) do
      generate_glyph_order(xml)

      tables_to_generate = select_tables(table_list)
      tables_to_generate.each do |tag|
        generate_table(xml, tag)
      end
    end
  end

  format_output(builder.to_xml)
end

#generate_binary_table(xml, tag, table) ⇒ void

This method returns an undefined value.

Generate binary table as hexdata

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • tag (String)

    Table tag

  • table (Object)

    Table object



294
295
296
297
298
299
300
301
# File 'lib/fontisan/export/ttx_generator.rb', line 294

def generate_binary_table(xml, tag, table)
  binary_data = table.to_binary_s
  xml.public_send(tag.to_sym) do
    xml.hexdata do
      xml.text("\n    #{format_hex_data(binary_data)}\n  ")
    end
  end
end

#generate_binary_table_from_data(xml, tag, data) ⇒ void

This method returns an undefined value.

Generate binary table from raw data

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • tag (String)

    Table tag

  • data (String)

    Raw binary data



309
310
311
312
313
314
315
316
317
# File 'lib/fontisan/export/ttx_generator.rb', line 309

def generate_binary_table_from_data(xml, tag, data)
  # Remove trailing space from tag for XML element name
  clean_tag = tag.strip
  xml.public_send(clean_tag.to_sym) do
    xml.hexdata do
      xml.text("\n    #{format_hex_data(data)}\n  ")
    end
  end
end

#generate_cff_table(xml, table) ⇒ void

This method returns an undefined value.

Generate CFF table XML (simplified for now)

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Object)

    CFF table



353
354
355
# File 'lib/fontisan/export/ttx_generator.rb', line 353

def generate_cff_table(xml, table)
  generate_binary_table(xml, "CFF", table)
end

#generate_cmap_table(xml, table) ⇒ void

This method returns an undefined value.

Generate cmap table XML (simplified for now)

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Object)

    Cmap table



324
325
326
# File 'lib/fontisan/export/ttx_generator.rb', line 324

def generate_cmap_table(xml, table)
  generate_binary_table(xml, "cmap", table)
end

#generate_fvar_table(xml, table) ⇒ void

This method returns an undefined value.

Generate fvar table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Fvar)

    Fvar table



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/fontisan/export/ttx_generator.rb', line 371

def generate_fvar_table(xml, table)
  xml.fvar do
    xml.Version("major" => to_int(table.major_version),
                "minor" => to_int(table.minor_version))

    table.axes.each do |axis|
      xml.Axis do
        xml.AxisTag axis.axis_tag
        xml.MinValue to_int(axis.min_value) / 65536.0
        xml.DefaultValue to_int(axis.default_value) / 65536.0
        xml.MaxValue to_int(axis.max_value) / 65536.0
        xml.AxisNameID to_int(axis.axis_name_id)
      end
    end
  end
end

#generate_glyf_table(xml, table) ⇒ void

This method returns an undefined value.

Generate glyf table XML (simplified for now)

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Object)

    Glyf table



344
345
346
# File 'lib/fontisan/export/ttx_generator.rb', line 344

def generate_glyf_table(xml, table)
  generate_binary_table(xml, "glyf", table)
end

#generate_head_table(xml, table) ⇒ void

This method returns an undefined value.

Generate head table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Head)

    Head table



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/fontisan/export/ttx_generator.rb', line 144

def generate_head_table(xml, table)
  xml.head do
    xml.comment(" Most of this table will be recalculated by the compiler ")
    xml.tableVersion("value" => format_fixed(to_int(table.version)))
    xml.fontRevision("value" => format_fixed(to_int(table.font_revision)))
    xml.checkSumAdjustment("value" => format_hex(to_int(table.checksum_adjustment)))
    xml.magicNumber("value" => format_hex(to_int(table.magic_number)))
    xml.flags("value" => to_int(table.flags))
    xml.unitsPerEm("value" => to_int(table.units_per_em))
    xml.created("value" => format_timestamp(to_int(table.created)))
    xml.modified("value" => format_timestamp(to_int(table.modified)))
    xml.xMin("value" => to_int(table.x_min))
    xml.yMin("value" => to_int(table.y_min))
    xml.xMax("value" => to_int(table.x_max))
    xml.yMax("value" => to_int(table.y_max))
    xml.macStyle("value" => format_binary_flags(to_int(table.mac_style),
                                                16))
    xml.lowestRecPPEM("value" => to_int(table.lowest_rec_ppem))
    xml.fontDirectionHint("value" => to_int(table.font_direction_hint))
    xml.indexToLocFormat("value" => to_int(table.index_to_loc_format))
    xml.glyphDataFormat("value" => to_int(table.glyph_data_format))
  end
end

#generate_hhea_table(xml, table) ⇒ void

This method returns an undefined value.

Generate hhea table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Hhea)

    Hhea table



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/fontisan/export/ttx_generator.rb', line 173

def generate_hhea_table(xml, table)
  xml.hhea do
    xml.tableVersion("value" => format_hex(to_int(table.version)))
    xml.ascent("value" => to_int(table.ascent))
    xml.descent("value" => to_int(table.descent))
    xml.lineGap("value" => to_int(table.line_gap))
    xml.advanceWidthMax("value" => to_int(table.advance_width_max))
    xml.minLeftSideBearing("value" => to_int(table.min_left_side_bearing))
    xml.minRightSideBearing("value" => to_int(table.min_right_side_bearing))
    xml.xMaxExtent("value" => to_int(table.x_max_extent))
    xml.caretSlopeRise("value" => to_int(table.caret_slope_rise))
    xml.caretSlopeRun("value" => to_int(table.caret_slope_run))
    xml.caretOffset("value" => to_int(table.caret_offset))
    xml.reserved0("value" => 0)
    xml.reserved1("value" => 0)
    xml.reserved2("value" => 0)
    xml.reserved3("value" => 0)
    xml.metricDataFormat("value" => to_int(table.metric_data_format))
    xml.numberOfHMetrics("value" => to_int(table.num_of_long_hor_metrics))
  end
end

#generate_hmtx_table(xml, table) ⇒ void

This method returns an undefined value.

Generate hmtx table XML (simplified for now)

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Object)

    Hmtx table



362
363
364
# File 'lib/fontisan/export/ttx_generator.rb', line 362

def generate_hmtx_table(xml, table)
  generate_binary_table(xml, "hmtx", table)
end

#generate_loca_table(xml, _table) ⇒ void

This method returns an undefined value.

Generate loca table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Object)

    Loca table



333
334
335
336
337
# File 'lib/fontisan/export/ttx_generator.rb', line 333

def generate_loca_table(xml, _table)
  xml.loca do
    xml.comment(" The 'loca' table will be calculated by the compiler ")
  end
end

#generate_maxp_table(xml, table) ⇒ void

This method returns an undefined value.

Generate maxp table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Maxp)

    Maxp table



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/fontisan/export/ttx_generator.rb', line 200

def generate_maxp_table(xml, table)
  xml.maxp do
    xml.comment(" Most of this table will be recalculated by the compiler ")
    version = to_int(table.version)
    xml.tableVersion("value" => format_hex(version))
    xml.numGlyphs("value" => to_int(table.num_glyphs))

    if version >= 0x00010000
      xml.maxPoints("value" => to_int(table.max_points))
      xml.maxContours("value" => to_int(table.max_contours))
      xml.maxCompositePoints("value" => to_int(table.max_component_points))
      xml.maxCompositeContours("value" => to_int(table.max_component_contours))
      xml.maxZones("value" => to_int(table.max_zones))
      xml.maxTwilightPoints("value" => to_int(table.max_twilight_points))
      xml.maxStorage("value" => to_int(table.max_storage))
      xml.maxFunctionDefs("value" => to_int(table.max_function_defs))
      xml.maxInstructionDefs("value" => to_int(table.max_instruction_defs))
      xml.maxStackElements("value" => to_int(table.max_stack_elements))
      xml.maxSizeOfInstructions("value" => to_int(table.max_size_of_instructions))
      xml.maxComponentElements("value" => to_int(table.max_component_elements))
      xml.maxComponentDepth("value" => to_int(table.max_component_depth))
    end
  end
end

#generate_name_table(xml, table) ⇒ void

This method returns an undefined value.

Generate name table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Name)

    Name table



249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/fontisan/export/ttx_generator.rb', line 249

def generate_name_table(xml, table)
  xml.name do
    table.name_records.each do |record|
      xml.namerecord(
        "nameID" => to_int(record.name_id),
        "platformID" => to_int(record.platform_id),
        "platEncID" => to_int(record.encoding_id),
        "langID" => format_hex(to_int(record.language_id), width: 3),
      ) do
        xml.text(record.string)
      end
    end
  end
end

#generate_os2_table(xml, table) ⇒ void

This method returns an undefined value.

Generate OS/2 table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Os2)

    OS/2 table



269
270
271
272
273
# File 'lib/fontisan/export/ttx_generator.rb', line 269

def generate_os2_table(xml, table)
  # OS/2 requires special handling due to slash in tag name
  # Generate it as a string and insert into the parent
  generate_binary_table(xml, "OS/2", table)
end

#generate_post_table(xml, table) ⇒ void

This method returns an undefined value.

Generate post table XML

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • table (Tables::Post)

    Post table



230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/fontisan/export/ttx_generator.rb', line 230

def generate_post_table(xml, table)
  xml.post do
    xml.formatType("value" => format_fixed(to_int(table.format)))
    xml.italicAngle("value" => format_fixed(to_int(table.italic_angle)))
    xml.underlinePosition("value" => to_int(table.underline_position))
    xml.underlineThickness("value" => to_int(table.underline_thickness))
    xml.isFixedPitch("value" => to_int(table.is_fixed_pitch))
    xml.minMemType42("value" => to_int(table.min_mem_type42))
    xml.maxMemType42("value" => to_int(table.max_mem_type42))
    xml.minMemType1("value" => to_int(table.min_mem_type1))
    xml.maxMemType1("value" => to_int(table.max_mem_type1))
  end
end

#generate_table(xml, tag) ⇒ Object



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
# File 'lib/fontisan/export/ttx_generator.rb', line 109

def generate_table(xml, tag)
  table = @font.table(tag)

  # If table can't be parsed but data exists, use binary fallback
  unless table
    if @font.table_data && @font.table_data[tag]
      generate_binary_table_from_data(xml, tag, @font.table_data[tag])
    end
    return
  end

  if tag == "OS/2"
    xml.comment(" OS/2 table skipped - requires special XML handling ")
    return
  end

  emitter = TABLE_EMITTERS[tag]
  if emitter
    method(emitter).call(xml, table)
  elsif VARIATION_TAGS.include?(tag)
    generate_variation_table(xml, tag, table)
  else
    generate_binary_table(xml, tag, table)
  end
rescue StandardError => e
  # Fallback to binary on error
  xml.comment(" Error generating #{tag}: #{e.message} ")
  generate_binary_table(xml, tag, table)
end

#generate_variation_table(xml, tag, table) ⇒ void

This method returns an undefined value.

Generate variation table XML (gvar, cvar, HVAR, etc.)

Parameters:

  • xml (Nokogiri::XML::Builder)

    XML builder

  • tag (String)

    Table tag

  • table (Object)

    Variation table



394
395
396
# File 'lib/fontisan/export/ttx_generator.rb', line 394

def generate_variation_table(xml, tag, table)
  generate_binary_table(xml, tag, table)
end

#get_glyph_name(glyph_id) ⇒ String

Get glyph name by ID

Parameters:

  • glyph_id (Integer)

    Glyph ID

Returns:

  • (String)

    Glyph name



432
433
434
435
436
437
438
439
440
441
# File 'lib/fontisan/export/ttx_generator.rb', line 432

def get_glyph_name(glyph_id)
  post = @font.table("post")
  if post.is_a?(Tables::Post) && post.glyph_names
    post.glyph_names[glyph_id] || ".notdef"
  elsif glyph_id.zero?
    ".notdef"
  else
    "glyph#{glyph_id.to_s.rjust(5, '0')}"
  end
end

#glyph_countInteger

Get number of glyphs

Returns:

  • (Integer)

    Number of glyphs



423
424
425
426
# File 'lib/fontisan/export/ttx_generator.rb', line 423

def glyph_count
  maxp = @font.table("maxp")
  maxp ? to_int(maxp.num_glyphs) : 0
end

#select_tables(table_list) ⇒ Array<String>

Select tables to generate

Parameters:

  • table_list (Symbol, Array<String>)

    :all or list of tags

Returns:

  • (Array<String>)

    Table tags to generate



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/fontisan/export/ttx_generator.rb', line 402

def select_tables(table_list)
  if table_list == :all
    @font.table_names
  else
    available = @font.table_names
    requested = Array(table_list).map(&:to_s)
    # Map CFF to "CFF " if needed
    requested = requested.map do |tag|
      if tag == "CFF" && !available.include?("CFF") && available.include?("CFF ")
        "CFF "
      else
        tag
      end
    end
    requested.select { |tag| available.include?(tag) }
  end
end