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,934 @@
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
+ raise CorruptedTableError, "Unexpected end of CharString reading shortint" if
243
+ b1.nil? || b2.nil?
244
+ value = (b1 << 8) | b2
245
+ value > 0x7FFF ? value - 0x10000 : value
246
+ when 32..246
247
+ # Small integer: -107 to +107
248
+ byte - 139
249
+ when 247..250
250
+ # Positive 2-byte integer: +108 to +1131
251
+ b2 = io.getbyte
252
+ raise CorruptedTableError, "Unexpected end of CharString reading positive integer" if
253
+ b2.nil?
254
+ (byte - 247) * 256 + b2 + 108
255
+ when 251..254
256
+ # Negative 2-byte integer: -108 to -1131
257
+ b2 = io.getbyte
258
+ raise CorruptedTableError, "Unexpected end of CharString reading negative integer" if
259
+ b2.nil?
260
+ -(byte - 251) * 256 - b2 - 108
261
+ when 255
262
+ # 5-byte signed integer (32-bit) as fixed-point 16.16
263
+ bytes = io.read(4)
264
+ raise CorruptedTableError, "Unexpected end of CharString reading fixed-point" if
265
+ bytes.nil? || bytes.length < 4
266
+ value = bytes.unpack1("l>") # Signed 32-bit big-endian
267
+ value / 65536.0 # Convert to float
268
+ else
269
+ raise CorruptedTableError, "Invalid CharString number byte: #{byte}"
270
+ end
271
+ end
272
+
273
+ # Execute a CharString operator
274
+ #
275
+ # @param operator [Symbol] Operator name
276
+ # @param width_parsed [Boolean] Whether width has been parsed
277
+ def execute_operator(operator, width_parsed)
278
+ case operator
279
+ # Path construction operators
280
+ when :rmoveto
281
+ rmoveto(width_parsed)
282
+ true # Width has now been parsed
283
+ when :hmoveto
284
+ hmoveto(width_parsed)
285
+ true # Width has now been parsed
286
+ when :vmoveto
287
+ vmoveto(width_parsed)
288
+ true # Width has now been parsed
289
+ when :rlineto
290
+ rlineto
291
+ when :hlineto
292
+ hlineto
293
+ when :vlineto
294
+ vlineto
295
+ when :rrcurveto
296
+ rrcurveto
297
+ when :hhcurveto
298
+ hhcurveto
299
+ when :vvcurveto
300
+ vvcurveto
301
+ when :hvcurveto
302
+ hvcurveto
303
+ when :vhcurveto
304
+ vhcurveto
305
+ when :rcurveline
306
+ rcurveline
307
+ when :rlinecurve
308
+ rlinecurve
309
+ when :endchar
310
+ endchar
311
+
312
+ # Hint operators (stub for now)
313
+ when :hstem, :vstem, :hstemhm, :vstemhm
314
+ hint_operator(width_parsed)
315
+ when :hintmask, :cntrmask
316
+ hintmask_operator
317
+
318
+ # Subroutine operators
319
+ when :callsubr
320
+ callsubr
321
+ when :callgsubr
322
+ callgsubr
323
+ when :return
324
+ # Return is handled by subroutine execution
325
+
326
+ # Arithmetic operators
327
+ when :add
328
+ arithmetic_add
329
+ when :sub
330
+ arithmetic_sub
331
+ when :mul
332
+ arithmetic_mul
333
+ when :div
334
+ arithmetic_div
335
+ when :neg
336
+ arithmetic_neg
337
+ when :abs
338
+ arithmetic_abs
339
+ when :sqrt
340
+ arithmetic_sqrt
341
+ when :drop
342
+ @stack.pop
343
+ when :exch
344
+ @stack[-1], @stack[-2] = @stack[-2], @stack[-1]
345
+ when :dup
346
+ @stack << @stack.last
347
+ when :put
348
+ value = @stack.pop
349
+ index = @stack.pop
350
+ @transient_array[index] = value
351
+ when :get
352
+ index = @stack.pop
353
+ @stack << (@transient_array[index] || 0)
354
+
355
+ # Flex operators
356
+ when :hflex, :flex, :hflex1, :flex1
357
+ flex_operator(operator)
358
+
359
+ when :unknown
360
+ # Unknown operator - clear stack and continue
361
+ @stack.clear
362
+ end
363
+ end
364
+
365
+ # rmoveto: dx dy rmoveto
366
+ # Relative move to (dx, dy)
367
+ def rmoveto(width_parsed)
368
+ # rmoveto takes 2 operands, so if stack has 3 and width not parsed,
369
+ # first is width
370
+ parse_width_for_operator(width_parsed, 2)
371
+ return if @stack.size < 2 # Need at least 2 values
372
+ dy = @stack.pop
373
+ dx = @stack.pop
374
+ @x += dx
375
+ @y += dy
376
+ @path << { type: :move_to, x: @x, y: @y }
377
+ @stack.clear
378
+ end
379
+
380
+ # hmoveto: dx hmoveto
381
+ # Horizontal move to (dx, 0)
382
+ def hmoveto(width_parsed)
383
+ # hmoveto takes 1 operand, so if stack has 2 and width not parsed,
384
+ # first is width
385
+ parse_width_for_operator(width_parsed, 1)
386
+ return if @stack.empty? # Need at least 1 value
387
+ dx = @stack.pop || 0
388
+ @x += dx
389
+ @path << { type: :move_to, x: @x, y: @y }
390
+ @stack.clear
391
+ end
392
+
393
+ # vmoveto: dy vmoveto
394
+ # Vertical move to (0, dy)
395
+ def vmoveto(width_parsed)
396
+ # vmoveto takes 1 operand, so if stack has 2 and width not parsed,
397
+ # first is width
398
+ parse_width_for_operator(width_parsed, 1)
399
+ return if @stack.empty? # Need at least 1 value
400
+ dy = @stack.pop || 0
401
+ @y += dy
402
+ @path << { type: :move_to, x: @x, y: @y }
403
+ @stack.clear
404
+ end
405
+
406
+ # rlineto: {dxa dya}+ rlineto
407
+ # Relative line to
408
+ def rlineto
409
+ while @stack.size >= 2
410
+ dx = @stack.shift
411
+ dy = @stack.shift
412
+ @x += dx
413
+ @y += dy
414
+ @path << { type: :line_to, x: @x, y: @y }
415
+ end
416
+ @stack.clear
417
+ end
418
+
419
+ # hlineto: dx1 {dya dxb}* hlineto
420
+ # Alternating horizontal and vertical lines
421
+ def hlineto
422
+ horizontal = true
423
+ while @stack.any?
424
+ delta = @stack.shift
425
+ if horizontal
426
+ @x += delta
427
+ else
428
+ @y += delta
429
+ end
430
+ @path << { type: :line_to, x: @x, y: @y }
431
+ horizontal = !horizontal
432
+ end
433
+ @stack.clear
434
+ end
435
+
436
+ # vlineto: dy1 {dxb dya}* vlineto
437
+ # Alternating vertical and horizontal lines
438
+ def vlineto
439
+ vertical = true
440
+ while @stack.any?
441
+ delta = @stack.shift
442
+ if vertical
443
+ @y += delta
444
+ else
445
+ @x += delta
446
+ end
447
+ @path << { type: :line_to, x: @x, y: @y }
448
+ vertical = !vertical
449
+ end
450
+ @stack.clear
451
+ end
452
+
453
+ # rrcurveto: {dxa dya dxb dyb dxc dyc}+ rrcurveto
454
+ # Relative cubic Bézier curve
455
+ def rrcurveto
456
+ while @stack.size >= 6
457
+ dx1 = @stack.shift
458
+ dy1 = @stack.shift
459
+ dx2 = @stack.shift
460
+ dy2 = @stack.shift
461
+ dx3 = @stack.shift
462
+ dy3 = @stack.shift
463
+
464
+ x1 = @x + dx1
465
+ y1 = @y + dy1
466
+ x2 = x1 + dx2
467
+ y2 = y1 + dy2
468
+ @x = x2 + dx3
469
+ @y = y2 + dy3
470
+
471
+ @path << {
472
+ type: :curve_to,
473
+ x1: x1, y1: y1,
474
+ x2: x2, y2: y2,
475
+ x: @x, y: @y
476
+ }
477
+ end
478
+ @stack.clear
479
+ end
480
+
481
+ # hhcurveto: dy1? {dxa dxb dyb dxc}+ hhcurveto
482
+ # Horizontal-horizontal curve
483
+ def hhcurveto
484
+ # First value might be dy1 if odd number of args
485
+ if @stack.size.odd?
486
+ @y += @stack.shift
487
+ end
488
+
489
+ while @stack.size >= 4
490
+ dx1 = @stack.shift
491
+ dx2 = @stack.shift
492
+ dy2 = @stack.shift
493
+ dx3 = @stack.shift
494
+
495
+ x1 = @x + dx1
496
+ y1 = @y
497
+ x2 = x1 + dx2
498
+ y2 = y1 + dy2
499
+ @x = x2 + dx3
500
+ @y = y2
501
+
502
+ @path << {
503
+ type: :curve_to,
504
+ x1: x1, y1: y1,
505
+ x2: x2, y2: y2,
506
+ x: @x, y: @y
507
+ }
508
+ end
509
+ @stack.clear
510
+ end
511
+
512
+ # vvcurveto: dx1? {dya dxb dyb dyc}+ vvcurveto
513
+ # Vertical-vertical curve
514
+ def vvcurveto
515
+ # First value might be dx1 if odd number of args
516
+ if @stack.size.odd?
517
+ @x += @stack.shift
518
+ end
519
+
520
+ while @stack.size >= 4
521
+ dy1 = @stack.shift
522
+ dx2 = @stack.shift
523
+ dy2 = @stack.shift
524
+ dy3 = @stack.shift
525
+
526
+ x1 = @x
527
+ y1 = @y + dy1
528
+ x2 = x1 + dx2
529
+ y2 = y1 + dy2
530
+ @x = x2
531
+ @y = y2 + dy3
532
+
533
+ @path << {
534
+ type: :curve_to,
535
+ x1: x1, y1: y1,
536
+ x2: x2, y2: y2,
537
+ x: @x, y: @y
538
+ }
539
+ end
540
+ @stack.clear
541
+ end
542
+
543
+ # hvcurveto: dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf?
544
+ # hvcurveto
545
+ # Horizontal-vertical curve
546
+ def hvcurveto
547
+ horizontal_first = true
548
+ while @stack.size >= 4
549
+ if horizontal_first
550
+ dx1 = @stack.shift
551
+ dx2 = @stack.shift
552
+ dy2 = @stack.shift
553
+ dy3 = @stack.shift
554
+ # Handle final dx if this is the last curve
555
+ dx3 = @stack.size == 1 ? @stack.shift : 0
556
+
557
+ x1 = @x + dx1
558
+ y1 = @y
559
+ else
560
+ dy1 = @stack.shift
561
+ dx2 = @stack.shift
562
+ dy2 = @stack.shift
563
+ dx3 = @stack.shift
564
+ # Handle final dy if this is the last curve
565
+ dy3 = @stack.size == 1 ? @stack.shift : 0
566
+
567
+ x1 = @x
568
+ y1 = @y + dy1
569
+ end
570
+ x2 = x1 + dx2
571
+ y2 = y1 + dy2
572
+ @x = x2 + dx3
573
+ @y = y2 + dy3
574
+
575
+ @path << {
576
+ type: :curve_to,
577
+ x1: x1, y1: y1,
578
+ x2: x2, y2: y2,
579
+ x: @x, y: @y
580
+ }
581
+ horizontal_first = !horizontal_first
582
+ end
583
+ @stack.clear
584
+ end
585
+
586
+ # vhcurveto: dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf?
587
+ # vhcurveto
588
+ # Vertical-horizontal curve
589
+ def vhcurveto
590
+ vertical_first = true
591
+ while @stack.size >= 4
592
+ if vertical_first
593
+ dy1 = @stack.shift
594
+ dx2 = @stack.shift
595
+ dy2 = @stack.shift
596
+ dx3 = @stack.shift
597
+ # Handle final dy if this is the last curve
598
+ dy3 = @stack.size == 1 ? @stack.shift : 0
599
+
600
+ x1 = @x
601
+ y1 = @y + dy1
602
+ else
603
+ dx1 = @stack.shift
604
+ dx2 = @stack.shift
605
+ dy2 = @stack.shift
606
+ dy3 = @stack.shift
607
+ # Handle final dx if this is the last curve
608
+ dx3 = @stack.size == 1 ? @stack.shift : 0
609
+
610
+ x1 = @x + dx1
611
+ y1 = @y
612
+ end
613
+ x2 = x1 + dx2
614
+ y2 = y1 + dy2
615
+ @x = x2 + dx3
616
+ @y = y2 + dy3
617
+
618
+ @path << {
619
+ type: :curve_to,
620
+ x1: x1, y1: y1,
621
+ x2: x2, y2: y2,
622
+ x: @x, y: @y
623
+ }
624
+ vertical_first = !vertical_first
625
+ end
626
+ @stack.clear
627
+ end
628
+
629
+ # rcurveline: {dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline
630
+ # Curves followed by a line
631
+ def rcurveline
632
+ # Process curves (all but last 2 values)
633
+ while @stack.size > 2
634
+ break if @stack.size < 6
635
+
636
+ dx1 = @stack.shift
637
+ dy1 = @stack.shift
638
+ dx2 = @stack.shift
639
+ dy2 = @stack.shift
640
+ dx3 = @stack.shift
641
+ dy3 = @stack.shift
642
+
643
+ x1 = @x + dx1
644
+ y1 = @y + dy1
645
+ x2 = x1 + dx2
646
+ y2 = y1 + dy2
647
+ @x = x2 + dx3
648
+ @y = y2 + dy3
649
+
650
+ @path << {
651
+ type: :curve_to,
652
+ x1: x1, y1: y1,
653
+ x2: x2, y2: y2,
654
+ x: @x, y: @y
655
+ }
656
+ end
657
+
658
+ # Process final line
659
+ if @stack.size == 2
660
+ dx = @stack.shift
661
+ dy = @stack.shift
662
+ @x += dx
663
+ @y += dy
664
+ @path << { type: :line_to, x: @x, y: @y }
665
+ end
666
+ @stack.clear
667
+ end
668
+
669
+ # rlinecurve: {dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve
670
+ # Lines followed by a curve
671
+ def rlinecurve
672
+ # Process lines (all but last 6 values)
673
+ while @stack.size > 6
674
+ dx = @stack.shift
675
+ dy = @stack.shift
676
+ @x += dx
677
+ @y += dy
678
+ @path << { type: :line_to, x: @x, y: @y }
679
+ end
680
+
681
+ # Process final curve
682
+ if @stack.size == 6
683
+ dx1 = @stack.shift
684
+ dy1 = @stack.shift
685
+ dx2 = @stack.shift
686
+ dy2 = @stack.shift
687
+ dx3 = @stack.shift
688
+ dy3 = @stack.shift
689
+
690
+ x1 = @x + dx1
691
+ y1 = @y +dy1
692
+ x2 = x1 + dx2
693
+ y2 = y1 + dy2
694
+ @x = x2 + dx3
695
+ @y = y2 + dy3
696
+
697
+ @path << {
698
+ type: :curve_to,
699
+ x1: x1, y1: y1,
700
+ x2: x2, y2: y2,
701
+ x: @x, y: @y
702
+ }
703
+ end
704
+ @stack.clear
705
+ end
706
+
707
+ # endchar: endchar
708
+ # End of glyph definition
709
+ def endchar
710
+ # Implicitly closes the path
711
+ @stack.clear
712
+ end
713
+
714
+ # Hint operators (stubbed - hints not needed for rendering)
715
+ def hint_operator(width_parsed)
716
+ parse_width_if_needed(width_parsed) unless width_parsed
717
+ # Count stems for width calculation
718
+ @stems += @stack.size / 2
719
+ @stack.clear
720
+ end
721
+
722
+ # Hintmask/cntrmask operators
723
+ def hintmask_operator
724
+ # Calculate number of bytes needed for hint mask
725
+ hint_bytes = (@stems + 7) / 8
726
+ # Skip hint mask bytes (not needed for rendering)
727
+ @io&.read(hint_bytes)
728
+ @stack.clear
729
+ end
730
+
731
+ # Flex operators (convert to curves)
732
+ def flex_operator(operator)
733
+ case operator
734
+ when :hflex
735
+ # dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex
736
+ dx1, dx2, dy2, dx3, dx4, dx5, dx6 = @stack.shift(7)
737
+ # Convert to two curves
738
+ add_curve(dx1, 0, dx2, dy2, dx3, 0)
739
+ add_curve(dx4, 0, dx5, -dy2, dx6, 0)
740
+ when :flex
741
+ # dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex
742
+ dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6,
743
+ _fd = @stack.shift(13)
744
+ # Convert to two curves
745
+ add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
746
+ add_curve(dx4, dy4, dx5, dy5, dx6, dy6)
747
+ when :hflex1
748
+ # dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1
749
+ dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = @stack.shift(9)
750
+ add_curve(dx1, dy1, dx2, dy2, dx3, 0)
751
+ add_curve(dx4, 0, dx5, dy5, dx6, -(dy1 + dy2 + dy5))
752
+ when :flex1
753
+ # dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1
754
+ dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, d6 =
755
+ @stack.shift(11)
756
+ dx = dx1 + dx2 + dx3 + dx4 + dx5
757
+ dy = dy1 + dy2 + dy3 + dy4 + dy5
758
+ add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
759
+ if dx.abs > dy.abs
760
+ add_curve(dx4, dy4, dx5, dy5, d6, -dy)
761
+ else
762
+ add_curve(dx4, dy4, dx5, dy5, -dx, d6)
763
+ end
764
+ end
765
+ @stack.clear
766
+ end
767
+
768
+ # Helper to add a curve to the path
769
+ def add_curve(dx1, dy1, dx2, dy2, dx3, dy3)
770
+ x1 = @x + dx1
771
+ y1 = @y + dy1
772
+ x2 = x1 + dx2
773
+ y2 = y1 + dy2
774
+ @x = x2 + dx3
775
+ @y = y2 + dy3
776
+
777
+ @path << {
778
+ type: :curve_to,
779
+ x1: x1, y1: y1,
780
+ x2: x2, y2: y2,
781
+ x: @x, y: @y
782
+ }
783
+ end
784
+
785
+ # Call local subroutine
786
+ def callsubr
787
+ return if @stack.empty?
788
+ subr_num = @stack.pop
789
+ return unless subr_num # Guard against empty stack
790
+
791
+ subr_index = subr_num + @subroutine_bias
792
+ if @local_subrs && subr_index >= 0 && subr_index < @local_subrs.count
793
+ subr_data = @local_subrs[subr_index]
794
+ execute_subroutine(subr_data)
795
+ end
796
+ end
797
+
798
+ # Call global subroutine
799
+ def callgsubr
800
+ return if @stack.empty?
801
+ subr_num = @stack.pop
802
+ return unless subr_num # Guard against empty stack
803
+
804
+ subr_index = subr_num + @global_subroutine_bias
805
+ if subr_index >= 0 && subr_index < @global_subrs.count
806
+ subr_data = @global_subrs[subr_index]
807
+ execute_subroutine(subr_data)
808
+ end
809
+ end
810
+
811
+ # Execute a subroutine
812
+ def execute_subroutine(data)
813
+ saved_io = @io
814
+ saved_data = @data
815
+ @data = data
816
+ @io = StringIO.new(data)
817
+
818
+ # Process subroutine until return or end
819
+ until @io.eof?
820
+ byte = @io.getbyte
821
+
822
+ if byte <= 31 && byte != 28
823
+ operator = read_operator(@io, byte)
824
+ break if operator == :return
825
+
826
+ execute_operator(operator, true) # Width already parsed
827
+ else
828
+ @io.pos -= 1
829
+ number = read_number(@io)
830
+ @stack << number
831
+ end
832
+ end
833
+
834
+ @io = saved_io
835
+ @data = saved_data
836
+ end
837
+
838
+ # Arithmetic operators
839
+ def arithmetic_add
840
+ return if @stack.size < 2
841
+ b = @stack.pop
842
+ a = @stack.pop
843
+ @stack << (a + b)
844
+ end
845
+
846
+ def arithmetic_sub
847
+ return if @stack.size < 2
848
+ b = @stack.pop
849
+ a = @stack.pop
850
+ @stack << (a - b)
851
+ end
852
+
853
+ def arithmetic_mul
854
+ return if @stack.size < 2
855
+ b = @stack.pop
856
+ a = @stack.pop
857
+ @stack << (a * b)
858
+ end
859
+
860
+ def arithmetic_div
861
+ return if @stack.size < 2
862
+ b = @stack.pop
863
+ a = @stack.pop
864
+ return if b.zero?
865
+ @stack << (a / b.to_f)
866
+ end
867
+
868
+ def arithmetic_neg
869
+ return if @stack.empty?
870
+ @stack << -@stack.pop
871
+ end
872
+
873
+ def arithmetic_abs
874
+ return if @stack.empty?
875
+ @stack << @stack.pop.abs
876
+ end
877
+
878
+ def arithmetic_sqrt
879
+ return if @stack.empty?
880
+ val = @stack.pop
881
+ return if val.negative?
882
+ @stack << Math.sqrt(val)
883
+ end
884
+
885
+ # Parse width for a specific operator
886
+ #
887
+ # @param width_parsed [Boolean] Whether width has already been parsed
888
+ # @param expected_operands [Integer] Number of operands this operator
889
+ # expects
890
+ def parse_width_for_operator(width_parsed, expected_operands)
891
+ return if width_parsed || @width
892
+
893
+ # Width is present if there's one more operand than expected
894
+ if @stack.size == expected_operands + 1
895
+ width_value = @stack.shift
896
+ @width = @private_dict.nominal_width_x + width_value
897
+ else
898
+ @width = @private_dict.default_width_x
899
+ end
900
+ end
901
+
902
+ # Parse width if present (for hint operators)
903
+ def parse_width_if_needed(width_parsed)
904
+ return if width_parsed || @width
905
+
906
+ # For hint operators, width is present if odd number of operands
907
+ if @stack.size.odd?
908
+ width_value = @stack.shift
909
+ @width = @private_dict.nominal_width_x + width_value
910
+ else
911
+ @width = @private_dict.default_width_x
912
+ end
913
+ end
914
+
915
+ # Calculate subroutine bias based on INDEX count
916
+ #
917
+ # @param index [Index, nil] Subroutine INDEX
918
+ # @return [Integer] Bias value
919
+ def calculate_bias(index)
920
+ return 0 unless index
921
+
922
+ count = index.count
923
+ if count < 1240
924
+ 107
925
+ elsif count < 33900
926
+ 1131
927
+ else
928
+ 32768
929
+ end
930
+ end
931
+ end
932
+ end
933
+ end
934
+ end