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,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Subset
5
+ # Table-specific subsetting strategies
6
+ #
7
+ # This class provides methods for subsetting individual font tables according
8
+ # to the glyph mapping. Each table type has different subsetting requirements:
9
+ #
10
+ # - maxp: Update glyph count
11
+ # - hhea: Update horizontal metrics count
12
+ # - hmtx: Subset horizontal metrics
13
+ # - glyf: Subset glyph data and remap component references
14
+ # - loca: Rebuild glyph location index
15
+ # - cmap: Remap character to glyph mappings
16
+ # - post: Optionally drop glyph names
17
+ # - name: Pass through (no subsetting needed)
18
+ # - head: Update checksum adjustment (handled by FontWriter)
19
+ # - OS/2: Optionally prune Unicode ranges
20
+ #
21
+ # The subsetting process preserves font validity by updating all references
22
+ # and recalculating offsets and checksums.
23
+ #
24
+ # @example Subset a single table
25
+ # subsetter = TableSubsetter.new(font, mapping, options)
26
+ # maxp_data = subsetter.subset_maxp(maxp_table)
27
+ #
28
+ # @example Subset all tables
29
+ # subsetter = TableSubsetter.new(font, mapping, options)
30
+ # subset_tables = {}
31
+ # profile_tables.each do |tag|
32
+ # table = font.table(tag)
33
+ # subset_tables[tag] = subsetter.subset_table(tag, table) if table
34
+ # end
35
+ class TableSubsetter
36
+ # Font instance being subset
37
+ # @return [TrueTypeFont, OpenTypeFont]
38
+ attr_reader :font
39
+
40
+ # Glyph ID mapping (old GID → new GID)
41
+ # @return [GlyphMapping]
42
+ attr_reader :mapping
43
+
44
+ # Subsetting options
45
+ # @return [Options]
46
+ attr_reader :options
47
+
48
+ # Initialize table subsetter
49
+ #
50
+ # @param font [TrueTypeFont, OpenTypeFont] Font to subset
51
+ # @param mapping [GlyphMapping] Glyph ID mapping
52
+ # @param options [Options] Subsetting options
53
+ def initialize(font, mapping, options)
54
+ @font = font
55
+ @mapping = mapping
56
+ @options = options
57
+ @glyf_data = nil
58
+ @loca_offsets = nil
59
+ end
60
+
61
+ # Subset a table by tag
62
+ #
63
+ # Delegates to table-specific subsetting methods. Unknown tables
64
+ # are passed through unchanged.
65
+ #
66
+ # @param tag [String] Table tag (e.g., "glyf", "hmtx")
67
+ # @param table [Object] Parsed table object
68
+ # @return [String] Binary data of subset table
69
+ def subset_table(tag, table)
70
+ case tag
71
+ when "maxp"
72
+ subset_maxp(table)
73
+ when "hhea"
74
+ subset_hhea(table)
75
+ when "hmtx"
76
+ subset_hmtx(table)
77
+ when "loca"
78
+ subset_loca(table)
79
+ when "glyf"
80
+ subset_glyf(table)
81
+ when "cmap"
82
+ subset_cmap(table)
83
+ when "post"
84
+ subset_post(table)
85
+ when "name"
86
+ subset_name(table)
87
+ when "head"
88
+ subset_head(table)
89
+ when "OS/2"
90
+ subset_os2(table)
91
+ else
92
+ # Unknown tables pass through unchanged
93
+ font.table_data[tag]
94
+ end
95
+ end
96
+
97
+ # Subset maxp table (update numGlyphs)
98
+ #
99
+ # Updates the numGlyphs field to reflect the number of glyphs in
100
+ # the subset font.
101
+ #
102
+ # @param table [Maxp] Parsed maxp table
103
+ # @return [String] Binary data of subset maxp table
104
+ def subset_maxp(table)
105
+ data = table.to_binary_s.dup
106
+
107
+ # Update numGlyphs field (at offset 4, uint16)
108
+ data[4, 2] = [mapping.size].pack("n")
109
+
110
+ data
111
+ end
112
+
113
+ # Subset hhea table (update numberOfHMetrics)
114
+ #
115
+ # Updates the numberOfHMetrics field to reflect the number of
116
+ # horizontal metrics in the subset font.
117
+ #
118
+ # @param table [Hhea] Parsed hhea table
119
+ # @param hmtx [Hmtx, nil] Optional parsed hmtx table (for calculating metrics)
120
+ # @return [String] Binary data of subset hhea table
121
+ def subset_hhea(table, hmtx = nil)
122
+ data = table.to_binary_s.dup
123
+
124
+ # Calculate new numberOfHMetrics
125
+ new_num_h_metrics = if hmtx && hmtx.h_metrics
126
+ hmtx.h_metrics.size
127
+ else
128
+ calculate_number_of_h_metrics
129
+ end
130
+
131
+ # Update numberOfHMetrics field (at offset 34, uint16)
132
+ data[34, 2] = [new_num_h_metrics].pack("n")
133
+
134
+ data
135
+ end
136
+
137
+ # Subset hmtx table (subset horizontal metrics)
138
+ #
139
+ # Builds new hmtx table with metrics for subset glyphs only,
140
+ # preserving the order of the glyph mapping.
141
+ #
142
+ # @param table [Hmtx] Parsed hmtx table
143
+ # @return [String] Binary data of subset hmtx table
144
+ def subset_hmtx(table)
145
+ # Ensure hmtx is parsed
146
+ unless table.parsed?
147
+ hhea = font.table("hhea")
148
+ maxp = font.table("maxp")
149
+ table.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
150
+ end
151
+
152
+ # Build new hmtx data
153
+ data = String.new(encoding: Encoding::BINARY)
154
+
155
+ mapping.each do |old_id, _new_id|
156
+ metric = table.metric_for(old_id)
157
+ next unless metric
158
+
159
+ data << [metric[:advance_width]].pack("n")
160
+ data << [metric[:lsb]].pack("n")
161
+ end
162
+
163
+ data
164
+ end
165
+
166
+ # Subset glyf table (subset glyph data)
167
+ #
168
+ # Extracts glyph data for subset glyphs and remaps component
169
+ # references in compound glyphs. Also builds loca offsets.
170
+ #
171
+ # @param table [Glyf] Parsed glyf table
172
+ # @return [String] Binary data of subset glyf table
173
+ def subset_glyf(table)
174
+ # Build glyf and loca together
175
+ build_glyf_and_loca(table)
176
+ @glyf_data
177
+ end
178
+
179
+ # Subset loca table (rebuild glyph location index)
180
+ #
181
+ # Builds new loca table based on subset glyph offsets. Must be
182
+ # called after subset_glyf.
183
+ #
184
+ # @param table [Loca] Parsed loca table
185
+ # @return [String] Binary data of subset loca table
186
+ def subset_loca(_table)
187
+ # Build glyf and loca together if not already done
188
+ glyf = font.table("glyf")
189
+ build_glyf_and_loca(glyf) unless @loca_offsets
190
+
191
+ head = font.table("head")
192
+ format = head.index_to_loc_format
193
+
194
+ data = String.new(encoding: Encoding::BINARY)
195
+
196
+ if format.zero?
197
+ # Short format: offsets / 2 as uint16
198
+ @loca_offsets.each do |offset|
199
+ data << [offset / 2].pack("n")
200
+ end
201
+ else
202
+ # Long format: offsets as uint32
203
+ @loca_offsets.each do |offset|
204
+ data << [offset].pack("N")
205
+ end
206
+ end
207
+
208
+ data
209
+ end
210
+
211
+ # Subset cmap table (remap character to glyph mappings)
212
+ #
213
+ # Builds new cmap table with only mappings for glyphs in the subset.
214
+ # Updates glyph IDs to new values from the mapping.
215
+ #
216
+ # @param table [Cmap] Parsed cmap table
217
+ # @return [String] Binary data of subset cmap table
218
+ def subset_cmap(table)
219
+ # Get old mappings
220
+ old_mappings = table.unicode_mappings
221
+ new_mappings = {}
222
+
223
+ # Remap to new glyph IDs
224
+ old_mappings.each do |char_code, old_gid|
225
+ new_gid = mapping.new_id(old_gid)
226
+ new_mappings[char_code] = new_gid if new_gid
227
+ end
228
+
229
+ # Build cmap binary with new mappings
230
+ build_cmap_binary(new_mappings)
231
+ end
232
+
233
+ # Subset post table (optionally drop glyph names)
234
+ #
235
+ # If drop_names option is set, converts to post version 3.0
236
+ # (no glyph names). Otherwise passes through unchanged.
237
+ #
238
+ # @param table [Post] Parsed post table
239
+ # @return [String] Binary data of subset post table
240
+ def subset_post(table)
241
+ if options.drop_names
242
+ # Build post table version 3.0 (no glyph names)
243
+ build_post_v3(table)
244
+ else
245
+ # Keep as-is
246
+ font.table_data["post"]
247
+ end
248
+ end
249
+
250
+ # Subset name table (pass through)
251
+ #
252
+ # Name table doesn't require subsetting, pass through unchanged.
253
+ #
254
+ # @param table [Name] Parsed name table
255
+ # @return [String] Binary data of subset name table
256
+ def subset_name(_table)
257
+ font.table_data["name"]
258
+ end
259
+
260
+ # Subset head table (pass through)
261
+ #
262
+ # head table will have checksum updated by FontWriter,
263
+ # no subsetting needed.
264
+ #
265
+ # @param table [Head] Parsed head table
266
+ # @return [String] Binary data of subset head table
267
+ def subset_head(_table)
268
+ font.table_data["head"]
269
+ end
270
+
271
+ # Subset OS/2 table (optionally prune Unicode ranges)
272
+ #
273
+ # If unicode_ranges option is set, updates Unicode range bits
274
+ # to reflect only the characters in the subset.
275
+ #
276
+ # @param table [Os2] Parsed OS/2 table
277
+ # @return [String] Binary data of subset OS/2 table
278
+ def subset_os2(_table)
279
+ if options.unicode_ranges
280
+ # TODO: Implement Unicode range pruning
281
+ # For now, pass through
282
+ end
283
+ font.table_data["OS/2"]
284
+ end
285
+
286
+ private
287
+
288
+ # Calculate numberOfHMetrics for subset
289
+ #
290
+ # For now, use the size of the mapping. In the future, this could
291
+ # be optimized by finding the last unique advance width.
292
+ #
293
+ # @return [Integer] Number of unique advance widths
294
+ def calculate_number_of_h_metrics
295
+ mapping.size
296
+ end
297
+
298
+ # Build glyf and loca tables together
299
+ #
300
+ # This method extracts glyph data for all glyphs in the mapping,
301
+ # remaps component references in compound glyphs, and builds the
302
+ # loca offset array.
303
+ #
304
+ # @param glyf_table [Glyf] Parsed glyf table
305
+ def build_glyf_and_loca(glyf_table)
306
+ return if @glyf_data && @loca_offsets
307
+
308
+ loca = font.table("loca")
309
+ head = font.table("head")
310
+
311
+ # Ensure loca is parsed
312
+ unless loca.parsed?
313
+ maxp = font.table("maxp")
314
+ loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
315
+ end
316
+
317
+ @glyf_data = String.new(encoding: Encoding::BINARY)
318
+ @loca_offsets = []
319
+ current_offset = 0
320
+
321
+ # Process glyphs in mapping order
322
+ mapping.each do |old_id, _new_id|
323
+ @loca_offsets << current_offset
324
+
325
+ # Get offset and size from original loca
326
+ offset = loca.offset_for(old_id)
327
+ size = loca.size_of(old_id)
328
+
329
+ # Empty glyph
330
+ if size.nil? || size.zero?
331
+ next
332
+ end
333
+
334
+ # Extract glyph data
335
+ glyph_data = glyf_table.raw_data[offset, size]
336
+
337
+ # Check if compound glyph and remap components
338
+ if compound_glyph?(glyph_data)
339
+ glyph_data = remap_compound_glyph(glyph_data)
340
+ end
341
+
342
+ # Add to new glyf data
343
+ @glyf_data << glyph_data
344
+ current_offset += glyph_data.bytesize
345
+ end
346
+
347
+ # Add final offset
348
+ @loca_offsets << current_offset
349
+ end
350
+
351
+ # Check if glyph data represents a compound glyph
352
+ #
353
+ # @param data [String] Glyph binary data
354
+ # @return [Boolean] True if compound glyph
355
+ def compound_glyph?(data)
356
+ return false if data.length < 2
357
+
358
+ num_contours_raw = data[0, 2].unpack1("n")
359
+ num_contours = to_signed_16(num_contours_raw)
360
+ num_contours == -1
361
+ end
362
+
363
+ # Remap component glyph IDs in compound glyph
364
+ #
365
+ # @param data [String] Original compound glyph data
366
+ # @return [String] Remapped compound glyph data
367
+ def remap_compound_glyph(data)
368
+ # Create a mutable copy
369
+ new_data = data.dup
370
+ offset = 10 # Skip header (10 bytes)
371
+
372
+ loop do
373
+ break if offset >= new_data.length - 4
374
+
375
+ # Read flags and old glyph index
376
+ flags = new_data[offset, 2].unpack1("n")
377
+ old_glyph_index = new_data[offset + 2, 2].unpack1("n")
378
+
379
+ # Remap glyph index
380
+ new_glyph_index = mapping.new_id(old_glyph_index)
381
+ unless new_glyph_index
382
+ raise Fontisan::SubsettingError,
383
+ "Component glyph #{old_glyph_index} not in subset"
384
+ end
385
+
386
+ # Write new glyph index
387
+ new_data[offset + 2, 2] = [new_glyph_index].pack("n")
388
+
389
+ # Move to next component
390
+ offset += 4 # flags + glyph_index
391
+
392
+ # Skip arguments
393
+ offset += if (flags & 0x0001).zero?
394
+ 2 # Two 8-bit arguments
395
+ else
396
+ 4 # Two 16-bit arguments
397
+ end
398
+
399
+ # Skip transformation
400
+ if (flags & 0x0080) != 0
401
+ offset += 8 # 2x2 matrix
402
+ elsif (flags & 0x0040) != 0
403
+ offset += 4 # X and Y scale
404
+ elsif (flags & 0x0008) != 0
405
+ offset += 2 # Uniform scale
406
+ end
407
+
408
+ # Check if more components
409
+ break unless (flags & 0x0020) != 0
410
+ end
411
+
412
+ new_data
413
+ end
414
+
415
+ # Build cmap binary from mappings
416
+ #
417
+ # Creates a minimal cmap table with format 4 subtable for BMP
418
+ # and format 12 for supplementary planes if needed.
419
+ #
420
+ # @param mappings [Hash<Integer, Integer>] Char code => glyph ID
421
+ # @return [String] Binary cmap data
422
+ def build_cmap_binary(_mappings)
423
+ # For now, pass through original cmap
424
+ # TODO: Implement proper cmap building
425
+ font.table_data["cmap"]
426
+ end
427
+
428
+ # Build post table version 3.0 (no glyph names)
429
+ #
430
+ # @param table [Post] Original post table
431
+ # @return [String] Binary post v3.0 data
432
+ def build_post_v3(_table)
433
+ # Post v3.0 header (32 bytes) - same as v2.0 but version = 3.0
434
+ data = String.new(encoding: Encoding::BINARY)
435
+
436
+ # Version 3.0
437
+ data << [0x00030000].pack("N")
438
+
439
+ # Copy italic angle, underline position/thickness from original
440
+ original_data = font.table_data["post"]
441
+ data << if original_data.length >= 32
442
+ # Copy fields from offset 4 to 32
443
+ original_data[4, 28]
444
+ else
445
+ # Use defaults
446
+ [0, 0, 0, 0, 0, 0, 0].pack("N7")
447
+ end
448
+
449
+ data
450
+ end
451
+
452
+ # Convert unsigned 16-bit value to signed
453
+ #
454
+ # @param value [Integer] Unsigned 16-bit value
455
+ # @return [Integer] Signed 16-bit value
456
+ def to_signed_16(value)
457
+ value > 0x7FFF ? value - 0x10000 : value
458
+ end
459
+ end
460
+ end
461
+ end