fontisan 0.1.0 → 0.2.1

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  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 +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -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 +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require_relative "variation_context"
6
+
7
+ module Fontisan
8
+ module Variation
9
+ # Inspects and analyzes variable font structure
10
+ #
11
+ # This class provides comprehensive analysis of variable font structure,
12
+ # including axes, instances, regions, and variation statistics. Results
13
+ # can be exported to JSON or YAML formats.
14
+ #
15
+ # @example Inspecting a variable font
16
+ # inspector = Fontisan::Variation::Inspector.new(font)
17
+ # info = inspector.inspect_variation
18
+ # # => { axes: [...], instances: [...], regions: {...}, statistics: {...} }
19
+ #
20
+ # @example Exporting to JSON
21
+ # inspector.export_json
22
+ # # => "{ \"axes\": [...], ... }"
23
+ #
24
+ # @example Exporting to YAML
25
+ # inspector.export_yaml
26
+ # # => "---\naxes:\n - ..."
27
+ class Inspector
28
+ # @return [TrueTypeFont, OpenTypeFont] Font to inspect
29
+ attr_reader :font
30
+
31
+ # @return [VariationContext] Variation context
32
+ attr_reader :context
33
+
34
+ # Initialize inspector
35
+ #
36
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
37
+ def initialize(font)
38
+ @font = font
39
+ @context = VariationContext.new(font)
40
+ end
41
+
42
+ # Inspect complete variation structure
43
+ #
44
+ # Returns comprehensive information about font variation capabilities.
45
+ #
46
+ # @return [Hash] Complete variation information
47
+ def inspect_variation
48
+ {
49
+ axes: inspect_axes,
50
+ instances: inspect_instances,
51
+ regions: inspect_regions,
52
+ statistics: calculate_statistics,
53
+ }
54
+ end
55
+
56
+ # Export inspection results as JSON
57
+ #
58
+ # @return [String] JSON formatted output
59
+ def export_json
60
+ JSON.pretty_generate(inspect_variation)
61
+ end
62
+
63
+ # Export inspection results as YAML
64
+ #
65
+ # @return [String] YAML formatted output
66
+ def export_yaml
67
+ YAML.dump(inspect_variation)
68
+ end
69
+
70
+ # Check if font is a variable font
71
+ #
72
+ # @return [Boolean] True if font has variation tables
73
+ def variable_font?
74
+ @context.variable_font?
75
+ end
76
+
77
+ private
78
+
79
+ # Inspect variation axes
80
+ #
81
+ # @return [Array<Hash>] Array of axis information
82
+ def inspect_axes
83
+ return [] unless variable_font?
84
+ return [] unless @context.fvar
85
+
86
+ @context.axes.map do |axis|
87
+ {
88
+ tag: axis.axis_tag,
89
+ name: axis_name(axis.axis_name_id),
90
+ min: axis.min_value,
91
+ default: axis.default_value,
92
+ max: axis.max_value,
93
+ hidden: axis.flags & 0x0001 != 0,
94
+ }
95
+ end
96
+ end
97
+
98
+ # Inspect named instances
99
+ #
100
+ # @return [Array<Hash>] Array of instance information
101
+ def inspect_instances
102
+ return [] unless variable_font?
103
+ return [] unless @context.fvar
104
+
105
+ @context.fvar.instances.map.with_index do |instance, index|
106
+ {
107
+ index: index,
108
+ name: instance_name(instance[:subfamily_name_id]),
109
+ postscript_name: instance_name(instance[:postscript_name_id]),
110
+ coordinates: instance_coordinates(instance[:coordinates], @context.axes),
111
+ }
112
+ end
113
+ end
114
+
115
+ # Inspect variation regions
116
+ #
117
+ # @return [Hash] Region statistics and information
118
+ def inspect_regions
119
+ regions = {
120
+ gvar: nil,
121
+ hvar: nil,
122
+ vvar: nil,
123
+ mvar: nil,
124
+ }
125
+
126
+ if @font.has_table?("gvar")
127
+ regions[:gvar] = inspect_gvar_regions
128
+ end
129
+
130
+ if @font.has_table?("HVAR")
131
+ regions[:hvar] = inspect_hvar_regions
132
+ end
133
+
134
+ if @font.has_table?("VVAR")
135
+ regions[:vvar] = inspect_vvar_regions
136
+ end
137
+
138
+ if @font.has_table?("MVAR")
139
+ regions[:mvar] = inspect_mvar_regions
140
+ end
141
+
142
+ regions.compact
143
+ end
144
+
145
+ # Inspect gvar table regions
146
+ #
147
+ # @return [Hash] Gvar region information
148
+ def inspect_gvar_regions
149
+ gvar = @font.table("gvar")
150
+ return nil unless gvar
151
+
152
+ {
153
+ glyph_count: gvar.glyph_count,
154
+ axis_count: gvar.axis_count,
155
+ shared_tuples: gvar.shared_tuple_count || 0,
156
+ glyph_variation_data_present: gvar.glyph_count.positive?,
157
+ }
158
+ end
159
+
160
+ # Inspect HVAR table regions
161
+ #
162
+ # @return [Hash] HVAR region information
163
+ def inspect_hvar_regions
164
+ hvar = @font.table("HVAR")
165
+ return nil unless hvar
166
+
167
+ {
168
+ advance_width_mapping: hvar.advance_width_mapping ? true : false,
169
+ lsb_mapping: hvar.lsb_mapping ? true : false,
170
+ rsb_mapping: hvar.rsb_mapping ? true : false,
171
+ }
172
+ end
173
+
174
+ # Inspect VVAR table regions
175
+ #
176
+ # @return [Hash] VVAR region information
177
+ def inspect_vvar_regions
178
+ vvar = @font.table("VVAR")
179
+ return nil unless vvar
180
+
181
+ {
182
+ advance_height_mapping: vvar.advance_height_mapping ? true : false,
183
+ tsb_mapping: vvar.tsb_mapping ? true : false,
184
+ bsb_mapping: vvar.bsb_mapping ? true : false,
185
+ }
186
+ end
187
+
188
+ # Inspect MVAR table regions
189
+ #
190
+ # @return [Hash] MVAR region information
191
+ def inspect_mvar_regions
192
+ mvar = @font.table("MVAR")
193
+ return nil unless mvar
194
+
195
+ {
196
+ value_record_count: mvar.value_record_count || 0,
197
+ metrics_varied: mvar.value_records&.map { |r| r[:value_tag] } || [],
198
+ }
199
+ end
200
+
201
+ # Calculate variation statistics
202
+ #
203
+ # @return [Hash] Statistical information
204
+ def calculate_statistics
205
+ stats = {
206
+ is_variable: variable_font?,
207
+ axis_count: 0,
208
+ instance_count: 0,
209
+ has_glyph_variations: @context.has_glyph_variations?,
210
+ has_metrics_variations: @context.has_metrics_variations?,
211
+ variation_tables: [],
212
+ }
213
+
214
+ if variable_font?
215
+ stats[:axis_count] = @context.axis_count
216
+ stats[:instance_count] = @context.fvar.instance_count if @context.fvar
217
+ end
218
+
219
+ # List variation tables present
220
+ variation_table_tags = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
221
+ stats[:variation_tables] = variation_table_tags.select do |tag|
222
+ @font.has_table?(tag)
223
+ end
224
+
225
+ # Calculate design space size
226
+ if stats[:axis_count].positive?
227
+ stats[:design_space_dimensions] = stats[:axis_count]
228
+ end
229
+
230
+ stats
231
+ end
232
+
233
+ # Get axis name from name table
234
+ #
235
+ # @param name_id [Integer] Name ID
236
+ # @return [String] Axis name
237
+ def axis_name(name_id)
238
+ return "Unknown" unless @font.has_table?("name")
239
+
240
+ name_table = @font.table("name")
241
+ record = name_table.names.find { |n| n[:name_id] == name_id }
242
+ record ? record[:string] : "Axis #{name_id}"
243
+ end
244
+
245
+ # Get instance name from name table
246
+ #
247
+ # @param name_id [Integer] Name ID
248
+ # @return [String, nil] Instance name
249
+ def instance_name(name_id)
250
+ return nil unless name_id
251
+ return nil unless @font.has_table?("name")
252
+
253
+ name_table = @font.table("name")
254
+ record = name_table.names.find { |n| n[:name_id] == name_id }
255
+ record ? record[:string] : "Instance #{name_id}"
256
+ end
257
+
258
+ # Build coordinates hash from instance
259
+ #
260
+ # @param coordinates [Array<Float>] Coordinate values
261
+ # @param axes [Array] Variation axes
262
+ # @return [Hash<String, Float>] Coordinates by axis tag
263
+ def instance_coordinates(coordinates, axes)
264
+ coords = {}
265
+ coordinates.each_with_index do |value, index|
266
+ break if index >= axes.length
267
+
268
+ axis = axes[index]
269
+ coords[axis.axis_tag] = value
270
+ end
271
+ coords
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "interpolator"
4
+ require_relative "region_matcher"
5
+ require_relative "metrics_adjuster"
6
+ require_relative "variation_context"
7
+ require_relative "table_accessor"
8
+
9
+ module Fontisan
10
+ module Variation
11
+ # Generates static font instances from variable fonts
12
+ #
13
+ # This class creates static font instances by applying variation deltas
14
+ # at specific design space coordinates. It supports both TrueType (gvar)
15
+ # and PostScript (CFF2) variable fonts.
16
+ #
17
+ # Process:
18
+ # 1. Extract variation data (axes, deltas, regions)
19
+ # 2. Calculate interpolation scalars for given coordinates
20
+ # 3. Apply deltas to outlines (gvar or CFF2)
21
+ # 4. Apply deltas to metrics (HVAR, VVAR, MVAR)
22
+ # 5. Remove variation tables to create static font
23
+ #
24
+ # @example Generating an instance at specific coordinates
25
+ # generator = Fontisan::Variation::InstanceGenerator.new(font, { "wght" => 700.0 })
26
+ # instance_tables = generator.generate
27
+ #
28
+ # @example Generating a named instance
29
+ # generator = Fontisan::Variation::InstanceGenerator.new(font)
30
+ # instance_tables = generator.generate_named_instance(0)
31
+ class InstanceGenerator
32
+ include TableAccessor
33
+
34
+ # @return [TrueTypeFont, OpenTypeFont] Variable font
35
+ attr_reader :font
36
+
37
+ # @return [Hash<String, Float>] Design space coordinates
38
+ attr_reader :coordinates
39
+
40
+ # @return [VariationContext] Variation context
41
+ attr_reader :context
42
+
43
+ # Initialize generator with font and optional coordinates
44
+ #
45
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
46
+ # @param coordinates [Hash<String, Float>] Design space coordinates (axis tag => value)
47
+ # @param options [Hash] Options
48
+ # @option options [Boolean] :skip_validation Skip context validation (default: false)
49
+ def initialize(font, coordinates = {}, options = {})
50
+ @font = font
51
+ @coordinates = coordinates
52
+
53
+ # Initialize variation context
54
+ @context = VariationContext.new(@font)
55
+ @context.validate! unless options[:skip_validation]
56
+
57
+ # Initialize table cache for lazy loading
58
+ @variation_tables = {}
59
+ end
60
+
61
+ # Generate static font instance
62
+ #
63
+ # Applies variation deltas and returns static font tables.
64
+ #
65
+ # @return [Hash<String, String>] Map of table tags to binary data
66
+ def generate
67
+ # Start with base font tables
68
+ tables = @font.table_data.dup
69
+
70
+ # Determine variation type
71
+ if has_variation_table?("gvar")
72
+ # TrueType outlines with gvar
73
+ apply_gvar_deltas(tables)
74
+ elsif has_variation_table?("CFF2")
75
+ # PostScript outlines with CFF2 blend
76
+ apply_cff2_blend(tables)
77
+ end
78
+
79
+ # Apply metrics variations if present
80
+ apply_metrics_deltas(tables) if @context.has_metrics_variations?
81
+
82
+ # Remove variation-specific tables to create static font
83
+ remove_variation_tables(tables)
84
+
85
+ tables
86
+ end
87
+
88
+ # Generate a named instance
89
+ #
90
+ # @param instance_index [Integer] Index of named instance in fvar table
91
+ # @return [Hash<String, String>] Map of table tags to binary data
92
+ def generate_named_instance(instance_index)
93
+ # Extract instance coordinates from fvar
94
+ return generate if instance_index.nil? || !@context.fvar
95
+
96
+ instances = @context.fvar.instances
97
+ return generate if instance_index >= instances.length
98
+
99
+ instance = instances[instance_index]
100
+ @coordinates = build_coordinates_from_instance(instance, @context.axes)
101
+
102
+ generate
103
+ end
104
+
105
+ # Apply gvar deltas to TrueType outlines
106
+ #
107
+ # @param tables [Hash<String, String>] Font tables
108
+ def apply_gvar_deltas(_tables)
109
+ gvar = variation_table("gvar")
110
+ glyf = @font.table("glyf")
111
+ return unless gvar && glyf
112
+
113
+ # Get glyph count
114
+ maxp = @font.table("maxp")
115
+ glyph_count = maxp ? maxp.num_glyphs : gvar.glyph_count
116
+
117
+ # Process each glyph
118
+ glyph_count.times do |glyph_id|
119
+ apply_glyph_deltas(glyph_id, gvar, glyf)
120
+ end
121
+
122
+ # Rebuild glyf and loca tables with adjusted outlines
123
+ # This is a placeholder - full implementation would reconstruct tables
124
+ end
125
+
126
+ # Apply deltas to a specific glyph
127
+ #
128
+ # @param glyph_id [Integer] Glyph ID
129
+ # @param gvar [Gvar] Gvar table
130
+ # @param glyf [Glyf] Glyf table
131
+ def apply_glyph_deltas(glyph_id, gvar, _glyf)
132
+ # Get tuple variations for this glyph
133
+ tuple_data = gvar.glyph_tuple_variations(glyph_id)
134
+ return unless tuple_data
135
+
136
+ # Match tuples to current coordinates
137
+ matches = @context.region_matcher.match_tuples(
138
+ coordinates: @coordinates,
139
+ tuples: tuple_data[:tuples],
140
+ )
141
+
142
+ nil if matches.empty?
143
+
144
+ # Get base glyph outline
145
+ # Apply matched deltas with their scalars
146
+ # This is a placeholder - full implementation would:
147
+ # 1. Parse glyph outline points
148
+ # 2. Parse delta data for each tuple
149
+ # 3. Apply: new_point = base_point + Σ(delta * scalar)
150
+ # 4. Update glyph outline
151
+ end
152
+
153
+ # Apply CFF2 blend operators
154
+ #
155
+ # @param tables [Hash<String, String>] Font tables
156
+ def apply_cff2_blend(_tables)
157
+ cff2 = variation_table("CFF2")
158
+ return unless cff2
159
+
160
+ # Set number of axes for CFF2
161
+ cff2.num_axes = @context.axis_count
162
+
163
+ # Process each glyph's CharString
164
+ glyph_count = cff2.glyph_count
165
+ return if glyph_count.zero?
166
+
167
+ # Calculate variation scalars once
168
+ calculate_variation_scalars
169
+
170
+ # Apply blend to each glyph
171
+ # This is a placeholder - full implementation would:
172
+ # 1. Parse CharString with blend operators
173
+ # 2. Apply scalars to blend operands
174
+ # 3. Rebuild CharStrings without blend operators
175
+ # 4. Update CFF2 table
176
+ end
177
+
178
+ # Calculate variation scalars for current coordinates
179
+ #
180
+ # @return [Array<Float>] Scalars for each axis
181
+ def calculate_variation_scalars
182
+ @context.axes.map do |axis|
183
+ coord = @coordinates[axis.axis_tag] || axis.default_value
184
+ @context.interpolator.normalize_coordinate(coord, axis.axis_tag)
185
+ end
186
+ end
187
+
188
+ # Apply metrics variations
189
+ #
190
+ # @param tables [Hash<String, String>] Font tables
191
+ def apply_metrics_deltas(tables)
192
+ # Apply HVAR (horizontal metrics)
193
+ apply_hvar_deltas(tables) if has_variation_table?("HVAR")
194
+
195
+ # Apply VVAR (vertical metrics)
196
+ apply_vvar_deltas(tables) if has_variation_table?("VVAR")
197
+
198
+ # Apply MVAR (font-wide metrics)
199
+ apply_mvar_deltas(tables) if has_variation_table?("MVAR")
200
+ end
201
+
202
+ # Apply HVAR deltas to horizontal metrics
203
+ #
204
+ # @param tables [Hash<String, String>] Font tables
205
+ def apply_hvar_deltas(_tables)
206
+ adjuster = MetricsAdjuster.new(@font, @context.interpolator)
207
+ adjuster.apply_hvar_deltas(@coordinates)
208
+ end
209
+
210
+ # Apply VVAR deltas to vertical metrics
211
+ #
212
+ # @param tables [Hash<String, String>] Font tables
213
+ def apply_vvar_deltas(_tables)
214
+ adjuster = MetricsAdjuster.new(@font, @context.interpolator)
215
+ adjuster.apply_vvar_deltas(@coordinates)
216
+ end
217
+
218
+ # Apply MVAR deltas to font-wide metrics
219
+ #
220
+ # @param tables [Hash<String, String>] Font tables
221
+ def apply_mvar_deltas(_tables)
222
+ adjuster = MetricsAdjuster.new(@font, @context.interpolator)
223
+ adjuster.apply_mvar_deltas(@coordinates)
224
+ end
225
+
226
+ # Remove variation tables from static font
227
+ #
228
+ # @param tables [Hash<String, String>] Font tables
229
+ def remove_variation_tables(tables)
230
+ variation_tables = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
231
+ variation_tables.each { |tag| tables.delete(tag) }
232
+ end
233
+
234
+ # Interpolate a single value
235
+ #
236
+ # @param base_value [Numeric] Base value
237
+ # @param deltas [Array<Numeric>] Delta values
238
+ # @param scalars [Array<Float>] Region scalars
239
+ # @return [Float] Interpolated value
240
+ def interpolate_value(base_value, deltas, scalars)
241
+ @context.interpolator.interpolate_value(base_value, deltas, scalars)
242
+ end
243
+
244
+ # Interpolate a point
245
+ #
246
+ # @param base_point [Hash] Base point with :x and :y
247
+ # @param delta_points [Array<Hash>] Delta points
248
+ # @param scalars [Array<Float>] Region scalars
249
+ # @return [Hash] Interpolated point
250
+ def interpolate_point(base_point, delta_points, scalars)
251
+ @context.interpolator.interpolate_point(base_point, delta_points, scalars)
252
+ end
253
+
254
+ private
255
+
256
+ # Build coordinates hash from instance
257
+ #
258
+ # @param instance [Hash] Instance data from fvar
259
+ # @param axes [Array<VariationAxisRecord>] Variation axes
260
+ # @return [Hash<String, Float>] Coordinates hash
261
+ def build_coordinates_from_instance(instance, axes)
262
+ coordinates = {}
263
+ instance[:coordinates].each_with_index do |value, index|
264
+ next if index >= axes.length
265
+
266
+ axis = axes[index]
267
+ coordinates[axis.axis_tag] = value
268
+ end
269
+ coordinates
270
+ end
271
+ end
272
+ end
273
+ end