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,905 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../../binary/base_record"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ class Cff
9
+ # Type 2 CharString interpreter
10
+ #
11
+ # CharStrings are stack-based programs that draw glyph outlines using
12
+ # a series of operators. They are stored in the CharStrings INDEX and
13
+ # contain path construction, hinting, and arithmetic operations.
14
+ #
15
+ # Type 2 CharString Format:
16
+ # - Numbers are pushed onto an operand stack
17
+ # - Operators pop operands and execute commands
18
+ # - Path operators build the glyph outline
19
+ # - Hint operators define stem hints (can be ignored for rendering)
20
+ # - Subroutine operators allow code reuse
21
+ # - Arithmetic operators perform calculations on the stack
22
+ #
23
+ # Path Construction Flow:
24
+ # 1. Optional width value (first operand if odd number before first move)
25
+ # 2. Initial moveto operator to start a path
26
+ # 3. Line/curve operators to construct the path
27
+ # 4. Optional closepath (implicit at endchar)
28
+ # 5. endchar to finish the glyph
29
+ #
30
+ # Reference: Adobe Type 2 CharString Format
31
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
32
+ #
33
+ # @example Interpreting a CharString
34
+ # charstring = CharString.new(data, private_dict, global_subrs,
35
+ # local_subrs)
36
+ # puts charstring.width # => glyph width
37
+ # puts charstring.path # => array of path commands
38
+ # bbox = charstring.bounding_box # => [xMin, yMin, xMax, yMax]
39
+ class CharString
40
+ # @return [Integer, nil] Glyph width (nil if using default width)
41
+ attr_reader :width
42
+
43
+ # @return [Array<Hash>] Path commands array
44
+ attr_reader :path
45
+
46
+ # @return [Float] Current X coordinate
47
+ attr_reader :x
48
+
49
+ # @return [Float] Current Y coordinate
50
+ attr_reader :y
51
+
52
+ # Type 2 CharString operators
53
+ #
54
+ # These operators define the behavior of the CharString interpreter
55
+ OPERATORS = {
56
+ # Path construction operators
57
+ 1 => :hstem,
58
+ 3 => :vstem,
59
+ 4 => :vmoveto,
60
+ 5 => :rlineto,
61
+ 6 => :hlineto,
62
+ 7 => :vlineto,
63
+ 8 => :rrcurveto,
64
+ 10 => :callsubr,
65
+ 11 => :return,
66
+ 14 => :endchar,
67
+ 18 => :hstemhm,
68
+ 19 => :hintmask,
69
+ 20 => :cntrmask,
70
+ 21 => :rmoveto,
71
+ 22 => :hmoveto,
72
+ 23 => :vstemhm,
73
+ 24 => :rcurveline,
74
+ 25 => :rlinecurve,
75
+ 26 => :vvcurveto,
76
+ 27 => :hhcurveto,
77
+ 28 => :shortint,
78
+ 29 => :callgsubr,
79
+ 30 => :vhcurveto,
80
+ 31 => :hvcurveto,
81
+ # 12 prefix for two-byte operators
82
+ [12, 3] => :and,
83
+ [12, 4] => :or,
84
+ [12, 5] => :not,
85
+ [12, 9] => :abs,
86
+ [12, 10] => :add,
87
+ [12, 11] => :sub,
88
+ [12, 12] => :div,
89
+ [12, 14] => :neg,
90
+ [12, 15] => :eq,
91
+ [12, 18] => :drop,
92
+ [12, 20] => :put,
93
+ [12, 21] => :get,
94
+ [12, 22] => :ifelse,
95
+ [12, 23] => :random,
96
+ [12, 24] => :mul,
97
+ [12, 26] => :sqrt,
98
+ [12, 27] => :dup,
99
+ [12, 28] => :exch,
100
+ [12, 29] => :index,
101
+ [12, 30] => :roll,
102
+ [12, 34] => :hflex,
103
+ [12, 35] => :flex,
104
+ [12, 36] => :hflex1,
105
+ [12, 37] => :flex1,
106
+ }.freeze
107
+
108
+ # Initialize and interpret a CharString
109
+ #
110
+ # @param data [String] Binary CharString data
111
+ # @param private_dict [PrivateDict] Private DICT for width defaults
112
+ # @param global_subrs [Index] Global subroutines INDEX
113
+ # @param local_subrs [Index, nil] Local subroutines INDEX
114
+ def initialize(data, private_dict, global_subrs, local_subrs = nil)
115
+ @data = data
116
+ @private_dict = private_dict
117
+ @global_subrs = global_subrs
118
+ @local_subrs = local_subrs
119
+
120
+ @stack = []
121
+ @path = []
122
+ @x = 0.0
123
+ @y = 0.0
124
+ @width = nil
125
+ @stems = 0
126
+ @transient_array = []
127
+ @subroutine_bias = calculate_bias(local_subrs)
128
+ @global_subroutine_bias = calculate_bias(global_subrs)
129
+
130
+ parse!
131
+ end
132
+
133
+ # Calculate the bounding box of the glyph
134
+ #
135
+ # @return [Array<Float>] [xMin, yMin, xMax, yMax] or nil if no path
136
+ def bounding_box
137
+ return nil if @path.empty?
138
+
139
+ x_coords = []
140
+ y_coords = []
141
+
142
+ @path.each do |cmd|
143
+ case cmd[:type]
144
+ when :move_to, :line_to
145
+ x_coords << cmd[:x]
146
+ y_coords << cmd[:y]
147
+ when :curve_to
148
+ x_coords << cmd[:x1] << cmd[:x2] << cmd[:x]
149
+ y_coords << cmd[:y1] << cmd[:y2] << cmd[:y]
150
+ end
151
+ end
152
+
153
+ return nil if x_coords.empty?
154
+
155
+ [x_coords.min, y_coords.min, x_coords.max, y_coords.max]
156
+ end
157
+
158
+ # Convert path to drawing commands
159
+ #
160
+ # @return [Array<Array>] Array of command arrays:
161
+ # [:move_to, x, y], [:line_to, x, y], [:curve_to, x1, y1, x2, y2,
162
+ # x, y]
163
+ def to_commands
164
+ @path.map do |cmd|
165
+ case cmd[:type]
166
+ when :move_to
167
+ [:move_to, cmd[:x], cmd[:y]]
168
+ when :line_to
169
+ [:line_to, cmd[:x], cmd[:y]]
170
+ when :curve_to
171
+ [:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2],
172
+ cmd[:x], cmd[:y]]
173
+ end
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ # Parse and execute the CharString program
180
+ def parse!
181
+ io = StringIO.new(@data)
182
+ width_parsed = false
183
+
184
+ until io.eof?
185
+ byte = io.getbyte
186
+
187
+ if byte <= 31 && byte != 28
188
+ # Operator byte
189
+ operator = read_operator(io, byte)
190
+ result = execute_operator(operator, width_parsed)
191
+ # Mark width as parsed after move operators or hint operators
192
+ if result == true || %i[hstem vstem hstemhm
193
+ vstemhm].include?(operator)
194
+ width_parsed = true
195
+ end
196
+ else
197
+ # Operand byte
198
+ io.pos -= 1
199
+ number = read_number(io)
200
+ @stack << number
201
+ end
202
+ end
203
+ rescue StandardError => e
204
+ raise CorruptedTableError,
205
+ "Failed to parse CharString: #{e.message}"
206
+ end
207
+
208
+ # Read an operator from the CharString
209
+ #
210
+ # @param io [StringIO] Input stream
211
+ # @param first_byte [Integer] First operator byte
212
+ # @return [Symbol] Operator name
213
+ def read_operator(io, first_byte)
214
+ if first_byte == 12
215
+ # Two-byte operator
216
+ second_byte = io.getbyte
217
+ raise CorruptedTableError, "Unexpected end of CharString" if
218
+ second_byte.nil?
219
+
220
+ operator_key = [first_byte, second_byte]
221
+ OPERATORS[operator_key] || :unknown
222
+ else
223
+ # Single-byte operator
224
+ OPERATORS[first_byte] || :unknown
225
+ end
226
+ end
227
+
228
+ # Read a number (integer or real) from the CharString
229
+ #
230
+ # @param io [StringIO] Input stream
231
+ # @return [Integer, Float] The number value
232
+ def read_number(io)
233
+ byte = io.getbyte
234
+ raise CorruptedTableError, "Unexpected end of CharString" if
235
+ byte.nil?
236
+
237
+ case byte
238
+ when 28
239
+ # 3-byte signed integer (16-bit)
240
+ b1 = io.getbyte
241
+ b2 = io.getbyte
242
+ value = (b1 << 8) | b2
243
+ value > 0x7FFF ? value - 0x10000 : value
244
+ when 32..246
245
+ # Small integer: -107 to +107
246
+ byte - 139
247
+ when 247..250
248
+ # Positive 2-byte integer: +108 to +1131
249
+ b2 = io.getbyte
250
+ (byte - 247) * 256 + b2 + 108
251
+ when 251..254
252
+ # Negative 2-byte integer: -108 to -1131
253
+ b2 = io.getbyte
254
+ -(byte - 251) * 256 - b2 - 108
255
+ when 255
256
+ # 5-byte signed integer (32-bit) as fixed-point 16.16
257
+ bytes = io.read(4)
258
+ value = bytes.unpack1("l>") # Signed 32-bit big-endian
259
+ value / 65536.0 # Convert to float
260
+ else
261
+ raise CorruptedTableError, "Invalid CharString number byte: #{byte}"
262
+ end
263
+ end
264
+
265
+ # Execute a CharString operator
266
+ #
267
+ # @param operator [Symbol] Operator name
268
+ # @param width_parsed [Boolean] Whether width has been parsed
269
+ def execute_operator(operator, width_parsed)
270
+ case operator
271
+ # Path construction operators
272
+ when :rmoveto
273
+ rmoveto(width_parsed)
274
+ true # Width has now been parsed
275
+ when :hmoveto
276
+ hmoveto(width_parsed)
277
+ true # Width has now been parsed
278
+ when :vmoveto
279
+ vmoveto(width_parsed)
280
+ true # Width has now been parsed
281
+ when :rlineto
282
+ rlineto
283
+ when :hlineto
284
+ hlineto
285
+ when :vlineto
286
+ vlineto
287
+ when :rrcurveto
288
+ rrcurveto
289
+ when :hhcurveto
290
+ hhcurveto
291
+ when :vvcurveto
292
+ vvcurveto
293
+ when :hvcurveto
294
+ hvcurveto
295
+ when :vhcurveto
296
+ vhcurveto
297
+ when :rcurveline
298
+ rcurveline
299
+ when :rlinecurve
300
+ rlinecurve
301
+ when :endchar
302
+ endchar
303
+
304
+ # Hint operators (stub for now)
305
+ when :hstem, :vstem, :hstemhm, :vstemhm
306
+ hint_operator(width_parsed)
307
+ when :hintmask, :cntrmask
308
+ hintmask_operator
309
+
310
+ # Subroutine operators
311
+ when :callsubr
312
+ callsubr
313
+ when :callgsubr
314
+ callgsubr
315
+ when :return
316
+ # Return is handled by subroutine execution
317
+
318
+ # Arithmetic operators
319
+ when :add
320
+ arithmetic_add
321
+ when :sub
322
+ arithmetic_sub
323
+ when :mul
324
+ arithmetic_mul
325
+ when :div
326
+ arithmetic_div
327
+ when :neg
328
+ arithmetic_neg
329
+ when :abs
330
+ arithmetic_abs
331
+ when :sqrt
332
+ arithmetic_sqrt
333
+ when :drop
334
+ @stack.pop
335
+ when :exch
336
+ @stack[-1], @stack[-2] = @stack[-2], @stack[-1]
337
+ when :dup
338
+ @stack << @stack.last
339
+ when :put
340
+ value = @stack.pop
341
+ index = @stack.pop
342
+ @transient_array[index] = value
343
+ when :get
344
+ index = @stack.pop
345
+ @stack << (@transient_array[index] || 0)
346
+
347
+ # Flex operators
348
+ when :hflex, :flex, :hflex1, :flex1
349
+ flex_operator(operator)
350
+
351
+ when :unknown
352
+ # Unknown operator - clear stack and continue
353
+ @stack.clear
354
+ end
355
+ end
356
+
357
+ # rmoveto: dx dy rmoveto
358
+ # Relative move to (dx, dy)
359
+ def rmoveto(width_parsed)
360
+ # rmoveto takes 2 operands, so if stack has 3 and width not parsed,
361
+ # first is width
362
+ parse_width_for_operator(width_parsed, 2)
363
+ dy = @stack.pop
364
+ dx = @stack.pop
365
+ @x += dx
366
+ @y += dy
367
+ @path << { type: :move_to, x: @x, y: @y }
368
+ @stack.clear
369
+ end
370
+
371
+ # hmoveto: dx hmoveto
372
+ # Horizontal move to (dx, 0)
373
+ def hmoveto(width_parsed)
374
+ # hmoveto takes 1 operand, so if stack has 2 and width not parsed,
375
+ # first is width
376
+ parse_width_for_operator(width_parsed, 1)
377
+ dx = @stack.pop || 0
378
+ @x += dx
379
+ @path << { type: :move_to, x: @x, y: @y }
380
+ @stack.clear
381
+ end
382
+
383
+ # vmoveto: dy vmoveto
384
+ # Vertical move to (0, dy)
385
+ def vmoveto(width_parsed)
386
+ # vmoveto takes 1 operand, so if stack has 2 and width not parsed,
387
+ # first is width
388
+ parse_width_for_operator(width_parsed, 1)
389
+ dy = @stack.pop || 0
390
+ @y += dy
391
+ @path << { type: :move_to, x: @x, y: @y }
392
+ @stack.clear
393
+ end
394
+
395
+ # rlineto: {dxa dya}+ rlineto
396
+ # Relative line to
397
+ def rlineto
398
+ while @stack.size >= 2
399
+ dx = @stack.shift
400
+ dy = @stack.shift
401
+ @x += dx
402
+ @y += dy
403
+ @path << { type: :line_to, x: @x, y: @y }
404
+ end
405
+ @stack.clear
406
+ end
407
+
408
+ # hlineto: dx1 {dya dxb}* hlineto
409
+ # Alternating horizontal and vertical lines
410
+ def hlineto
411
+ horizontal = true
412
+ while @stack.any?
413
+ delta = @stack.shift
414
+ if horizontal
415
+ @x += delta
416
+ else
417
+ @y += delta
418
+ end
419
+ @path << { type: :line_to, x: @x, y: @y }
420
+ horizontal = !horizontal
421
+ end
422
+ @stack.clear
423
+ end
424
+
425
+ # vlineto: dy1 {dxb dya}* vlineto
426
+ # Alternating vertical and horizontal lines
427
+ def vlineto
428
+ vertical = true
429
+ while @stack.any?
430
+ delta = @stack.shift
431
+ if vertical
432
+ @y += delta
433
+ else
434
+ @x += delta
435
+ end
436
+ @path << { type: :line_to, x: @x, y: @y }
437
+ vertical = !vertical
438
+ end
439
+ @stack.clear
440
+ end
441
+
442
+ # rrcurveto: {dxa dya dxb dyb dxc dyc}+ rrcurveto
443
+ # Relative cubic Bézier curve
444
+ def rrcurveto
445
+ while @stack.size >= 6
446
+ dx1 = @stack.shift
447
+ dy1 = @stack.shift
448
+ dx2 = @stack.shift
449
+ dy2 = @stack.shift
450
+ dx3 = @stack.shift
451
+ dy3 = @stack.shift
452
+
453
+ x1 = @x + dx1
454
+ y1 = @y + dy1
455
+ x2 = x1 + dx2
456
+ y2 = y1 + dy2
457
+ @x = x2 + dx3
458
+ @y = y2 + dy3
459
+
460
+ @path << {
461
+ type: :curve_to,
462
+ x1: x1, y1: y1,
463
+ x2: x2, y2: y2,
464
+ x: @x, y: @y
465
+ }
466
+ end
467
+ @stack.clear
468
+ end
469
+
470
+ # hhcurveto: dy1? {dxa dxb dyb dxc}+ hhcurveto
471
+ # Horizontal-horizontal curve
472
+ def hhcurveto
473
+ # First value might be dy1 if odd number of args
474
+ if @stack.size.odd?
475
+ @y += @stack.shift
476
+ end
477
+
478
+ while @stack.size >= 4
479
+ dx1 = @stack.shift
480
+ dx2 = @stack.shift
481
+ dy2 = @stack.shift
482
+ dx3 = @stack.shift
483
+
484
+ x1 = @x + dx1
485
+ y1 = @y
486
+ x2 = x1 + dx2
487
+ y2 = y1 + dy2
488
+ @x = x2 + dx3
489
+ @y = y2
490
+
491
+ @path << {
492
+ type: :curve_to,
493
+ x1: x1, y1: y1,
494
+ x2: x2, y2: y2,
495
+ x: @x, y: @y
496
+ }
497
+ end
498
+ @stack.clear
499
+ end
500
+
501
+ # vvcurveto: dx1? {dya dxb dyb dyc}+ vvcurveto
502
+ # Vertical-vertical curve
503
+ def vvcurveto
504
+ # First value might be dx1 if odd number of args
505
+ if @stack.size.odd?
506
+ @x += @stack.shift
507
+ end
508
+
509
+ while @stack.size >= 4
510
+ dy1 = @stack.shift
511
+ dx2 = @stack.shift
512
+ dy2 = @stack.shift
513
+ dy3 = @stack.shift
514
+
515
+ x1 = @x
516
+ y1 = @y + dy1
517
+ x2 = x1 + dx2
518
+ y2 = y1 + dy2
519
+ @x = x2
520
+ @y = y2 + dy3
521
+
522
+ @path << {
523
+ type: :curve_to,
524
+ x1: x1, y1: y1,
525
+ x2: x2, y2: y2,
526
+ x: @x, y: @y
527
+ }
528
+ end
529
+ @stack.clear
530
+ end
531
+
532
+ # hvcurveto: dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf?
533
+ # hvcurveto
534
+ # Horizontal-vertical curve
535
+ def hvcurveto
536
+ horizontal_first = true
537
+ while @stack.size >= 4
538
+ if horizontal_first
539
+ dx1 = @stack.shift
540
+ dx2 = @stack.shift
541
+ dy2 = @stack.shift
542
+ dy3 = @stack.shift
543
+ # Handle final dx if this is the last curve
544
+ dx3 = @stack.size == 1 ? @stack.shift : 0
545
+
546
+ x1 = @x + dx1
547
+ y1 = @y
548
+ else
549
+ dy1 = @stack.shift
550
+ dx2 = @stack.shift
551
+ dy2 = @stack.shift
552
+ dx3 = @stack.shift
553
+ # Handle final dy if this is the last curve
554
+ dy3 = @stack.size == 1 ? @stack.shift : 0
555
+
556
+ x1 = @x
557
+ y1 = @y + dy1
558
+ end
559
+ x2 = x1 + dx2
560
+ y2 = y1 + dy2
561
+ @x = x2 + dx3
562
+ @y = y2 + dy3
563
+
564
+ @path << {
565
+ type: :curve_to,
566
+ x1: x1, y1: y1,
567
+ x2: x2, y2: y2,
568
+ x: @x, y: @y
569
+ }
570
+ horizontal_first = !horizontal_first
571
+ end
572
+ @stack.clear
573
+ end
574
+
575
+ # vhcurveto: dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf?
576
+ # vhcurveto
577
+ # Vertical-horizontal curve
578
+ def vhcurveto
579
+ vertical_first = true
580
+ while @stack.size >= 4
581
+ if vertical_first
582
+ dy1 = @stack.shift
583
+ dx2 = @stack.shift
584
+ dy2 = @stack.shift
585
+ dx3 = @stack.shift
586
+ # Handle final dy if this is the last curve
587
+ dy3 = @stack.size == 1 ? @stack.shift : 0
588
+
589
+ x1 = @x
590
+ y1 = @y + dy1
591
+ else
592
+ dx1 = @stack.shift
593
+ dx2 = @stack.shift
594
+ dy2 = @stack.shift
595
+ dy3 = @stack.shift
596
+ # Handle final dx if this is the last curve
597
+ dx3 = @stack.size == 1 ? @stack.shift : 0
598
+
599
+ x1 = @x + dx1
600
+ y1 = @y
601
+ end
602
+ x2 = x1 + dx2
603
+ y2 = y1 + dy2
604
+ @x = x2 + dx3
605
+ @y = y2 + dy3
606
+
607
+ @path << {
608
+ type: :curve_to,
609
+ x1: x1, y1: y1,
610
+ x2: x2, y2: y2,
611
+ x: @x, y: @y
612
+ }
613
+ vertical_first = !vertical_first
614
+ end
615
+ @stack.clear
616
+ end
617
+
618
+ # rcurveline: {dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline
619
+ # Curves followed by a line
620
+ def rcurveline
621
+ # Process curves (all but last 2 values)
622
+ while @stack.size > 2
623
+ break if @stack.size < 6
624
+
625
+ dx1 = @stack.shift
626
+ dy1 = @stack.shift
627
+ dx2 = @stack.shift
628
+ dy2 = @stack.shift
629
+ dx3 = @stack.shift
630
+ dy3 = @stack.shift
631
+
632
+ x1 = @x + dx1
633
+ y1 = @y + dy1
634
+ x2 = x1 + dx2
635
+ y2 = y1 + dy2
636
+ @x = x2 + dx3
637
+ @y = y2 + dy3
638
+
639
+ @path << {
640
+ type: :curve_to,
641
+ x1: x1, y1: y1,
642
+ x2: x2, y2: y2,
643
+ x: @x, y: @y
644
+ }
645
+ end
646
+
647
+ # Process final line
648
+ if @stack.size == 2
649
+ dx = @stack.shift
650
+ dy = @stack.shift
651
+ @x += dx
652
+ @y += dy
653
+ @path << { type: :line_to, x: @x, y: @y }
654
+ end
655
+ @stack.clear
656
+ end
657
+
658
+ # rlinecurve: {dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve
659
+ # Lines followed by a curve
660
+ def rlinecurve
661
+ # Process lines (all but last 6 values)
662
+ while @stack.size > 6
663
+ dx = @stack.shift
664
+ dy = @stack.shift
665
+ @x += dx
666
+ @y += dy
667
+ @path << { type: :line_to, x: @x, y: @y }
668
+ end
669
+
670
+ # Process final curve
671
+ if @stack.size == 6
672
+ dx1 = @stack.shift
673
+ dy1 = @stack.shift
674
+ dx2 = @stack.shift
675
+ dy2 = @stack.shift
676
+ dx3 = @stack.shift
677
+ dy3 = @stack.shift
678
+
679
+ x1 = @x + dx1
680
+ y1 = @y + dy1
681
+ x2 = x1 + dx2
682
+ y2 = y1 + dy2
683
+ @x = x2 + dx3
684
+ @y = y2 + dy3
685
+
686
+ @path << {
687
+ type: :curve_to,
688
+ x1: x1, y1: y1,
689
+ x2: x2, y2: y2,
690
+ x: @x, y: @y
691
+ }
692
+ end
693
+ @stack.clear
694
+ end
695
+
696
+ # endchar: endchar
697
+ # End of glyph definition
698
+ def endchar
699
+ # Implicitly closes the path
700
+ @stack.clear
701
+ end
702
+
703
+ # Hint operators (stubbed - hints not needed for rendering)
704
+ def hint_operator(width_parsed)
705
+ parse_width_if_needed(width_parsed) unless width_parsed
706
+ # Count stems for width calculation
707
+ @stems += @stack.size / 2
708
+ @stack.clear
709
+ end
710
+
711
+ # Hintmask/cntrmask operators
712
+ def hintmask_operator
713
+ # Calculate number of bytes needed for hint mask
714
+ hint_bytes = (@stems + 7) / 8
715
+ # Skip hint mask bytes (not needed for rendering)
716
+ @io&.read(hint_bytes)
717
+ @stack.clear
718
+ end
719
+
720
+ # Flex operators (convert to curves)
721
+ def flex_operator(operator)
722
+ case operator
723
+ when :hflex
724
+ # dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex
725
+ dx1, dx2, dy2, dx3, dx4, dx5, dx6 = @stack.shift(7)
726
+ # Convert to two curves
727
+ add_curve(dx1, 0, dx2, dy2, dx3, 0)
728
+ add_curve(dx4, 0, dx5, -dy2, dx6, 0)
729
+ when :flex
730
+ # dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex
731
+ dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6,
732
+ _fd = @stack.shift(13)
733
+ # Convert to two curves
734
+ add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
735
+ add_curve(dx4, dy4, dx5, dy5, dx6, dy6)
736
+ when :hflex1
737
+ # dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1
738
+ dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = @stack.shift(9)
739
+ add_curve(dx1, dy1, dx2, dy2, dx3, 0)
740
+ add_curve(dx4, 0, dx5, dy5, dx6, -(dy1 + dy2 + dy5))
741
+ when :flex1
742
+ # dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1
743
+ dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 =
744
+ @stack.shift(11)
745
+ dx = dx1 + dx2 + dx3 + dx4 + dx5
746
+ dy = dy1 + dy2 + dy3 + dy4 + dy5
747
+ add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
748
+ if dx.abs > dy.abs
749
+ add_curve(dx4, dy4, dx5, dy5, d6, -dy)
750
+ else
751
+ add_curve(dx4, dy4, dx5, dy5, -dx, d6)
752
+ end
753
+ end
754
+ @stack.clear
755
+ end
756
+
757
+ # Helper to add a curve to the path
758
+ def add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
759
+ x1 = @x + dx1
760
+ y1 = @y + dy1
761
+ x2 = x1 + dx2
762
+ y2 = y1 + dy2
763
+ @x = x2 + dx3
764
+ @y = y2 + dy3
765
+
766
+ @path << {
767
+ type: :curve_to,
768
+ x1: x1, y1: y1,
769
+ x2: x2, y2: y2,
770
+ x: @x, y: @y
771
+ }
772
+ end
773
+
774
+ # Call local subroutine
775
+ def callsubr
776
+ subr_index = @stack.pop + @subroutine_bias
777
+ if @local_subrs && subr_index >= 0 && subr_index < @local_subrs.count
778
+ subr_data = @local_subrs[subr_index]
779
+ execute_subroutine(subr_data)
780
+ end
781
+ end
782
+
783
+ # Call global subroutine
784
+ def callgsubr
785
+ subr_index = @stack.pop + @global_subroutine_bias
786
+ if subr_index >= 0 && subr_index < @global_subrs.count
787
+ subr_data = @global_subrs[subr_index]
788
+ execute_subroutine(subr_data)
789
+ end
790
+ end
791
+
792
+ # Execute a subroutine
793
+ def execute_subroutine(data)
794
+ saved_io = @io
795
+ saved_data = @data
796
+ @data = data
797
+ @io = StringIO.new(data)
798
+
799
+ # Process subroutine until return or end
800
+ until @io.eof?
801
+ byte = @io.getbyte
802
+
803
+ if byte <= 31 && byte != 28
804
+ operator = read_operator(@io, byte)
805
+ break if operator == :return
806
+
807
+ execute_operator(operator, true) # Width already parsed
808
+ else
809
+ @io.pos -= 1
810
+ number = read_number(@io)
811
+ @stack << number
812
+ end
813
+ end
814
+
815
+ @io = saved_io
816
+ @data = saved_data
817
+ end
818
+
819
+ # Arithmetic operators
820
+ def arithmetic_add
821
+ b = @stack.pop
822
+ a = @stack.pop
823
+ @stack << (a + b)
824
+ end
825
+
826
+ def arithmetic_sub
827
+ b = @stack.pop
828
+ a = @stack.pop
829
+ @stack << (a - b)
830
+ end
831
+
832
+ def arithmetic_mul
833
+ b = @stack.pop
834
+ a = @stack.pop
835
+ @stack << (a * b)
836
+ end
837
+
838
+ def arithmetic_div
839
+ b = @stack.pop
840
+ a = @stack.pop
841
+ @stack << (a / b.to_f)
842
+ end
843
+
844
+ def arithmetic_neg
845
+ @stack << -@stack.pop
846
+ end
847
+
848
+ def arithmetic_abs
849
+ @stack << @stack.pop.abs
850
+ end
851
+
852
+ def arithmetic_sqrt
853
+ @stack << Math.sqrt(@stack.pop)
854
+ end
855
+
856
+ # Parse width for a specific operator
857
+ #
858
+ # @param width_parsed [Boolean] Whether width has already been parsed
859
+ # @param expected_operands [Integer] Number of operands this operator
860
+ # expects
861
+ def parse_width_for_operator(width_parsed, expected_operands)
862
+ return if width_parsed || @width
863
+
864
+ # Width is present if there's one more operand than expected
865
+ if @stack.size == expected_operands + 1
866
+ width_value = @stack.shift
867
+ @width = @private_dict.nominal_width_x + width_value
868
+ else
869
+ @width = @private_dict.default_width_x
870
+ end
871
+ end
872
+
873
+ # Parse width if present (for hint operators)
874
+ def parse_width_if_needed(width_parsed)
875
+ return if width_parsed || @width
876
+
877
+ # For hint operators, width is present if odd number of operands
878
+ if @stack.size.odd?
879
+ width_value = @stack.shift
880
+ @width = @private_dict.nominal_width_x + width_value
881
+ else
882
+ @width = @private_dict.default_width_x
883
+ end
884
+ end
885
+
886
+ # Calculate subroutine bias based on INDEX count
887
+ #
888
+ # @param index [Index, nil] Subroutine INDEX
889
+ # @return [Integer] Bias value
890
+ def calculate_bias(index)
891
+ return 0 unless index
892
+
893
+ count = index.count
894
+ if count < 1240
895
+ 107
896
+ elsif count < 33900
897
+ 1131
898
+ else
899
+ 32768
900
+ end
901
+ end
902
+ end
903
+ end
904
+ end
905
+ end