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,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "constants"
5
+
6
+ module Fontisan
7
+ # FontWriter handles writing font binaries from table data
8
+ #
9
+ # This class assembles a complete font binary from individual table data,
10
+ # including:
11
+ # - Writing the sfnt header (offset table)
12
+ # - Building the table directory
13
+ # - Writing table data with proper 4-byte alignment
14
+ # - Calculating all checksums
15
+ # - Updating the head table's checksumAdjustment field
16
+ #
17
+ # @example Write font from tables
18
+ # tables = {
19
+ # 'head' => head_data,
20
+ # 'hhea' => hhea_data,
21
+ # 'maxp' => maxp_data,
22
+ # 'hmtx' => hmtx_data,
23
+ # 'cmap' => cmap_data
24
+ # }
25
+ # binary = FontWriter.write_font(tables)
26
+ # File.binwrite('subset.ttf', binary)
27
+ #
28
+ # @example Write to file directly
29
+ # FontWriter.write_to_file(tables, 'subset.ttf')
30
+ #
31
+ # Reference: OpenType spec section on font file structure
32
+ class FontWriter
33
+ # OpenType/TrueType table ordering (recommended order)
34
+ TRUETYPE_TABLE_ORDER = %w[
35
+ head hhea maxp OS/2 hmtx LTSH VDMX hdmx cmap fpgm prep cvt
36
+ loca glyf kern name post gasp PCLT DSIG
37
+ ].freeze
38
+
39
+ # OpenType/CFF table ordering (recommended order)
40
+ OPENTYPE_TABLE_ORDER = %w[
41
+ head hhea maxp OS/2 name cmap post CFF CFF2
42
+ ].freeze
43
+
44
+ # Write complete font binary from table data
45
+ #
46
+ # @param tables_hash [Hash<String, String>] Map of table tag to binary data
47
+ # @param sfnt_version [Integer, nil] Font sfnt version (0x00010000 for TrueType,
48
+ # 0x4F54544F for OpenType/CFF). If nil, auto-detects based on tables.
49
+ # @return [String] Complete font binary
50
+ #
51
+ # @example
52
+ # binary = FontWriter.write_font(tables_hash)
53
+ # binary = FontWriter.write_font(tables_hash, sfnt_version: 0x4F54544F)
54
+ def self.write_font(tables_hash, sfnt_version: nil)
55
+ # Auto-detect sfnt version if not provided
56
+ sfnt_version ||= detect_sfnt_version(tables_hash)
57
+ new(tables_hash, sfnt_version: sfnt_version).write
58
+ end
59
+
60
+ # Detect sfnt version based on table presence
61
+ #
62
+ # @param tables_hash [Hash<String, String>] Map of table tag to binary data
63
+ # @return [Integer] Detected sfnt version
64
+ def self.detect_sfnt_version(tables_hash)
65
+ if tables_hash.key?("CFF ") || tables_hash.key?("CFF2")
66
+ 0x4F54544F # 'OTTO' for OpenType/CFF
67
+ else
68
+ 0x00010000 # 1.0 for TrueType
69
+ end
70
+ end
71
+
72
+ # Write font binary to file
73
+ #
74
+ # @param tables_hash [Hash<String, String>] Map of table tag to binary data
75
+ # @param path [String] Output file path
76
+ # @param sfnt_version [Integer, nil] Font sfnt version. If nil, auto-detects.
77
+ # @return [Integer] Number of bytes written
78
+ #
79
+ # @example
80
+ # FontWriter.write_to_file(tables_hash, 'output.ttf')
81
+ def self.write_to_file(tables_hash, path, sfnt_version: nil)
82
+ binary = write_font(tables_hash, sfnt_version: sfnt_version)
83
+
84
+ # Create parent directories if they don't exist
85
+ dir = File.dirname(path)
86
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
87
+
88
+ File.binwrite(path, binary)
89
+ end
90
+
91
+ # Initialize writer with table data
92
+ #
93
+ # @param tables_hash [Hash<String, String>] Map of table tag to binary data
94
+ # @param sfnt_version [Integer] Font sfnt version
95
+ def initialize(tables_hash, sfnt_version: 0x00010000)
96
+ @tables = tables_hash
97
+ @sfnt_version = sfnt_version
98
+ end
99
+
100
+ # Write the complete font binary
101
+ #
102
+ # @return [String] Complete font binary
103
+ def write
104
+ # Order tables according to format
105
+ ordered_tags = order_tables
106
+
107
+ # Calculate table offsets
108
+ table_entries = calculate_table_entries(ordered_tags)
109
+
110
+ # Build font binary
111
+ font_data = String.new(encoding: Encoding::BINARY)
112
+
113
+ # Write offset table (sfnt header)
114
+ font_data << write_offset_table(table_entries.size)
115
+
116
+ # Write table directory (ALL entries first)
117
+ table_entries.each do |entry|
118
+ font_data << write_table_entry(entry)
119
+ end
120
+
121
+ # Write table data (ALL data after directory)
122
+ table_entries.each do |entry|
123
+ font_data << entry[:data]
124
+ font_data << entry[:padding]
125
+ end
126
+
127
+ # Calculate and update head table checksum adjustment
128
+ update_checksum_adjustment!(font_data, table_entries)
129
+
130
+ font_data
131
+ end
132
+
133
+ private
134
+
135
+ # Order tables according to recommended order
136
+ #
137
+ # @return [Array<String>] Ordered table tags
138
+ def order_tables
139
+ # Determine if this is OpenType/CFF or TrueType
140
+ is_cff = @tables.key?("CFF ") || @tables.key?("CFF2")
141
+ order = is_cff ? OPENTYPE_TABLE_ORDER : TRUETYPE_TABLE_ORDER
142
+
143
+ # Start with tables in recommended order that exist
144
+ ordered = order.select { |tag| @tables.key?(tag) }
145
+
146
+ # Add any remaining tables not in the recommended order
147
+ remaining = @tables.keys - ordered
148
+ ordered + remaining.sort
149
+ end
150
+
151
+ # Calculate table directory entries with offsets
152
+ #
153
+ # @param tags [Array<String>] Ordered table tags
154
+ # @return [Array<Hash>] Table entries with offsets, checksums, data
155
+ def calculate_table_entries(tags)
156
+ # Calculate offset for first table
157
+ # Offset table (12 bytes) + table directory (16 bytes per table)
158
+ offset = 12 + (tags.size * 16)
159
+
160
+ entries = []
161
+
162
+ tags.each do |tag|
163
+ data = @tables[tag]
164
+ checksum = calculate_table_checksum(data)
165
+
166
+ # Calculate padding to 4-byte boundary
167
+ padding_length = (4 - (data.bytesize % 4)) % 4
168
+ padding = "\0" * padding_length
169
+
170
+ entries << {
171
+ tag: tag,
172
+ checksum: checksum,
173
+ offset: offset,
174
+ length: data.bytesize,
175
+ data: data,
176
+ padding: padding,
177
+ }
178
+
179
+ # Update offset for next table
180
+ offset += data.bytesize + padding_length
181
+ end
182
+
183
+ entries
184
+ end
185
+
186
+ # Write offset table (sfnt header)
187
+ #
188
+ # @param num_tables [Integer] Number of tables
189
+ # @return [String] Offset table binary data
190
+ def write_offset_table(num_tables)
191
+ # Calculate search range, entry selector, and range shift
192
+ # searchRange = (maximum power of 2 <= num_tables) * 16
193
+ # entrySelector = log2(maximum power of 2 <= num_tables)
194
+ # rangeShift = num_tables * 16 - searchRange
195
+
196
+ max_power = 0
197
+ n = num_tables
198
+ while n > 1
199
+ n >>= 1
200
+ max_power += 1
201
+ end
202
+
203
+ search_range = (1 << max_power) * 16
204
+ entry_selector = max_power
205
+ range_shift = (num_tables * 16) - search_range
206
+
207
+ [
208
+ @sfnt_version, # uint32 - sfnt version
209
+ num_tables, # uint16 - number of tables
210
+ search_range, # uint16 - search range
211
+ entry_selector, # uint16 - entry selector
212
+ range_shift, # uint16 - range shift
213
+ ].pack("N n n n n")
214
+ end
215
+
216
+ # Write a table directory entry
217
+ #
218
+ # @param entry [Hash] Table entry with tag, checksum, offset, length
219
+ # @return [String] Table directory entry binary data
220
+ def write_table_entry(entry)
221
+ [
222
+ entry[:tag], # char[4] - table tag
223
+ entry[:checksum], # uint32 - checksum
224
+ entry[:offset], # uint32 - offset
225
+ entry[:length], # uint32 - length
226
+ ].pack("a4 N N N")
227
+ end
228
+
229
+ # Calculate checksum for a table
230
+ #
231
+ # The checksum is calculated by summing all uint32 values in the table.
232
+ # The table is padded with zeros to a multiple of 4 bytes if necessary.
233
+ #
234
+ # @param data [String] Table binary data
235
+ # @return [Integer] Table checksum
236
+ def calculate_table_checksum(data)
237
+ # Pad to 4-byte boundary
238
+ padded_data = data.dup
239
+ padding_length = (4 - (data.bytesize % 4)) % 4
240
+ padded_data << ("\0" * padding_length) if padding_length.positive?
241
+
242
+ # Sum all uint32 values
243
+ sum = 0
244
+ (0...padded_data.bytesize).step(4) do |i|
245
+ value = padded_data[i, 4].unpack1("N")
246
+ sum = (sum + value) & 0xFFFFFFFF
247
+ end
248
+
249
+ sum
250
+ end
251
+
252
+ # Update head table checksum adjustment
253
+ #
254
+ # The checksumAdjustment field in the head table (at offset 8) must be
255
+ # set such that the sum of all uint32 values in the entire font equals
256
+ # the magic number 0xB1B0AFBA.
257
+ #
258
+ # @param font_data [String] Complete font binary (modified in place)
259
+ # @param table_entries [Array<Hash>] Table entries
260
+ # @return [void]
261
+ def update_checksum_adjustment!(font_data, table_entries)
262
+ # Find head table entry
263
+ head_entry = table_entries.find { |e| e[:tag] == "head" }
264
+ return unless head_entry
265
+
266
+ # Calculate font checksum (with head checksumAdjustment set to 0)
267
+ # The head table at offset 8 should already be 0 from original table
268
+ font_checksum = calculate_font_checksum(font_data)
269
+
270
+ # Calculate adjustment
271
+ adjustment = (Constants::CHECKSUM_ADJUSTMENT_MAGIC - font_checksum) & 0xFFFFFFFF
272
+
273
+ # Update head table checksumAdjustment field (offset 8 in head table)
274
+ head_offset = head_entry[:offset]
275
+ checksum_offset = head_offset + 8
276
+
277
+ # Write adjustment as uint32 big-endian
278
+ font_data[checksum_offset, 4] = [adjustment].pack("N")
279
+ end
280
+
281
+ # Calculate checksum of entire font file
282
+ #
283
+ # @param data [String] Complete font binary
284
+ # @return [Integer] Font checksum
285
+ def calculate_font_checksum(data)
286
+ # Pad to 4-byte boundary
287
+ padded_data = data.dup
288
+ padding_length = (4 - (data.bytesize % 4)) % 4
289
+ padded_data << ("\0" * padding_length) if padding_length.positive?
290
+
291
+ # Sum all uint32 values
292
+ sum = 0
293
+ (0...padded_data.bytesize).step(4) do |i|
294
+ value = padded_data[i, 4].unpack1("N")
295
+ sum = (sum + value) & 0xFFFFFFFF
296
+ end
297
+
298
+ sum
299
+ end
300
+ end
301
+ end
@@ -36,6 +36,12 @@ module Fontisan
36
36
  format_all_scripts_features_info(model)
37
37
  when Models::FeaturesInfo
38
38
  format_features_info(model)
39
+ when Models::CollectionListInfo
40
+ format_collection_list_info(model)
41
+ when Models::FontSummary
42
+ format_font_summary(model)
43
+ when Models::CollectionInfo
44
+ format_collection_info(model)
39
45
  else
40
46
  model.to_s
41
47
  end
@@ -309,6 +315,102 @@ module Fontisan
309
315
  type += " (Variable)" if is_variable
310
316
  type
311
317
  end
318
+
319
+ # Format CollectionListInfo as human-readable text.
320
+ #
321
+ # @param info [Models::CollectionListInfo] Collection list information to format
322
+ # @return [String] Formatted text with fonts in collection
323
+ def format_collection_list_info(info)
324
+ lines = []
325
+
326
+ lines << "Collection: #{info.collection_path}"
327
+ lines << "Fonts: #{info.num_fonts}"
328
+ lines << ""
329
+
330
+ info.fonts.each do |font|
331
+ lines << "#{font.index}. #{font.family_name} #{font.subfamily_name}"
332
+ lines << " PostScript: #{font.postscript_name}"
333
+ lines << " Format: #{font.font_format}"
334
+ lines << " Glyphs: #{font.num_glyphs}, Tables: #{font.num_tables}"
335
+ lines << "" unless font.index == info.num_fonts - 1
336
+ end
337
+
338
+ lines.join("\n")
339
+ end
340
+
341
+ # Format FontSummary as human-readable text.
342
+ #
343
+ # @param summary [Models::FontSummary] Font summary to format
344
+ # @return [String] Formatted text with font summary
345
+ def format_font_summary(summary)
346
+ lines = []
347
+
348
+ lines << "Font: #{summary.font_path}"
349
+ lines << "Family: #{summary.family_name} #{summary.subfamily_name}"
350
+ lines << "Format: #{summary.font_format}"
351
+ lines << "Glyphs: #{summary.num_glyphs}"
352
+ lines << "Tables: #{summary.num_tables}"
353
+
354
+ lines.join("\n")
355
+ end
356
+
357
+ # Format CollectionInfo as human-readable text.
358
+ #
359
+ # @param info [Models::CollectionInfo] Collection information to format
360
+ # @return [String] Formatted text with collection metadata
361
+ def format_collection_info(info)
362
+ lines = []
363
+
364
+ # Header section
365
+ lines << "=== Collection Information ==="
366
+ lines << ""
367
+ lines << "File: #{info.collection_path}"
368
+ lines << "Format: #{info.collection_format}"
369
+ lines << "Size: #{format_bytes(info.file_size_bytes)}"
370
+ lines << ""
371
+
372
+ # Header details
373
+ lines << "=== Header ==="
374
+ lines << "Tag: #{info.ttc_tag}"
375
+ lines << "Version: #{info.version_string} (#{info.version_hex})"
376
+ lines << "Number of fonts: #{info.num_fonts}"
377
+ lines << ""
378
+
379
+ # Font offsets
380
+ lines << "=== Font Offsets ==="
381
+ info.font_offsets.each_with_index do |offset, index|
382
+ lines << Kernel.format(" %d. Offset: %8d (0x%08X)",
383
+ index, offset, offset)
384
+ end
385
+ lines << ""
386
+
387
+ # Table sharing statistics
388
+ if info.table_sharing
389
+ lines << "=== Table Sharing ==="
390
+ lines << "Shared tables: #{info.table_sharing.shared_tables}"
391
+ lines << "Unique tables: #{info.table_sharing.unique_tables}"
392
+ lines << "Sharing: #{format_float(info.table_sharing.sharing_percentage)}%"
393
+ lines << "Space saved: #{format_bytes(info.table_sharing.space_saved_bytes)}"
394
+ end
395
+
396
+ lines.join("\n")
397
+ end
398
+
399
+ # Format bytes for human-readable display.
400
+ #
401
+ # @param bytes [Integer] Number of bytes
402
+ # @return [String] Formatted byte size
403
+ def format_bytes(bytes)
404
+ return "0 B" if bytes.nil? || bytes.zero?
405
+
406
+ if bytes < 1024
407
+ "#{bytes} B"
408
+ elsif bytes < 1024 * 1024
409
+ "#{(bytes / 1024.0).round(2)} KB"
410
+ else
411
+ "#{(bytes / (1024.0 * 1024)).round(2)} MB"
412
+ end
413
+ end
312
414
  end
313
415
  end
314
416
  end