fontisan 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
@@ -0,0 +1,527 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Fontisan
6
+ module Export
7
+ # TtxGenerator generates TTX XML format from font data
8
+ #
9
+ # Generates fonttools-compatible TTX XML format for font debugging
10
+ # and interoperability. Handles various table types with appropriate
11
+ # XML structures per the TTX specification.
12
+ #
13
+ # @example Generating TTX from a font
14
+ # generator = TtxGenerator.new(font, "font.ttf")
15
+ # ttx_xml = generator.generate
16
+ # File.write("font.ttx", ttx_xml)
17
+ #
18
+ # @example Selective table generation
19
+ # ttx_xml = generator.generate(tables: ["head", "name", "glyf"])
20
+ class TtxGenerator
21
+ # Initialize TTX generator
22
+ #
23
+ # @param font [TrueTypeFont, OpenTypeFont] The font to export
24
+ # @param source_path [String] Path to source font file
25
+ # @param options [Hash] Generation options
26
+ # @option options [Boolean] :pretty Pretty-print XML (default: true)
27
+ # @option options [Integer] :indent Indentation spaces (default: 2)
28
+ def initialize(font, source_path, options = {})
29
+ @font = font
30
+ @source_path = source_path
31
+ @pretty = options.fetch(:pretty, true)
32
+ @indent = options.fetch(:indent, 2)
33
+ end
34
+
35
+ # Generate TTX XML
36
+ #
37
+ # @param options [Hash] Generation options
38
+ # @option options [Array<String>] :tables Specific tables to include
39
+ # @return [String] TTX XML content
40
+ def generate(options = {})
41
+ table_list = options[:tables] || :all
42
+
43
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
44
+ xml.ttFont(
45
+ "sfntVersion" => format_sfnt_version(to_int(@font.header.sfnt_version)),
46
+ "ttLibVersion" => "4.0",
47
+ ) do
48
+ generate_glyph_order(xml)
49
+
50
+ tables_to_generate = select_tables(table_list)
51
+ tables_to_generate.each do |tag|
52
+ generate_table(xml, tag)
53
+ end
54
+ end
55
+ end
56
+
57
+ format_output(builder.to_xml)
58
+ end
59
+
60
+ private
61
+
62
+ # Convert BinData value to native Ruby integer
63
+ #
64
+ # @param value [Object] BinData value or integer
65
+ # @return [Integer] Native integer
66
+ def to_int(value)
67
+ value.respond_to?(:to_i) ? value.to_i : value
68
+ end
69
+
70
+ # Generate GlyphOrder section (required first)
71
+ #
72
+ # @param xml [Nokogiri::XML::Builder] XML builder
73
+ # @return [void]
74
+ def generate_glyph_order(xml)
75
+ xml.GlyphOrder do
76
+ xml.comment(" The 'id' attribute is only for humans; it is ignored when parsed. ")
77
+ glyph_count.times do |glyph_id|
78
+ glyph_name = get_glyph_name(glyph_id)
79
+ xml.GlyphID("id" => glyph_id, "name" => glyph_name)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Generate individual table XML
85
+ #
86
+ # @param xml [Nokogiri::XML::Builder] XML builder
87
+ # @param tag [String] Table tag
88
+ # @return [void]
89
+ def generate_table(xml, tag)
90
+ table = @font.table(tag)
91
+
92
+ # If table can't be parsed but data exists, use binary fallback
93
+ unless table
94
+ if @font.table_data && @font.table_data[tag]
95
+ generate_binary_table_from_data(xml, tag, @font.table_data[tag])
96
+ end
97
+ return
98
+ end
99
+
100
+ case tag
101
+ when "head"
102
+ generate_head_table(xml, table)
103
+ when "hhea"
104
+ generate_hhea_table(xml, table)
105
+ when "maxp"
106
+ generate_maxp_table(xml, table)
107
+ when "post"
108
+ generate_post_table(xml, table)
109
+ when "name"
110
+ generate_name_table(xml, table)
111
+ when "OS/2"
112
+ # Skip OS/2 for now - Nokogiri builder can't handle slashes in element names
113
+ # TODO: Implement OS/2 table generation with proper XML escaping
114
+ xml.comment(" OS/2 table skipped - requires special XML handling ")
115
+ when "cmap"
116
+ generate_cmap_table(xml, table)
117
+ when "loca"
118
+ generate_loca_table(xml, table)
119
+ when "glyf"
120
+ generate_glyf_table(xml, table)
121
+ when "CFF"
122
+ generate_cff_table(xml, table)
123
+ when "CFF "
124
+ generate_cff_table(xml, table)
125
+ when "hmtx"
126
+ generate_hmtx_table(xml, table)
127
+ when "fvar"
128
+ generate_fvar_table(xml, table)
129
+ when "gvar", "cvar", "HVAR", "VVAR", "MVAR"
130
+ generate_variation_table(xml, tag, table)
131
+ else
132
+ generate_binary_table(xml, tag, table)
133
+ end
134
+ rescue StandardError => e
135
+ # Fallback to binary on error
136
+ xml.comment(" Error generating #{tag}: #{e.message} ")
137
+ generate_binary_table(xml, tag, table)
138
+ end
139
+
140
+ # Generate head table XML
141
+ #
142
+ # @param xml [Nokogiri::XML::Builder] XML builder
143
+ # @param table [Tables::Head] Head table
144
+ # @return [void]
145
+ def generate_head_table(xml, table)
146
+ xml.head do
147
+ xml.comment(" Most of this table will be recalculated by the compiler ")
148
+ xml.tableVersion("value" => format_fixed(to_int(table.version)))
149
+ xml.fontRevision("value" => format_fixed(to_int(table.font_revision)))
150
+ xml.checkSumAdjustment("value" => format_hex(to_int(table.checksum_adjustment)))
151
+ xml.magicNumber("value" => format_hex(to_int(table.magic_number)))
152
+ xml.flags("value" => to_int(table.flags))
153
+ xml.unitsPerEm("value" => to_int(table.units_per_em))
154
+ xml.created("value" => format_timestamp(to_int(table.created)))
155
+ xml.modified("value" => format_timestamp(to_int(table.modified)))
156
+ xml.xMin("value" => to_int(table.x_min))
157
+ xml.yMin("value" => to_int(table.y_min))
158
+ xml.xMax("value" => to_int(table.x_max))
159
+ xml.yMax("value" => to_int(table.y_max))
160
+ xml.macStyle("value" => format_binary_flags(to_int(table.mac_style),
161
+ 16))
162
+ xml.lowestRecPPEM("value" => to_int(table.lowest_rec_ppem))
163
+ xml.fontDirectionHint("value" => to_int(table.font_direction_hint))
164
+ xml.indexToLocFormat("value" => to_int(table.index_to_loc_format))
165
+ xml.glyphDataFormat("value" => to_int(table.glyph_data_format))
166
+ end
167
+ end
168
+
169
+ # Generate hhea table XML
170
+ #
171
+ # @param xml [Nokogiri::XML::Builder] XML builder
172
+ # @param table [Tables::Hhea] Hhea table
173
+ # @return [void]
174
+ def generate_hhea_table(xml, table)
175
+ xml.hhea do
176
+ xml.tableVersion("value" => format_hex(to_int(table.version)))
177
+ xml.ascent("value" => to_int(table.ascent))
178
+ xml.descent("value" => to_int(table.descent))
179
+ xml.lineGap("value" => to_int(table.line_gap))
180
+ xml.advanceWidthMax("value" => to_int(table.advance_width_max))
181
+ xml.minLeftSideBearing("value" => to_int(table.min_left_side_bearing))
182
+ xml.minRightSideBearing("value" => to_int(table.min_right_side_bearing))
183
+ xml.xMaxExtent("value" => to_int(table.x_max_extent))
184
+ xml.caretSlopeRise("value" => to_int(table.caret_slope_rise))
185
+ xml.caretSlopeRun("value" => to_int(table.caret_slope_run))
186
+ xml.caretOffset("value" => to_int(table.caret_offset))
187
+ xml.reserved0("value" => 0)
188
+ xml.reserved1("value" => 0)
189
+ xml.reserved2("value" => 0)
190
+ xml.reserved3("value" => 0)
191
+ xml.metricDataFormat("value" => to_int(table.metric_data_format))
192
+ xml.numberOfHMetrics("value" => to_int(table.num_of_long_hor_metrics))
193
+ end
194
+ end
195
+
196
+ # Generate maxp table XML
197
+ #
198
+ # @param xml [Nokogiri::XML::Builder] XML builder
199
+ # @param table [Tables::Maxp] Maxp table
200
+ # @return [void]
201
+ def generate_maxp_table(xml, table)
202
+ xml.maxp do
203
+ xml.comment(" Most of this table will be recalculated by the compiler ")
204
+ version = to_int(table.version)
205
+ xml.tableVersion("value" => format_hex(version))
206
+ xml.numGlyphs("value" => to_int(table.num_glyphs))
207
+
208
+ if version >= 0x00010000
209
+ xml.maxPoints("value" => to_int(table.max_points))
210
+ xml.maxContours("value" => to_int(table.max_contours))
211
+ xml.maxCompositePoints("value" => to_int(table.max_component_points))
212
+ xml.maxCompositeContours("value" => to_int(table.max_component_contours))
213
+ xml.maxZones("value" => to_int(table.max_zones))
214
+ xml.maxTwilightPoints("value" => to_int(table.max_twilight_points))
215
+ xml.maxStorage("value" => to_int(table.max_storage))
216
+ xml.maxFunctionDefs("value" => to_int(table.max_function_defs))
217
+ xml.maxInstructionDefs("value" => to_int(table.max_instruction_defs))
218
+ xml.maxStackElements("value" => to_int(table.max_stack_elements))
219
+ xml.maxSizeOfInstructions("value" => to_int(table.max_size_of_instructions))
220
+ xml.maxComponentElements("value" => to_int(table.max_component_elements))
221
+ xml.maxComponentDepth("value" => to_int(table.max_component_depth))
222
+ end
223
+ end
224
+ end
225
+
226
+ # Generate post table XML
227
+ #
228
+ # @param xml [Nokogiri::XML::Builder] XML builder
229
+ # @param table [Tables::Post] Post table
230
+ # @return [void]
231
+ def generate_post_table(xml, table)
232
+ xml.post do
233
+ xml.formatType("value" => format_fixed(to_int(table.format)))
234
+ xml.italicAngle("value" => format_fixed(to_int(table.italic_angle)))
235
+ xml.underlinePosition("value" => to_int(table.underline_position))
236
+ xml.underlineThickness("value" => to_int(table.underline_thickness))
237
+ xml.isFixedPitch("value" => to_int(table.is_fixed_pitch))
238
+ xml.minMemType42("value" => to_int(table.min_mem_type42))
239
+ xml.maxMemType42("value" => to_int(table.max_mem_type42))
240
+ xml.minMemType1("value" => to_int(table.min_mem_type1))
241
+ xml.maxMemType1("value" => to_int(table.max_mem_type1))
242
+ end
243
+ end
244
+
245
+ # Generate name table XML
246
+ #
247
+ # @param xml [Nokogiri::XML::Builder] XML builder
248
+ # @param table [Tables::Name] Name table
249
+ # @return [void]
250
+ def generate_name_table(xml, table)
251
+ xml.name do
252
+ table.name_records.each do |record|
253
+ xml.namerecord(
254
+ "nameID" => to_int(record.name_id),
255
+ "platformID" => to_int(record.platform_id),
256
+ "platEncID" => to_int(record.encoding_id),
257
+ "langID" => format_hex(to_int(record.language_id), width: 3),
258
+ ) do
259
+ xml.text(record.string)
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ # Generate OS/2 table XML
266
+ #
267
+ # @param xml [Nokogiri::XML::Builder] XML builder
268
+ # @param table [Tables::Os2] OS/2 table
269
+ # @return [void]
270
+ def generate_os2_table(xml, table)
271
+ # OS/2 requires special handling due to slash in tag name
272
+ # Generate it as a string and insert into the parent
273
+ generate_binary_table(xml, "OS/2", table)
274
+ end
275
+
276
+ # Helper to add element with value attribute
277
+ #
278
+ # @param parent [Nokogiri::XML::Element] Parent element
279
+ # @param name [String] Element name
280
+ # @param value [Object] Value
281
+ # @param doc [Nokogiri::XML::Document] Document
282
+ # @return [void]
283
+ def add_element_with_value(parent, name, value, doc)
284
+ elem = doc.create_element(name)
285
+ elem["value"] = value.to_s
286
+ parent.add_child(elem)
287
+ end
288
+
289
+ # Generate binary table as hexdata
290
+ #
291
+ # @param xml [Nokogiri::XML::Builder] XML builder
292
+ # @param tag [String] Table tag
293
+ # @param table [Object] Table object
294
+ # @return [void]
295
+ def generate_binary_table(xml, tag, table)
296
+ binary_data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
297
+ xml.send(tag.to_sym) do
298
+ xml.hexdata do
299
+ xml.text("\n #{format_hex_data(binary_data)}\n ")
300
+ end
301
+ end
302
+ end
303
+
304
+ # Generate binary table from raw data
305
+ #
306
+ # @param xml [Nokogiri::XML::Builder] XML builder
307
+ # @param tag [String] Table tag
308
+ # @param data [String] Raw binary data
309
+ # @return [void]
310
+ def generate_binary_table_from_data(xml, tag, data)
311
+ # Remove trailing space from tag for XML element name
312
+ clean_tag = tag.strip
313
+ xml.send(clean_tag.to_sym) do
314
+ xml.hexdata do
315
+ xml.text("\n #{format_hex_data(data)}\n ")
316
+ end
317
+ end
318
+ end
319
+
320
+ # Generate cmap table XML (simplified for now)
321
+ #
322
+ # @param xml [Nokogiri::XML::Builder] XML builder
323
+ # @param table [Object] Cmap table
324
+ # @return [void]
325
+ def generate_cmap_table(xml, table)
326
+ generate_binary_table(xml, "cmap", table)
327
+ end
328
+
329
+ # Generate loca table XML
330
+ #
331
+ # @param xml [Nokogiri::XML::Builder] XML builder
332
+ # @param table [Object] Loca table
333
+ # @return [void]
334
+ def generate_loca_table(xml, _table)
335
+ xml.loca do
336
+ xml.comment(" The 'loca' table will be calculated by the compiler ")
337
+ end
338
+ end
339
+
340
+ # Generate glyf table XML (simplified for now)
341
+ #
342
+ # @param xml [Nokogiri::XML::Builder] XML builder
343
+ # @param table [Object] Glyf table
344
+ # @return [void]
345
+ def generate_glyf_table(xml, table)
346
+ generate_binary_table(xml, "glyf", table)
347
+ end
348
+
349
+ # Generate CFF table XML (simplified for now)
350
+ #
351
+ # @param xml [Nokogiri::XML::Builder] XML builder
352
+ # @param table [Object] CFF table
353
+ # @return [void]
354
+ def generate_cff_table(xml, table)
355
+ generate_binary_table(xml, "CFF", table)
356
+ end
357
+
358
+ # Generate hmtx table XML (simplified for now)
359
+ #
360
+ # @param xml [Nokogiri::XML::Builder] XML builder
361
+ # @param table [Object] Hmtx table
362
+ # @return [void]
363
+ def generate_hmtx_table(xml, table)
364
+ generate_binary_table(xml, "hmtx", table)
365
+ end
366
+
367
+ # Generate fvar table XML
368
+ #
369
+ # @param xml [Nokogiri::XML::Builder] XML builder
370
+ # @param table [Tables::Fvar] Fvar table
371
+ # @return [void]
372
+ def generate_fvar_table(xml, table)
373
+ xml.fvar do
374
+ xml.Version("major" => to_int(table.major_version),
375
+ "minor" => to_int(table.minor_version))
376
+
377
+ table.axes.each do |axis|
378
+ xml.Axis do
379
+ xml.AxisTag axis.axis_tag
380
+ xml.MinValue to_int(axis.min_value) / 65536.0
381
+ xml.DefaultValue to_int(axis.default_value) / 65536.0
382
+ xml.MaxValue to_int(axis.max_value) / 65536.0
383
+ xml.AxisNameID to_int(axis.axis_name_id)
384
+ end
385
+ end
386
+ end
387
+ end
388
+
389
+ # Generate variation table XML (gvar, cvar, HVAR, etc.)
390
+ #
391
+ # @param xml [Nokogiri::XML::Builder] XML builder
392
+ # @param tag [String] Table tag
393
+ # @param table [Object] Variation table
394
+ # @return [void]
395
+ def generate_variation_table(xml, tag, table)
396
+ generate_binary_table(xml, tag, table)
397
+ end
398
+
399
+ # Select tables to generate
400
+ #
401
+ # @param table_list [Symbol, Array<String>] :all or list of tags
402
+ # @return [Array<String>] Table tags to generate
403
+ def select_tables(table_list)
404
+ if table_list == :all
405
+ @font.table_names
406
+ else
407
+ available = @font.table_names
408
+ requested = Array(table_list).map(&:to_s)
409
+ # Map CFF to "CFF " if needed
410
+ requested = requested.map do |tag|
411
+ if tag == "CFF" && !available.include?("CFF") && available.include?("CFF ")
412
+ "CFF "
413
+ else
414
+ tag
415
+ end
416
+ end
417
+ requested.select { |tag| available.include?(tag) }
418
+ end
419
+ end
420
+
421
+ # Get number of glyphs
422
+ #
423
+ # @return [Integer] Number of glyphs
424
+ def glyph_count
425
+ maxp = @font.table("maxp")
426
+ maxp ? to_int(maxp.num_glyphs) : 0
427
+ end
428
+
429
+ # Get glyph name by ID
430
+ #
431
+ # @param glyph_id [Integer] Glyph ID
432
+ # @return [String] Glyph name
433
+ def get_glyph_name(glyph_id)
434
+ post = @font.table("post")
435
+ if post.respond_to?(:glyph_names) && post.glyph_names
436
+ post.glyph_names[glyph_id] || ".notdef"
437
+ elsif glyph_id.zero?
438
+ ".notdef"
439
+ else
440
+ "glyph#{glyph_id.to_s.rjust(5, '0')}"
441
+ end
442
+ end
443
+
444
+ # Format SFNT version
445
+ #
446
+ # @param version [Integer] SFNT version
447
+ # @return [String] Formatted version as escaped bytes
448
+ def format_sfnt_version(version)
449
+ # Format as 4 bytes for TTX compatibility
450
+ bytes = [version].pack("N").bytes
451
+ "\\x#{bytes.map { |b| b.to_s(16).rjust(2, '0') }.join('\\x')}"
452
+ end
453
+
454
+ # Format fixed-point number (16.16)
455
+ #
456
+ # @param value [Integer] Fixed-point value
457
+ # @return [String] Decimal string
458
+ def format_fixed(value)
459
+ result = value.to_f / 65536.0
460
+ # Format with minimal decimal places
461
+ if result == result.to_i
462
+ "#{result.to_i}.0"
463
+ else
464
+ result.to_s
465
+ end
466
+ end
467
+
468
+ # Format hex value
469
+ #
470
+ # @param value [Integer] Integer value
471
+ # @param width [Integer] Minimum hex width
472
+ # @return [String] Hex string (e.g., "0x1234")
473
+ def format_hex(value, width: 8)
474
+ int_value = value.respond_to?(:to_i) ? value.to_i : value
475
+ "0x#{int_value.to_s(16).rjust(width, '0')}"
476
+ end
477
+
478
+ # Format binary flags
479
+ #
480
+ # @param value [Integer] Integer value
481
+ # @param bits [Integer] Number of bits
482
+ # @return [String] Binary string with spaces every 8 bits
483
+ def format_binary_flags(value, bits)
484
+ binary = value.to_s(2).rjust(bits, "0")
485
+ # Add spaces every 8 bits from left
486
+ binary.scan(/.{1,8}/).join(" ")
487
+ end
488
+
489
+ # Format timestamp
490
+ #
491
+ # @param timestamp [Integer] Mac timestamp (seconds since 1904-01-01)
492
+ # @return [String] Human-readable date string
493
+ def format_timestamp(timestamp)
494
+ # Mac epoch: Jan 1, 1904 00:00:00 UTC
495
+ mac_epoch = Time.utc(1904, 1, 1)
496
+ time = mac_epoch + timestamp
497
+ time.strftime("%a %b %e %H:%M:%S %Y")
498
+ rescue StandardError
499
+ "Invalid Date"
500
+ end
501
+
502
+ # Format binary data as hex
503
+ #
504
+ # @param data [String] Binary data
505
+ # @return [String] Hex string with newlines every 32 bytes
506
+ def format_hex_data(data)
507
+ hex = data.unpack1("H*")
508
+ # Format in lines of 64 hex chars (32 bytes) for readability
509
+ hex.scan(/.{1,64}/).join("\n ")
510
+ end
511
+
512
+ # Format output XML
513
+ #
514
+ # @param xml [String] Raw XML
515
+ # @return [String] Formatted XML
516
+ def format_output(xml)
517
+ if @pretty
518
+ doc = Nokogiri::XML(xml)
519
+ doc.to_xml(indent: @indent)
520
+ else
521
+ # Remove extra whitespace for compact format
522
+ xml.gsub(/>\s+</, "><").gsub(/\n\s*/, "")
523
+ end
524
+ end
525
+ end
526
+ end
527
+ end