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,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ class Cff2
6
+ # Blend operator handler for CFF2 CharStrings
7
+ #
8
+ # The blend operator is the key mechanism for applying variations in CFF2.
9
+ # It takes base values and deltas, and applies them based on variation
10
+ # coordinates to produce blended values.
11
+ #
12
+ # Blend Operator Format:
13
+ # Stack: base1 Δ1_axis1 ... Δ1_axisN base2 Δ2_axis1 ... Δ2_axisN ... K N
14
+ #
15
+ # Where:
16
+ # - base_i = base value for the i-th operand
17
+ # - Δi_axisj = delta for i-th operand on j-th axis
18
+ # - K = number of operands to blend (integer)
19
+ # - N = number of variation axes (integer)
20
+ #
21
+ # Result:
22
+ # Produces K blended values on the stack
23
+ #
24
+ # Blending Formula:
25
+ # blended_value = base + Σ(delta[i] * scalar[i])
26
+ #
27
+ # Where scalar[i] is computed from the current design space coordinates
28
+ # for axis i.
29
+ #
30
+ # Reference: Adobe Technical Note #5177
31
+ #
32
+ # @example Applying blend with coordinates
33
+ # blend = BlendOperator.new(num_axes: 2)
34
+ # data = {
35
+ # num_values: 2,
36
+ # num_axes: 2,
37
+ # blends: [
38
+ # { base: 100, deltas: [10, 5] },
39
+ # { base: 200, deltas: [20, 10] }
40
+ # ]
41
+ # }
42
+ # scalars = [0.5, 0.3] # From coordinate interpolation
43
+ # result = blend.apply(data, scalars)
44
+ # # => [105.0, 216.0] # 100 + (10*0.5 + 5*0.3), 200 + (20*0.5 + 10*0.3)
45
+ class BlendOperator
46
+ # @return [Integer] Number of variation axes
47
+ attr_reader :num_axes
48
+
49
+ # Initialize blend operator handler
50
+ #
51
+ # @param num_axes [Integer] Number of variation axes
52
+ def initialize(num_axes:)
53
+ @num_axes = num_axes
54
+ end
55
+
56
+ # Parse blend operands from stack
57
+ #
58
+ # Extracts blend data from a flattened array of operands.
59
+ #
60
+ # @param operands [Array<Numeric>] Stack operands (including K and N)
61
+ # @return [Hash, nil] Parsed blend data or nil if invalid
62
+ def parse(operands)
63
+ return nil if operands.size < 2
64
+
65
+ # Last two values are K and N
66
+ n = operands[-1].to_i
67
+ k = operands[-2].to_i
68
+
69
+ # Validate number of axes matches
70
+ if n != @num_axes
71
+ warn "Blend operator axes mismatch: expected #{@num_axes}, got #{n}"
72
+ return nil
73
+ end
74
+
75
+ # Validate we have enough operands: K * (N + 1) + 2
76
+ required_total = k * (n + 1) + 2
77
+ if operands.size < required_total
78
+ warn "Blend requires #{required_total} operands, got #{operands.size}"
79
+ return nil
80
+ end
81
+
82
+ # Extract blend operands (everything except K and N)
83
+ blend_operands = operands[-required_total..-3]
84
+
85
+ # Parse into base + deltas structure
86
+ blends = []
87
+ k.times do |i|
88
+ offset = i * (n + 1)
89
+ base = blend_operands[offset]
90
+ deltas = blend_operands[offset + 1, n] || []
91
+
92
+ blends << {
93
+ base: base,
94
+ deltas: deltas,
95
+ }
96
+ end
97
+
98
+ {
99
+ num_values: k,
100
+ num_axes: n,
101
+ blends: blends,
102
+ }
103
+ end
104
+
105
+ # Apply blend operation with variation scalars
106
+ #
107
+ # Computes blended values from base values and deltas using the
108
+ # provided scalars (one per axis).
109
+ #
110
+ # @param blend_data [Hash] Parsed blend data from parse()
111
+ # @param scalars [Array<Float>] Variation scalars (one per axis)
112
+ # @return [Array<Float>] Blended values
113
+ def apply(blend_data, scalars)
114
+ return [] if blend_data.nil?
115
+
116
+ # Ensure we have scalars for all axes
117
+ scalars = Array(scalars)
118
+ if scalars.size < blend_data[:num_axes]
119
+ # Pad with zeros if not enough scalars
120
+ scalars = scalars + ([0.0] * (blend_data[:num_axes] - scalars.size))
121
+ end
122
+
123
+ # Apply blend to each value
124
+ blend_data[:blends].map do |blend|
125
+ apply_single_blend(blend, scalars)
126
+ end
127
+ end
128
+
129
+ # Apply blend to a single value
130
+ #
131
+ # @param blend [Hash] Single blend entry with :base and :deltas
132
+ # @param scalars [Array<Float>] Variation scalars
133
+ # @return [Float] Blended value
134
+ def apply_single_blend(blend, scalars)
135
+ base = blend[:base].to_f
136
+ deltas = blend[:deltas]
137
+
138
+ # Apply formula: result = base + Σ(delta[i] * scalar[i])
139
+ result = base
140
+ deltas.each_with_index do |delta, axis_index|
141
+ scalar = scalars[axis_index] || 0.0
142
+ result += delta.to_f * scalar
143
+ end
144
+
145
+ result
146
+ end
147
+
148
+ # Calculate variation scalars from coordinates
149
+ #
150
+ # This converts normalized coordinates [-1, 1] to scalars for each axis.
151
+ # For now, this is a simple pass-through, but will integrate with the
152
+ # interpolator in Phase B.
153
+ #
154
+ # @param coordinates [Hash<String, Float>] Axis coordinates
155
+ # @param axes [Array<VariationAxisRecord>] Variation axes from fvar
156
+ # @return [Array<Float>] Scalars for each axis
157
+ def calculate_scalars(coordinates, axes)
158
+ return [] if axes.nil? || axes.empty?
159
+
160
+ axes.map do |axis|
161
+ coord = coordinates[axis.axis_tag] || axis.default_value
162
+ normalize_coordinate(coord, axis)
163
+ end
164
+ end
165
+
166
+ # Normalize a coordinate value to [-1, 1] range
167
+ #
168
+ # @param value [Float] Coordinate value
169
+ # @param axis [VariationAxisRecord] Axis definition
170
+ # @return [Float] Normalized coordinate in [-1, 1]
171
+ def normalize_coordinate(value, axis)
172
+ # Clamp to axis range
173
+ value = [[value, axis.min_value].max, axis.max_value].min
174
+
175
+ # Normalize to [-1, 1]
176
+ if value < axis.default_value
177
+ # Normalize between min and default (maps to -1..0)
178
+ range = axis.default_value - axis.min_value
179
+ return -1.0 if range.zero?
180
+
181
+ (value - axis.default_value) / range
182
+ elsif value > axis.default_value
183
+ # Normalize between default and max (maps to 0..1)
184
+ range = axis.max_value - axis.default_value
185
+ return 1.0 if range.zero?
186
+
187
+ (value - axis.default_value) / range
188
+ else
189
+ # At default value
190
+ 0.0
191
+ end
192
+ end
193
+
194
+ # Validate blend data structure
195
+ #
196
+ # @param blend_data [Hash] Blend data to validate
197
+ # @return [Boolean] True if valid
198
+ def valid?(blend_data)
199
+ return false if blend_data.nil?
200
+ return false unless blend_data.is_a?(Hash)
201
+ return false unless blend_data.key?(:num_values)
202
+ return false unless blend_data.key?(:num_axes)
203
+ return false unless blend_data.key?(:blends)
204
+ return false unless blend_data[:num_values].is_a?(Integer)
205
+ return false unless blend_data[:num_axes].is_a?(Integer)
206
+ return false unless blend_data[:blends].is_a?(Array)
207
+ return false if blend_data[:blends].size != blend_data[:num_values]
208
+
209
+ # Validate each blend entry
210
+ blend_data[:blends].all? do |blend|
211
+ blend.is_a?(Hash) &&
212
+ blend.key?(:base) &&
213
+ blend.key?(:deltas) &&
214
+ blend[:deltas].is_a?(Array) &&
215
+ blend[:deltas].size == blend_data[:num_axes]
216
+ end
217
+ end
218
+
219
+ # Get number of operands required for blend
220
+ #
221
+ # @param k [Integer] Number of values to blend
222
+ # @param n [Integer] Number of axes
223
+ # @return [Integer] Total operands required (including K and N)
224
+ def self.operand_count(k, n)
225
+ k * (n + 1) + 2
226
+ end
227
+
228
+ # Check if enough operands are available
229
+ #
230
+ # @param stack_size [Integer] Current stack size
231
+ # @param k [Integer] Number of values to blend
232
+ # @param n [Integer] Number of axes
233
+ # @return [Boolean] True if enough operands
234
+ def self.sufficient_operands?(stack_size, k, n)
235
+ stack_size >= operand_count(k, n)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end