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,591 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../../binary/base_record"
5
+ require_relative "../cff/charstring"
6
+
7
+ module Fontisan
8
+ module Tables
9
+ class Cff2
10
+ # Type 2 CharString parser for CFF2 (variable fonts)
11
+ #
12
+ # CFF2 CharStrings extend Type 2 CharStrings with the blend operator
13
+ # for variation support. The blend operator applies variation deltas
14
+ # to base values based on design space coordinates.
15
+ #
16
+ # Blend Operator (operator 16):
17
+ # - Takes N*K+1 operands where:
18
+ # - N = number of design variation axes
19
+ # - K = number of values to blend
20
+ # - Format: [v1, Δv1_axis1, Δv1_axis2, ..., v2, Δv2_axis1, ..., K, N, blend]
21
+ # - Produces K blended values on the stack
22
+ #
23
+ # Example for 2 axes (wght, wdth) blending 3 values:
24
+ # Input: [100, 10, 5, 200, 20, 10, 50, 5, 2, 3, 2, blend]
25
+ # - v1=100 with deltas [10, 5]
26
+ # - v2=200 with deltas [20, 10]
27
+ # - v3=50 with deltas [5, 2]
28
+ # - K=3 (number of values), N=2 (number of axes)
29
+ #
30
+ # Reference: Adobe Technical Note #5177 (CFF2 specification)
31
+ #
32
+ # @example Parsing a CFF2 CharString with blend
33
+ # parser = Fontisan::Tables::Cff2::CharstringParser.new(
34
+ # data, num_axes, variation_store
35
+ # )
36
+ # charstring = parser.parse
37
+ # puts charstring.path
38
+ # puts charstring.blend_data
39
+ class CharstringParser
40
+ # @return [String] Binary CharString data
41
+ attr_reader :data
42
+
43
+ # @return [Integer] Number of variation axes
44
+ attr_reader :num_axes
45
+
46
+ # @return [Array<Hash>] Parsed path commands
47
+ attr_reader :path
48
+
49
+ # @return [Array<Hash>] Blend operator data
50
+ attr_reader :blend_data
51
+
52
+ # @return [Float] Current X coordinate
53
+ attr_reader :x
54
+
55
+ # @return [Float] Current Y coordinate
56
+ attr_reader :y
57
+
58
+ # @return [Integer, nil] Glyph width
59
+ attr_reader :width
60
+
61
+ # CFF2-specific operators
62
+ BLEND_OPERATOR = 16
63
+
64
+ # Initialize parser
65
+ #
66
+ # @param data [String] Binary CharString data
67
+ # @param num_axes [Integer] Number of variation axes (from fvar)
68
+ # @param global_subrs [Cff::Index, nil] Global subroutines INDEX
69
+ # @param local_subrs [Cff::Index, nil] Local subroutines INDEX
70
+ # @param vsindex [Integer] Variation store index (default 0)
71
+ def initialize(data, num_axes = 0, global_subrs = nil, local_subrs = nil, vsindex = 0)
72
+ @data = data
73
+ @num_axes = num_axes
74
+ @global_subrs = global_subrs
75
+ @local_subrs = local_subrs
76
+ @vsindex = vsindex
77
+
78
+ @path = []
79
+ @blend_data = []
80
+ @x = 0.0
81
+ @y = 0.0
82
+ @width = nil
83
+ @stems = 0
84
+ end
85
+
86
+ # Parse the CharString
87
+ #
88
+ # @return [self]
89
+ def parse
90
+ return self if @parsed
91
+
92
+ @stack = []
93
+ @io = StringIO.new(@data)
94
+ @io.set_encoding(Encoding::BINARY)
95
+
96
+ parse_charstring_program
97
+
98
+ @parsed = true
99
+ self
100
+ end
101
+
102
+ # Get blended values for a specific set of coordinates
103
+ #
104
+ # @param coordinates [Hash<String, Float>] Axis coordinates
105
+ # @return [Array<Float>] Blended values
106
+ def blend_values(coordinates)
107
+ return [] if @blend_data.empty?
108
+
109
+ # Apply blend operations with coordinates
110
+ @blend_data.map do |blend_op|
111
+ apply_blend(blend_op, coordinates)
112
+ end.flatten
113
+ end
114
+
115
+ # Convert path to drawing commands
116
+ #
117
+ # @return [Array<Array>] Array of command arrays
118
+ def to_commands
119
+ @path.map do |cmd|
120
+ case cmd[:type]
121
+ when :move_to
122
+ [:move_to, cmd[:x], cmd[:y]]
123
+ when :line_to
124
+ [:line_to, cmd[:x], cmd[:y]]
125
+ when :curve_to
126
+ [:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x], cmd[:y]]
127
+ end
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ # Parse the CharString program
134
+ def parse_charstring_program
135
+ until @io.eof?
136
+ byte = @io.getbyte
137
+
138
+ if operator_byte?(byte)
139
+ operator = read_operator(byte)
140
+ execute_operator(operator)
141
+ else
142
+ # Operand byte
143
+ @io.pos -= 1
144
+ number = read_number
145
+ @stack << number
146
+ end
147
+ end
148
+ rescue StandardError => e
149
+ raise CorruptedTableError, "Failed to parse CFF2 CharString: #{e.message}"
150
+ end
151
+
152
+ # Check if byte is an operator
153
+ #
154
+ # @param byte [Integer] Byte value
155
+ # @return [Boolean] True if operator
156
+ def operator_byte?(byte)
157
+ byte <= 31 && byte != 28
158
+ end
159
+
160
+ # Read an operator from the CharString
161
+ #
162
+ # @param first_byte [Integer] First operator byte
163
+ # @return [Integer, Array<Integer>] Operator code
164
+ def read_operator(first_byte)
165
+ if first_byte == 12
166
+ # Two-byte operator
167
+ second_byte = @io.getbyte
168
+ raise CorruptedTableError, "Unexpected end of CharString" if second_byte.nil?
169
+
170
+ [12, second_byte]
171
+ else
172
+ # Single-byte operator
173
+ first_byte
174
+ end
175
+ end
176
+
177
+ # Read a number from the CharString
178
+ #
179
+ # @return [Integer, Float] The number value
180
+ def read_number
181
+ byte = @io.getbyte
182
+ raise CorruptedTableError, "Unexpected end of CharString" if byte.nil?
183
+
184
+ case byte
185
+ when 28
186
+ # 3-byte signed integer (16-bit)
187
+ b1 = @io.getbyte
188
+ b2 = @io.getbyte
189
+ value = (b1 << 8) | b2
190
+ value > 0x7FFF ? value - 0x10000 : value
191
+ when 32..246
192
+ # Small integer: -107 to +107
193
+ byte - 139
194
+ when 247..250
195
+ # Positive 2-byte integer: +108 to +1131
196
+ b2 = @io.getbyte
197
+ (byte - 247) * 256 + b2 + 108
198
+ when 251..254
199
+ # Negative 2-byte integer: -108 to -1131
200
+ b2 = @io.getbyte
201
+ -(byte - 251) * 256 - b2 - 108
202
+ when 255
203
+ # 5-byte signed integer (32-bit) as fixed-point 16.16
204
+ bytes = @io.read(4)
205
+ value = bytes.unpack1("l>") # Signed 32-bit big-endian
206
+ value / 65536.0 # Convert to float
207
+ else
208
+ raise CorruptedTableError, "Invalid CharString number byte: #{byte}"
209
+ end
210
+ end
211
+
212
+ # Execute a CharString operator
213
+ #
214
+ # @param operator [Integer, Array<Integer>] Operator code
215
+ def execute_operator(operator)
216
+ case operator
217
+ when BLEND_OPERATOR
218
+ execute_blend
219
+ when 21 # rmoveto
220
+ rmoveto
221
+ when 22 # hmoveto
222
+ hmoveto
223
+ when 4 # vmoveto
224
+ vmoveto
225
+ when 5 # rlineto
226
+ rlineto
227
+ when 6 # hlineto
228
+ hlineto
229
+ when 7 # vlineto
230
+ vlineto
231
+ when 8 # rrcurveto
232
+ rrcurveto
233
+ when 27 # hhcurveto
234
+ hhcurveto
235
+ when 26 # vvcurveto
236
+ vvcurveto
237
+ when 31 # hvcurveto
238
+ hvcurveto
239
+ when 30 # vhcurveto
240
+ vhcurveto
241
+ when 14 # endchar
242
+ endchar
243
+ when 1, 3, 18, 23 # hstem, vstem, hstemhm, vstemhm
244
+ hint_operator
245
+ when 19, 20 # hintmask, cntrmask
246
+ hintmask_operator
247
+ when 10 # callsubr
248
+ callsubr
249
+ when 29 # callgsubr
250
+ callgsubr
251
+ else
252
+ # Unknown operator - clear stack
253
+ @stack.clear
254
+ end
255
+ end
256
+
257
+ # Execute blend operator
258
+ #
259
+ # Stack: v1 Δv1_1 ... Δv1_N v2 Δv2_1 ... Δv2_N ... K N blend
260
+ # Result: blended_v1 blended_v2 ... blended_vK
261
+ def execute_blend
262
+ return if @stack.size < 2
263
+
264
+ # Pop N (number of axes) and K (number of values to blend)
265
+ n = @stack.pop.to_i
266
+ k = @stack.pop.to_i
267
+
268
+ # Validate we have enough operands: K * (N + 1)
269
+ required_operands = k * (n + 1)
270
+ if @stack.size < required_operands
271
+ warn "Blend operator requires #{required_operands} operands, got #{@stack.size}"
272
+ @stack.clear
273
+ return
274
+ end
275
+
276
+ # Extract base values and deltas
277
+ blend_operands = @stack.pop(required_operands)
278
+ blends = []
279
+
280
+ k.times do |i|
281
+ offset = i * (n + 1)
282
+ base_value = blend_operands[offset]
283
+ deltas = blend_operands[offset + 1, n] || []
284
+
285
+ blends << {
286
+ base: base_value,
287
+ deltas: deltas,
288
+ num_axes: n,
289
+ }
290
+
291
+ # For now, push base value back (will be blended later with coordinates)
292
+ @stack << base_value
293
+ end
294
+
295
+ # Store blend data for later application
296
+ @blend_data << {
297
+ num_values: k,
298
+ num_axes: n,
299
+ blends: blends,
300
+ }
301
+ end
302
+
303
+ # Apply blend operation with coordinates
304
+ #
305
+ # @param blend_op [Hash] Blend operation data
306
+ # @param coordinates [Hash<String, Float>] Axis coordinates
307
+ # @return [Array<Float>] Blended values
308
+ def apply_blend(blend_op, coordinates)
309
+ blend_op[:blends].map do |blend|
310
+ base = blend[:base]
311
+ deltas = blend[:deltas]
312
+
313
+ # Apply deltas based on coordinates
314
+ # This will be enhanced when we have proper coordinate interpolation
315
+ blended_value = base
316
+ deltas.each_with_index do |delta, axis_index|
317
+ # Placeholder: use normalized coordinate (will be replaced with proper interpolation)
318
+ scalar = 0.0 # Will be calculated by interpolator
319
+ blended_value += delta * scalar
320
+ end
321
+
322
+ blended_value
323
+ end
324
+ end
325
+
326
+ # Path construction operators (simplified implementations)
327
+
328
+ def rmoveto
329
+ return if @stack.size < 2
330
+
331
+ dy = @stack.pop
332
+ dx = @stack.pop
333
+ @x += dx
334
+ @y += dy
335
+ @path << { type: :move_to, x: @x, y: @y }
336
+ @stack.clear
337
+ end
338
+
339
+ def hmoveto
340
+ return if @stack.empty?
341
+
342
+ dx = @stack.pop
343
+ @x += dx
344
+ @path << { type: :move_to, x: @x, y: @y }
345
+ @stack.clear
346
+ end
347
+
348
+ def vmoveto
349
+ return if @stack.empty?
350
+
351
+ dy = @stack.pop
352
+ @y += dy
353
+ @path << { type: :move_to, x: @x, y: @y }
354
+ @stack.clear
355
+ end
356
+
357
+ def rlineto
358
+ while @stack.size >= 2
359
+ dx = @stack.shift
360
+ dy = @stack.shift
361
+ @x += dx
362
+ @y += dy
363
+ @path << { type: :line_to, x: @x, y: @y }
364
+ end
365
+ @stack.clear
366
+ end
367
+
368
+ def hlineto
369
+ horizontal = true
370
+ while @stack.any?
371
+ delta = @stack.shift
372
+ if horizontal
373
+ @x += delta
374
+ else
375
+ @y += delta
376
+ end
377
+ @path << { type: :line_to, x: @x, y: @y }
378
+ horizontal = !horizontal
379
+ end
380
+ @stack.clear
381
+ end
382
+
383
+ def vlineto
384
+ vertical = true
385
+ while @stack.any?
386
+ delta = @stack.shift
387
+ if vertical
388
+ @y += delta
389
+ else
390
+ @x += delta
391
+ end
392
+ @path << { type: :line_to, x: @x, y: @y }
393
+ vertical = !vertical
394
+ end
395
+ @stack.clear
396
+ end
397
+
398
+ def rrcurveto
399
+ while @stack.size >= 6
400
+ dx1 = @stack.shift
401
+ dy1 = @stack.shift
402
+ dx2 = @stack.shift
403
+ dy2 = @stack.shift
404
+ dx3 = @stack.shift
405
+ dy3 = @stack.shift
406
+
407
+ x1 = @x + dx1
408
+ y1 = @y + dy1
409
+ x2 = x1 + dx2
410
+ y2 = y1 + dy2
411
+ @x = x2 + dx3
412
+ @y = y2 + dy3
413
+
414
+ @path << {
415
+ type: :curve_to,
416
+ x1: x1, y1: y1,
417
+ x2: x2, y2: y2,
418
+ x: @x, y: @y
419
+ }
420
+ end
421
+ @stack.clear
422
+ end
423
+
424
+ def hhcurveto
425
+ if @stack.size.odd?
426
+ @y += @stack.shift
427
+ end
428
+
429
+ while @stack.size >= 4
430
+ dx1 = @stack.shift
431
+ dx2 = @stack.shift
432
+ dy2 = @stack.shift
433
+ dx3 = @stack.shift
434
+
435
+ x1 = @x + dx1
436
+ y1 = @y
437
+ x2 = x1 + dx2
438
+ y2 = y1 + dy2
439
+ @x = x2 + dx3
440
+ @y = y2
441
+
442
+ @path << {
443
+ type: :curve_to,
444
+ x1: x1, y1: y1,
445
+ x2: x2, y2: y2,
446
+ x: @x, y: @y
447
+ }
448
+ end
449
+ @stack.clear
450
+ end
451
+
452
+ def vvcurveto
453
+ if @stack.size.odd?
454
+ @x += @stack.shift
455
+ end
456
+
457
+ while @stack.size >= 4
458
+ dy1 = @stack.shift
459
+ dx2 = @stack.shift
460
+ dy2 = @stack.shift
461
+ dy3 = @stack.shift
462
+
463
+ x1 = @x
464
+ y1 = @y + dy1
465
+ x2 = x1 + dx2
466
+ y2 = y1 + dy2
467
+ @x = x2
468
+ @y = y2 + dy3
469
+
470
+ @path << {
471
+ type: :curve_to,
472
+ x1: x1, y1: y1,
473
+ x2: x2, y2: y2,
474
+ x: @x, y: @y
475
+ }
476
+ end
477
+ @stack.clear
478
+ end
479
+
480
+ def hvcurveto
481
+ horizontal_first = true
482
+ while @stack.size >= 4
483
+ if horizontal_first
484
+ dx1 = @stack.shift
485
+ dx2 = @stack.shift
486
+ dy2 = @stack.shift
487
+ dy3 = @stack.shift
488
+ dx3 = @stack.size == 1 ? @stack.shift : 0
489
+
490
+ x1 = @x + dx1
491
+ y1 = @y
492
+ else
493
+ dy1 = @stack.shift
494
+ dx2 = @stack.shift
495
+ dy2 = @stack.shift
496
+ dx3 = @stack.shift
497
+ dy3 = @stack.size == 1 ? @stack.shift : 0
498
+
499
+ x1 = @x
500
+ y1 = @y + dy1
501
+ end
502
+
503
+ x2 = x1 + dx2
504
+ y2 = y1 + dy2
505
+ @x = x2 + dx3
506
+ @y = y2 + dy3
507
+
508
+ @path << {
509
+ type: :curve_to,
510
+ x1: x1, y1: y1,
511
+ x2: x2, y2: y2,
512
+ x: @x, y: @y
513
+ }
514
+ horizontal_first = !horizontal_first
515
+ end
516
+ @stack.clear
517
+ end
518
+
519
+ def vhcurveto
520
+ vertical_first = true
521
+ while @stack.size >= 4
522
+ if vertical_first
523
+ dy1 = @stack.shift
524
+ dx2 = @stack.shift
525
+ dy2 = @stack.shift
526
+ dx3 = @stack.shift
527
+ dy3 = @stack.size == 1 ? @stack.shift : 0
528
+
529
+ x1 = @x
530
+ y1 = @y + dy1
531
+ else
532
+ dx1 = @stack.shift
533
+ dx2 = @stack.shift
534
+ dy2 = @stack.shift
535
+ dy3 = @stack.shift
536
+ dx3 = @stack.size == 1 ? @stack.shift : 0
537
+
538
+ x1 = @x + dx1
539
+ y1 = @y
540
+ end
541
+
542
+ x2 = x1 + dx2
543
+ y2 = y1 + dy2
544
+ @x = x2 + dx3
545
+ @y = y2 + dy3
546
+
547
+ @path << {
548
+ type: :curve_to,
549
+ x1: x1, y1: y1,
550
+ x2: x2, y2: y2,
551
+ x: @x, y: @y
552
+ }
553
+ vertical_first = !vertical_first
554
+ end
555
+ @stack.clear
556
+ end
557
+
558
+ def endchar
559
+ @stack.clear
560
+ end
561
+
562
+ def hint_operator
563
+ @stems += @stack.size / 2
564
+ @stack.clear
565
+ end
566
+
567
+ def hintmask_operator
568
+ hint_bytes = (@stems + 7) / 8
569
+ @io.read(hint_bytes)
570
+ @stack.clear
571
+ end
572
+
573
+ def callsubr
574
+ return if @local_subrs.nil? || @stack.empty?
575
+
576
+ subr_index = @stack.pop
577
+ # Implement subroutine call (placeholder)
578
+ @stack.clear
579
+ end
580
+
581
+ def callgsubr
582
+ return if @global_subrs.nil? || @stack.empty?
583
+
584
+ subr_index = @stack.pop
585
+ # Implement global subroutine call (placeholder)
586
+ @stack.clear
587
+ end
588
+ end
589
+ end
590
+ end
591
+ end