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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../binary/base_record"
4
+ require_relative "variation_common"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ # Parser for the 'VVAR' (Vertical Metrics Variations) table
9
+ #
10
+ # The VVAR table provides variation data for vertical metrics including:
11
+ # - Advance height variations
12
+ # - Top side bearing (TSB) variations
13
+ #
14
+ # This table uses the ItemVariationStore structure to efficiently store
15
+ # delta values for different regions in the design space.
16
+ #
17
+ # Reference: OpenType specification, VVAR table
18
+ #
19
+ # @example Reading a VVAR table
20
+ # data = font.table_data("VVAR")
21
+ # vvar = Fontisan::Tables::Vvar.read(data)
22
+ # advance_deltas = vvar.advance_height_deltas(glyph_id, coordinates)
23
+ class Vvar < Binary::BaseRecord
24
+ uint16 :major_version
25
+ uint16 :minor_version
26
+ uint32 :item_variation_store_offset
27
+ uint32 :advance_height_mapping_offset
28
+ uint32 :tsb_mapping_offset
29
+ uint32 :bsb_mapping_offset
30
+ uint32 :v_org_mapping_offset
31
+
32
+ # Get version as a float
33
+ #
34
+ # @return [Float] Version number (e.g., 1.0)
35
+ def version
36
+ major_version + (minor_version / 10.0)
37
+ end
38
+
39
+ # Parse the item variation store
40
+ #
41
+ # @return [VariationCommon::ItemVariationStore, nil] Variation store
42
+ def item_variation_store
43
+ return @item_variation_store if defined?(@item_variation_store)
44
+ return @item_variation_store = nil if item_variation_store_offset.zero?
45
+
46
+ data = raw_data
47
+ offset = item_variation_store_offset
48
+
49
+ return @item_variation_store = nil if offset >= data.bytesize
50
+
51
+ store_data = data.byteslice(offset..-1)
52
+ @item_variation_store = VariationCommon::ItemVariationStore.read(store_data)
53
+ rescue StandardError => e
54
+ warn "Failed to parse VVAR item variation store: #{e.message}"
55
+ @item_variation_store = nil
56
+ end
57
+
58
+ # Parse advance height mapping
59
+ #
60
+ # @return [VariationCommon::DeltaSetIndexMap, nil] Advance height map
61
+ def advance_height_mapping
62
+ return @advance_height_mapping if defined?(@advance_height_mapping)
63
+ return @advance_height_mapping = nil if advance_height_mapping_offset.zero?
64
+
65
+ data = raw_data
66
+ offset = advance_height_mapping_offset
67
+
68
+ return @advance_height_mapping = nil if offset >= data.bytesize
69
+
70
+ map_data = data.byteslice(offset..-1)
71
+ @advance_height_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
72
+ rescue StandardError => e
73
+ warn "Failed to parse VVAR advance height mapping: #{e.message}"
74
+ @advance_height_mapping = nil
75
+ end
76
+
77
+ # Parse TSB (top side bearing) mapping
78
+ #
79
+ # @return [VariationCommon::DeltaSetIndexMap, nil] TSB map
80
+ def tsb_mapping
81
+ return @tsb_mapping if defined?(@tsb_mapping)
82
+ return @tsb_mapping = nil if tsb_mapping_offset.zero?
83
+
84
+ data = raw_data
85
+ offset = tsb_mapping_offset
86
+
87
+ return @tsb_mapping = nil if offset >= data.bytesize
88
+
89
+ map_data = data.byteslice(offset..-1)
90
+ @tsb_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
91
+ rescue StandardError => e
92
+ warn "Failed to parse VVAR TSB mapping: #{e.message}"
93
+ @tsb_mapping = nil
94
+ end
95
+
96
+ # Parse BSB (bottom side bearing) mapping
97
+ #
98
+ # @return [VariationCommon::DeltaSetIndexMap, nil] BSB map
99
+ def bsb_mapping
100
+ return @bsb_mapping if defined?(@bsb_mapping)
101
+ return @bsb_mapping = nil if bsb_mapping_offset.zero?
102
+
103
+ data = raw_data
104
+ offset = bsb_mapping_offset
105
+
106
+ return @bsb_mapping = nil if offset >= data.bytesize
107
+
108
+ map_data = data.byteslice(offset..-1)
109
+ @bsb_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
110
+ rescue StandardError => e
111
+ warn "Failed to parse VVAR BSB mapping: #{e.message}"
112
+ @bsb_mapping = nil
113
+ end
114
+
115
+ # Parse vertical origin mapping
116
+ #
117
+ # @return [VariationCommon::DeltaSetIndexMap, nil] VOrig map
118
+ def v_org_mapping
119
+ return @v_org_mapping if defined?(@v_org_mapping)
120
+ return @v_org_mapping = nil if v_org_mapping_offset.zero?
121
+
122
+ data = raw_data
123
+ offset = v_org_mapping_offset
124
+
125
+ return @v_org_mapping = nil if offset >= data.bytesize
126
+
127
+ map_data = data.byteslice(offset..-1)
128
+ @v_org_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
129
+ rescue StandardError => e
130
+ warn "Failed to parse VVAR vertical origin mapping: #{e.message}"
131
+ @v_org_mapping = nil
132
+ end
133
+
134
+ # Get advance height delta set for a glyph
135
+ #
136
+ # @param glyph_id [Integer] Glyph ID
137
+ # @return [Array<Integer>, nil] Delta values or nil
138
+ def advance_height_delta_set(glyph_id)
139
+ return nil unless item_variation_store
140
+
141
+ # If no mapping, use glyph_id directly
142
+ if advance_height_mapping.nil?
143
+ return item_variation_store.delta_set(0, glyph_id)
144
+ end
145
+
146
+ # Use mapping to get delta set indices
147
+ map_data = advance_height_mapping.map_data
148
+ return nil if glyph_id >= map_data.length
149
+
150
+ delta_index = map_data[glyph_id]
151
+ outer_index = (delta_index >> 16) & 0xFFFF
152
+ inner_index = delta_index & 0xFFFF
153
+
154
+ item_variation_store.delta_set(outer_index, inner_index)
155
+ end
156
+
157
+ # Get TSB delta set for a glyph
158
+ #
159
+ # @param glyph_id [Integer] Glyph ID
160
+ # @return [Array<Integer>, nil] Delta values or nil
161
+ def tsb_delta_set(glyph_id)
162
+ return nil unless item_variation_store
163
+
164
+ # If no mapping, use glyph_id directly
165
+ if tsb_mapping.nil?
166
+ return item_variation_store.delta_set(0, glyph_id)
167
+ end
168
+
169
+ # Use mapping to get delta set indices
170
+ map_data = tsb_mapping.map_data
171
+ return nil if glyph_id >= map_data.length
172
+
173
+ delta_index = map_data[glyph_id]
174
+ outer_index = (delta_index >> 16) & 0xFFFF
175
+ inner_index = delta_index & 0xFFFF
176
+
177
+ item_variation_store.delta_set(outer_index, inner_index)
178
+ end
179
+
180
+ # Get BSB delta set for a glyph
181
+ #
182
+ # @param glyph_id [Integer] Glyph ID
183
+ # @return [Array<Integer>, nil] Delta values or nil
184
+ def bsb_delta_set(glyph_id)
185
+ return nil unless item_variation_store
186
+
187
+ # If no mapping, use glyph_id directly
188
+ if bsb_mapping.nil?
189
+ return item_variation_store.delta_set(0, glyph_id)
190
+ end
191
+
192
+ # Use mapping to get delta set indices
193
+ map_data = bsb_mapping.map_data
194
+ return nil if glyph_id >= map_data.length
195
+
196
+ delta_index = map_data[glyph_id]
197
+ outer_index = (delta_index >> 16) & 0xFFFF
198
+ inner_index = delta_index & 0xFFFF
199
+
200
+ item_variation_store.delta_set(outer_index, inner_index)
201
+ end
202
+
203
+ # Get vertical origin delta set for a glyph
204
+ #
205
+ # @param glyph_id [Integer] Glyph ID
206
+ # @return [Array<Integer>, nil] Delta values or nil
207
+ def v_org_delta_set(glyph_id)
208
+ return nil unless item_variation_store
209
+
210
+ # If no mapping, use glyph_id directly
211
+ if v_org_mapping.nil?
212
+ return item_variation_store.delta_set(0, glyph_id)
213
+ end
214
+
215
+ # Use mapping to get delta set indices
216
+ map_data = v_org_mapping.map_data
217
+ return nil if glyph_id >= map_data.length
218
+
219
+ delta_index = map_data[glyph_id]
220
+ outer_index = (delta_index >> 16) & 0xFFFF
221
+ inner_index = delta_index & 0xFFFF
222
+
223
+ item_variation_store.delta_set(outer_index, inner_index)
224
+ end
225
+
226
+ # Check if table is valid
227
+ #
228
+ # @return [Boolean] True if valid
229
+ def valid?
230
+ major_version == 1 && minor_version.zero?
231
+ end
232
+ end
233
+ end
234
+ end
@@ -64,12 +64,13 @@ module Fontisan
64
64
  #
65
65
  # @param index [Integer] Index of the font (0-based)
66
66
  # @param io [IO] Open file handle
67
+ # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
67
68
  # @return [TrueTypeFont, nil] Font object or nil if index out of range
68
- def font(index, io)
69
+ def font(index, io, mode: LoadingModes::FULL)
69
70
  return nil if index >= num_fonts
70
71
 
71
72
  require_relative "true_type_font"
72
- TrueTypeFont.from_ttc(io, font_offsets[index])
73
+ TrueTypeFont.from_ttc(io, font_offsets[index], mode: mode)
73
74
  end
74
75
 
75
76
  # Get font count (Fontisan extension)
@@ -94,5 +95,158 @@ module Fontisan
94
95
  def version
95
96
  (major_version << 16) | minor_version
96
97
  end
98
+
99
+ # List all fonts in the collection with basic metadata
100
+ #
101
+ # Returns a CollectionListInfo model containing summaries of all fonts.
102
+ # This is the API method used by the `ls` command for collections.
103
+ #
104
+ # @param io [IO] Open file handle to read fonts from
105
+ # @return [CollectionListInfo] List of fonts with metadata
106
+ #
107
+ # @example List fonts in collection
108
+ # File.open("fonts.ttc", "rb") do |io|
109
+ # ttc = TrueTypeCollection.read(io)
110
+ # list = ttc.list_fonts(io)
111
+ # list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
112
+ # end
113
+ def list_fonts(io)
114
+ require_relative "models/collection_list_info"
115
+ require_relative "models/collection_font_summary"
116
+ require_relative "true_type_font"
117
+ require_relative "tables/name"
118
+
119
+ fonts = font_offsets.map.with_index do |offset, index|
120
+ font = TrueTypeFont.from_ttc(io, offset)
121
+
122
+ # Extract basic font info
123
+ name_table = font.table("name")
124
+ post_table = font.table("post")
125
+
126
+ family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
127
+ subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
128
+ postscript_name = name_table&.english_name(Tables::Name::POSTSCRIPT_NAME) || "Unknown"
129
+
130
+ # Determine font format
131
+ sfnt = font.header.sfnt_version
132
+ font_format = case sfnt
133
+ when 0x00010000, 0x74727565 # 0x74727565 = 'true'
134
+ "TrueType"
135
+ when 0x4F54544F # 'OTTO'
136
+ "OpenType"
137
+ else
138
+ "Unknown"
139
+ end
140
+
141
+ num_glyphs = post_table&.glyph_names&.length || 0
142
+ num_tables = font.table_names.length
143
+
144
+ Models::CollectionFontSummary.new(
145
+ index: index,
146
+ family_name: family_name,
147
+ subfamily_name: subfamily_name,
148
+ postscript_name: postscript_name,
149
+ font_format: font_format,
150
+ num_glyphs: num_glyphs,
151
+ num_tables: num_tables,
152
+ )
153
+ end
154
+
155
+ Models::CollectionListInfo.new(
156
+ collection_path: nil, # Will be set by command
157
+ num_fonts: num_fonts,
158
+ fonts: fonts,
159
+ )
160
+ end
161
+
162
+ # Get comprehensive collection metadata
163
+ #
164
+ # Returns a CollectionInfo model with header information, offsets,
165
+ # and table sharing statistics.
166
+ # This is the API method used by the `info` command for collections.
167
+ #
168
+ # @param io [IO] Open file handle to read fonts from
169
+ # @param path [String] Collection file path (for file size)
170
+ # @return [CollectionInfo] Collection metadata
171
+ #
172
+ # @example Get collection info
173
+ # File.open("fonts.ttc", "rb") do |io|
174
+ # ttc = TrueTypeCollection.read(io)
175
+ # info = ttc.collection_info(io, "fonts.ttc")
176
+ # puts "Version: #{info.version_string}"
177
+ # end
178
+ def collection_info(io, path)
179
+ require_relative "models/collection_info"
180
+ require_relative "models/table_sharing_info"
181
+
182
+ # Calculate table sharing statistics
183
+ table_sharing = calculate_table_sharing(io)
184
+
185
+ # Get file size
186
+ file_size = path ? File.size(path) : 0
187
+
188
+ Models::CollectionInfo.new(
189
+ collection_path: path,
190
+ collection_format: "TTC",
191
+ ttc_tag: tag,
192
+ major_version: major_version,
193
+ minor_version: minor_version,
194
+ num_fonts: num_fonts,
195
+ font_offsets: font_offsets.to_a,
196
+ file_size_bytes: file_size,
197
+ table_sharing: table_sharing,
198
+ )
199
+ end
200
+
201
+ private
202
+
203
+ # Calculate table sharing statistics
204
+ #
205
+ # Analyzes which tables are shared between fonts and calculates
206
+ # space savings from deduplication.
207
+ #
208
+ # @param io [IO] Open file handle
209
+ # @return [TableSharingInfo] Sharing statistics
210
+ def calculate_table_sharing(io)
211
+ require_relative "models/table_sharing_info"
212
+ require_relative "true_type_font"
213
+
214
+ # Extract all fonts
215
+ fonts = font_offsets.map do |offset|
216
+ TrueTypeFont.from_ttc(io, offset)
217
+ end
218
+
219
+ # Build table hash map (checksum -> size)
220
+ table_map = {}
221
+ total_table_size = 0
222
+
223
+ fonts.each do |font|
224
+ font.tables.each do |entry|
225
+ key = entry.checksum
226
+ size = entry.table_length
227
+ table_map[key] ||= size
228
+ total_table_size += size
229
+ end
230
+ end
231
+
232
+ # Count unique vs shared
233
+ unique_tables = table_map.size
234
+ total_tables = fonts.sum { |f| f.tables.length }
235
+ shared_tables = total_tables - unique_tables
236
+
237
+ # Calculate space saved
238
+ unique_size = table_map.values.sum
239
+ space_saved = total_table_size - unique_size
240
+
241
+ # Calculate sharing percentage
242
+ sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
243
+
244
+ Models::TableSharingInfo.new(
245
+ shared_tables: shared_tables,
246
+ unique_tables: unique_tables,
247
+ sharing_percentage: sharing_pct,
248
+ space_saved_bytes: space_saved,
249
+ )
250
+ end
97
251
  end
98
252
  end