fontisan 0.2.1 → 0.2.3

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -392
  3. data/README.adoc +1509 -1430
  4. data/Rakefile +3 -2
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/base_collection.rb +296 -0
  9. data/lib/fontisan/cli.rb +10 -3
  10. data/lib/fontisan/collection/builder.rb +2 -1
  11. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  12. data/lib/fontisan/commands/base_command.rb +5 -2
  13. data/lib/fontisan/commands/convert_command.rb +6 -2
  14. data/lib/fontisan/commands/info_command.rb +129 -5
  15. data/lib/fontisan/commands/instance_command.rb +8 -7
  16. data/lib/fontisan/commands/validate_command.rb +4 -1
  17. data/lib/fontisan/constants.rb +24 -24
  18. data/lib/fontisan/converters/format_converter.rb +8 -4
  19. data/lib/fontisan/converters/outline_converter.rb +21 -16
  20. data/lib/fontisan/converters/woff_writer.rb +8 -3
  21. data/lib/fontisan/font_loader.rb +120 -30
  22. data/lib/fontisan/font_writer.rb +2 -0
  23. data/lib/fontisan/formatters/text_formatter.rb +116 -19
  24. data/lib/fontisan/hints/hint_converter.rb +43 -47
  25. data/lib/fontisan/hints/hint_validator.rb +284 -0
  26. data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
  27. data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
  28. data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
  29. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  30. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  31. data/lib/fontisan/loading_modes.rb +4 -4
  32. data/lib/fontisan/models/collection_brief_info.rb +37 -0
  33. data/lib/fontisan/models/collection_info.rb +6 -1
  34. data/lib/fontisan/models/font_export.rb +2 -2
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +22 -23
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/models/validation_report.rb +1 -1
  39. data/lib/fontisan/open_type_collection.rb +17 -220
  40. data/lib/fontisan/open_type_font.rb +3 -1
  41. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  42. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  43. data/lib/fontisan/pipeline/output_writer.rb +8 -3
  44. data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
  45. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  46. data/lib/fontisan/tables/cff/charstring.rb +38 -12
  47. data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
  48. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
  49. data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
  50. data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
  51. data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
  52. data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
  53. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  54. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  55. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
  56. data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
  57. data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
  58. data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
  59. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
  60. data/lib/fontisan/tables/cff2.rb +1 -1
  61. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  62. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  63. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  64. data/lib/fontisan/tables/name.rb +4 -4
  65. data/lib/fontisan/true_type_collection.rb +29 -113
  66. data/lib/fontisan/true_type_font.rb +3 -1
  67. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  68. data/lib/fontisan/variation/cache.rb +3 -1
  69. data/lib/fontisan/variation/converter.rb +2 -1
  70. data/lib/fontisan/variation/delta_applier.rb +2 -1
  71. data/lib/fontisan/variation/inspector.rb +2 -1
  72. data/lib/fontisan/variation/instance_generator.rb +2 -1
  73. data/lib/fontisan/variation/optimizer.rb +6 -3
  74. data/lib/fontisan/variation/subsetter.rb +32 -10
  75. data/lib/fontisan/variation/variation_preserver.rb +4 -1
  76. data/lib/fontisan/version.rb +1 -1
  77. data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
  78. data/lib/fontisan/woff2_font.rb +31 -15
  79. data/lib/fontisan.rb +42 -2
  80. data/scripts/measure_optimization.rb +15 -7
  81. metadata +9 -2
@@ -239,8 +239,11 @@ module Fontisan
239
239
  # 3-byte signed integer (16-bit)
240
240
  b1 = io.getbyte
241
241
  b2 = io.getbyte
242
- raise CorruptedTableError, "Unexpected end of CharString reading shortint" if
243
- b1.nil? || b2.nil?
242
+ if b1.nil? || b2.nil?
243
+ raise CorruptedTableError,
244
+ "Unexpected end of CharString reading shortint"
245
+ end
246
+
244
247
  value = (b1 << 8) | b2
245
248
  value > 0x7FFF ? value - 0x10000 : value
246
249
  when 32..246
@@ -249,20 +252,29 @@ module Fontisan
249
252
  when 247..250
250
253
  # Positive 2-byte integer: +108 to +1131
251
254
  b2 = io.getbyte
252
- raise CorruptedTableError, "Unexpected end of CharString reading positive integer" if
253
- b2.nil?
255
+ if b2.nil?
256
+ raise CorruptedTableError,
257
+ "Unexpected end of CharString reading positive integer"
258
+ end
259
+
254
260
  (byte - 247) * 256 + b2 + 108
255
261
  when 251..254
256
262
  # Negative 2-byte integer: -108 to -1131
257
263
  b2 = io.getbyte
258
- raise CorruptedTableError, "Unexpected end of CharString reading negative integer" if
259
- b2.nil?
264
+ if b2.nil?
265
+ raise CorruptedTableError,
266
+ "Unexpected end of CharString reading negative integer"
267
+ end
268
+
260
269
  -(byte - 251) * 256 - b2 - 108
261
270
  when 255
262
271
  # 5-byte signed integer (32-bit) as fixed-point 16.16
263
272
  bytes = io.read(4)
264
- raise CorruptedTableError, "Unexpected end of CharString reading fixed-point" if
265
- bytes.nil? || bytes.length < 4
273
+ if bytes.nil? || bytes.length < 4
274
+ raise CorruptedTableError,
275
+ "Unexpected end of CharString reading fixed-point"
276
+ end
277
+
266
278
  value = bytes.unpack1("l>") # Signed 32-bit big-endian
267
279
  value / 65536.0 # Convert to float
268
280
  else
@@ -368,7 +380,8 @@ module Fontisan
368
380
  # rmoveto takes 2 operands, so if stack has 3 and width not parsed,
369
381
  # first is width
370
382
  parse_width_for_operator(width_parsed, 2)
371
- return if @stack.size < 2 # Need at least 2 values
383
+ return if @stack.size < 2 # Need at least 2 values
384
+
372
385
  dy = @stack.pop
373
386
  dx = @stack.pop
374
387
  @x += dx
@@ -383,7 +396,8 @@ module Fontisan
383
396
  # hmoveto takes 1 operand, so if stack has 2 and width not parsed,
384
397
  # first is width
385
398
  parse_width_for_operator(width_parsed, 1)
386
- return if @stack.empty? # Need at least 1 value
399
+ return if @stack.empty? # Need at least 1 value
400
+
387
401
  dx = @stack.pop || 0
388
402
  @x += dx
389
403
  @path << { type: :move_to, x: @x, y: @y }
@@ -396,7 +410,8 @@ module Fontisan
396
410
  # vmoveto takes 1 operand, so if stack has 2 and width not parsed,
397
411
  # first is width
398
412
  parse_width_for_operator(width_parsed, 1)
399
- return if @stack.empty? # Need at least 1 value
413
+ return if @stack.empty? # Need at least 1 value
414
+
400
415
  dy = @stack.pop || 0
401
416
  @y += dy
402
417
  @path << { type: :move_to, x: @x, y: @y }
@@ -688,7 +703,7 @@ module Fontisan
688
703
  dy3 = @stack.shift
689
704
 
690
705
  x1 = @x + dx1
691
- y1 = @y +dy1
706
+ y1 = @y + dy1
692
707
  x2 = x1 + dx2
693
708
  y2 = y1 + dy2
694
709
  @x = x2 + dx3
@@ -785,6 +800,7 @@ module Fontisan
785
800
  # Call local subroutine
786
801
  def callsubr
787
802
  return if @stack.empty?
803
+
788
804
  subr_num = @stack.pop
789
805
  return unless subr_num # Guard against empty stack
790
806
 
@@ -798,6 +814,7 @@ module Fontisan
798
814
  # Call global subroutine
799
815
  def callgsubr
800
816
  return if @stack.empty?
817
+
801
818
  subr_num = @stack.pop
802
819
  return unless subr_num # Guard against empty stack
803
820
 
@@ -838,6 +855,7 @@ module Fontisan
838
855
  # Arithmetic operators
839
856
  def arithmetic_add
840
857
  return if @stack.size < 2
858
+
841
859
  b = @stack.pop
842
860
  a = @stack.pop
843
861
  @stack << (a + b)
@@ -845,6 +863,7 @@ module Fontisan
845
863
 
846
864
  def arithmetic_sub
847
865
  return if @stack.size < 2
866
+
848
867
  b = @stack.pop
849
868
  a = @stack.pop
850
869
  @stack << (a - b)
@@ -852,6 +871,7 @@ module Fontisan
852
871
 
853
872
  def arithmetic_mul
854
873
  return if @stack.size < 2
874
+
855
875
  b = @stack.pop
856
876
  a = @stack.pop
857
877
  @stack << (a * b)
@@ -859,26 +879,32 @@ module Fontisan
859
879
 
860
880
  def arithmetic_div
861
881
  return if @stack.size < 2
882
+
862
883
  b = @stack.pop
863
884
  a = @stack.pop
864
885
  return if b.zero?
886
+
865
887
  @stack << (a / b.to_f)
866
888
  end
867
889
 
868
890
  def arithmetic_neg
869
891
  return if @stack.empty?
892
+
870
893
  @stack << -@stack.pop
871
894
  end
872
895
 
873
896
  def arithmetic_abs
874
897
  return if @stack.empty?
898
+
875
899
  @stack << @stack.pop.abs
876
900
  end
877
901
 
878
902
  def arithmetic_sqrt
879
903
  return if @stack.empty?
904
+
880
905
  val = @stack.pop
881
906
  return if val.negative?
907
+
882
908
  @stack << Math.sqrt(val)
883
909
  end
884
910
 
@@ -130,7 +130,7 @@ module Fontisan
130
130
  type: :operator,
131
131
  name: operator,
132
132
  operands: operand_stack.dup,
133
- hint_data: hint_data
133
+ hint_data: hint_data,
134
134
  }
135
135
 
136
136
  # Clear operand stack
@@ -163,7 +163,7 @@ module Fontisan
163
163
  # @param byte [Integer] Byte value
164
164
  # @return [Boolean] True if operator byte
165
165
  def operator_byte?(byte)
166
- (byte <= 31 && byte != 28) # Operators are 0-31 except 28 (shortint)
166
+ byte <= 31 && byte != 28 # Operators are 0-31 except 28 (shortint)
167
167
  end
168
168
 
169
169
  # Read operator from CharString
@@ -200,8 +200,11 @@ module Fontisan
200
200
  # 3-byte signed integer (16-bit)
201
201
  b1 = io.getbyte
202
202
  b2 = io.getbyte
203
- raise CorruptedTableError, "Unexpected end of CharString reading shortint" if
204
- b1.nil? || b2.nil?
203
+ if b1.nil? || b2.nil?
204
+ raise CorruptedTableError,
205
+ "Unexpected end of CharString reading shortint"
206
+ end
207
+
205
208
  value = (b1 << 8) | b2
206
209
  value > 0x7FFF ? value - 0x10000 : value
207
210
  when 32..246
@@ -210,20 +213,29 @@ module Fontisan
210
213
  when 247..250
211
214
  # Positive 2-byte integer: +108 to +1131
212
215
  b2 = io.getbyte
213
- raise CorruptedTableError, "Unexpected end of CharString reading positive integer" if
214
- b2.nil?
216
+ if b2.nil?
217
+ raise CorruptedTableError,
218
+ "Unexpected end of CharString reading positive integer"
219
+ end
220
+
215
221
  (byte - 247) * 256 + b2 + 108
216
222
  when 251..254
217
223
  # Negative 2-byte integer: -108 to -1131
218
224
  b2 = io.getbyte
219
- raise CorruptedTableError, "Unexpected end of CharString reading negative integer" if
220
- b2.nil?
225
+ if b2.nil?
226
+ raise CorruptedTableError,
227
+ "Unexpected end of CharString reading negative integer"
228
+ end
229
+
221
230
  -(byte - 251) * 256 - b2 - 108
222
231
  when 255
223
232
  # 5-byte signed integer (32-bit) as fixed-point 16.16
224
233
  bytes = io.read(4)
225
- raise CorruptedTableError, "Unexpected end of CharString reading fixed-point" if
226
- bytes.nil? || bytes.length < 4
234
+ if bytes.nil? || bytes.length < 4
235
+ raise CorruptedTableError,
236
+ "Unexpected end of CharString reading fixed-point"
237
+ end
238
+
227
239
  value = bytes.unpack1("l>") # Signed 32-bit big-endian
228
240
  value / 65536.0 # Convert to float
229
241
  else
@@ -234,4 +246,4 @@ module Fontisan
234
246
  end
235
247
  end
236
248
  end
237
- end
249
+ end
@@ -56,7 +56,7 @@ module Fontisan
56
56
  # @yield [operations] Block to modify operations
57
57
  # @yieldparam operations [Array<Hash>] Parsed operations
58
58
  # @yieldreturn [Array<Hash>] Modified operations
59
- def modify_charstring(glyph_index, &block)
59
+ def modify_charstring(glyph_index)
60
60
  # Get original CharString data
61
61
  original_data = @source_index[glyph_index]
62
62
  return unless original_data
@@ -66,7 +66,7 @@ module Fontisan
66
66
  operations = parser.parse
67
67
 
68
68
  # Apply modification
69
- modified_operations = block.call(operations)
69
+ modified_operations = yield(operations)
70
70
 
71
71
  # Build new CharString
72
72
  new_data = CharStringBuilder.build_from_operations(modified_operations)
@@ -86,13 +86,13 @@ module Fontisan
86
86
  charstrings = []
87
87
 
88
88
  (0...@source_index.count).each do |i|
89
- if @modifications.key?(i)
90
- # Use modified CharString
91
- charstrings << @modifications[i]
92
- else
93
- # Use original CharString
94
- charstrings << @source_index[i]
95
- end
89
+ charstrings << if @modifications.key?(i)
90
+ # Use modified CharString
91
+ @modifications[i]
92
+ else
93
+ # Use original CharString
94
+ @source_index[i]
95
+ end
96
96
  end
97
97
 
98
98
  # Build INDEX
@@ -108,10 +108,10 @@ module Fontisan
108
108
  # @yieldparam glyph_index [Integer] Current glyph index
109
109
  # @yieldparam operations [Array<Hash>] Parsed operations
110
110
  # @yieldreturn [Array<Hash>] Modified operations
111
- def batch_modify(glyph_indices, &block)
111
+ def batch_modify(glyph_indices)
112
112
  glyph_indices.each do |glyph_index|
113
113
  modify_charstring(glyph_index) do |operations|
114
- block.call(glyph_index, operations)
114
+ yield(glyph_index, operations)
115
115
  end
116
116
  end
117
117
  end
@@ -124,10 +124,10 @@ module Fontisan
124
124
  # @yieldparam glyph_index [Integer] Current glyph index
125
125
  # @yieldparam operations [Array<Hash>] Parsed operations
126
126
  # @yieldreturn [Array<Hash>] Modified operations
127
- def modify_all(&block)
127
+ def modify_all
128
128
  (0...@source_index.count).each do |i|
129
129
  modify_charstring(i) do |operations|
130
- block.call(i, operations)
130
+ yield(i, operations)
131
131
  end
132
132
  end
133
133
  end
@@ -169,4 +169,4 @@ module Fontisan
169
169
  end
170
170
  end
171
171
  end
172
- end
172
+ end
@@ -93,7 +93,10 @@ module Fontisan
93
93
  dict_hash.each do |operator_name, value|
94
94
  # Get operator bytes
95
95
  operator_bytes = operator_for_name(operator_name)
96
- raise ArgumentError, "Unknown operator: #{operator_name}" unless operator_bytes
96
+ unless operator_bytes
97
+ raise ArgumentError,
98
+ "Unknown operator: #{operator_name}"
99
+ end
97
100
 
98
101
  # Write operands (value can be single value or array)
99
102
  if value.is_a?(Array)
@@ -120,7 +120,7 @@ module Fontisan
120
120
  type: :operator,
121
121
  name: operator,
122
122
  operands: args,
123
- hint_data: nil
123
+ hint_data: nil,
124
124
  }]
125
125
  end
126
126
 
@@ -144,7 +144,7 @@ module Fontisan
144
144
  type: :operator,
145
145
  name: :hintmask,
146
146
  operands: [],
147
- hint_data: hint_data
147
+ hint_data: hint_data,
148
148
  }]
149
149
  end
150
150
 
@@ -168,7 +168,7 @@ module Fontisan
168
168
  type: :operator,
169
169
  name: :cntrmask,
170
170
  operands: [],
171
- hint_data: hint_data
171
+ hint_data: hint_data,
172
172
  }]
173
173
  end
174
174
 
@@ -188,6 +188,7 @@ module Fontisan
188
188
  vvcurveto hhcurveto vhcurveto hvcurveto
189
189
  ]
190
190
 
191
+ # rubocop:disable Style/CombinableLoops
191
192
  # Find first path operator
192
193
  operations.each_with_index do |op, index|
193
194
  return index if path_operators.include?(op[:name])
@@ -197,6 +198,7 @@ module Fontisan
197
198
  operations.each_with_index do |op, index|
198
199
  return index if op[:name] == :endchar
199
200
  end
201
+ # rubocop:enable Style/CombinableLoops
200
202
 
201
203
  # Empty or malformed - inject at start
202
204
  0
@@ -204,4 +206,4 @@ module Fontisan
204
206
  end
205
207
  end
206
208
  end
207
- end
209
+ end
@@ -67,4 +67,4 @@ module Fontisan
67
67
  end
68
68
  end
69
69
  end
70
- end
70
+ end
@@ -99,7 +99,11 @@ module Fontisan
99
99
  # Check array limits
100
100
  if HINT_LIMITS[key]
101
101
  raise ArgumentError, "#{key} invalid" unless value.is_a?(Array)
102
- raise ArgumentError, "#{key} too long" if value.length > HINT_LIMITS[key][:max]
102
+
103
+ if value.length > HINT_LIMITS[key][:max]
104
+ raise ArgumentError,
105
+ "#{key} too long"
106
+ end
103
107
  if HINT_LIMITS[key][:pairs] && value.length.odd?
104
108
  raise ArgumentError, "#{key} must be pairs"
105
109
  end
@@ -114,12 +118,14 @@ module Fontisan
114
118
  when :blue_shift, :blue_fuzz
115
119
  raise ArgumentError, "#{key} invalid" unless value.is_a?(Numeric)
116
120
  when :force_bold
117
- raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
121
+ raise ArgumentError, "#{key} must be 0 or 1" unless [0,
122
+ 1].include?(value)
118
123
  when :language_group
119
- raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
124
+ raise ArgumentError, "#{key} must be 0 or 1" unless [0,
125
+ 1].include?(value)
120
126
  end
121
127
  end
122
128
  end
123
129
  end
124
130
  end
125
- end
131
+ end
@@ -218,4 +218,4 @@ module Fontisan
218
218
  end
219
219
  end
220
220
  end
221
- end
221
+ end
@@ -68,7 +68,8 @@ module Fontisan
68
68
  # @param global_subrs [Cff::Index, nil] Global subroutines INDEX
69
69
  # @param local_subrs [Cff::Index, nil] Local subroutines INDEX
70
70
  # @param vsindex [Integer] Variation store index (default 0)
71
- def initialize(data, num_axes = 0, global_subrs = nil, local_subrs = nil, vsindex = 0)
71
+ def initialize(data, num_axes = 0, global_subrs = nil,
72
+ local_subrs = nil, vsindex = 0)
72
73
  @data = data
73
74
  @num_axes = num_axes
74
75
  @global_subrs = global_subrs
@@ -123,7 +124,8 @@ module Fontisan
123
124
  when :line_to
124
125
  [:line_to, cmd[:x], cmd[:y]]
125
126
  when :curve_to
126
- [:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x], cmd[:y]]
127
+ [:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x],
128
+ cmd[:y]]
127
129
  end
128
130
  end
129
131
  end
@@ -146,7 +148,8 @@ module Fontisan
146
148
  end
147
149
  end
148
150
  rescue StandardError => e
149
- raise CorruptedTableError, "Failed to parse CFF2 CharString: #{e.message}"
151
+ raise CorruptedTableError,
152
+ "Failed to parse CFF2 CharString: #{e.message}"
150
153
  end
151
154
 
152
155
  # Check if byte is an operator
@@ -165,7 +168,10 @@ module Fontisan
165
168
  if first_byte == 12
166
169
  # Two-byte operator
167
170
  second_byte = @io.getbyte
168
- raise CorruptedTableError, "Unexpected end of CharString" if second_byte.nil?
171
+ if second_byte.nil?
172
+ raise CorruptedTableError,
173
+ "Unexpected end of CharString"
174
+ end
169
175
 
170
176
  [12, second_byte]
171
177
  else
@@ -305,7 +311,7 @@ module Fontisan
305
311
  # @param blend_op [Hash] Blend operation data
306
312
  # @param coordinates [Hash<String, Float>] Axis coordinates
307
313
  # @return [Array<Float>] Blended values
308
- def apply_blend(blend_op, coordinates)
314
+ def apply_blend(blend_op, _coordinates)
309
315
  blend_op[:blends].map do |blend|
310
316
  base = blend[:base]
311
317
  deltas = blend[:deltas]
@@ -313,7 +319,7 @@ module Fontisan
313
319
  # Apply deltas based on coordinates
314
320
  # This will be enhanced when we have proper coordinate interpolation
315
321
  blended_value = base
316
- deltas.each_with_index do |delta, axis_index|
322
+ deltas.each_with_index do |delta, _axis_index|
317
323
  # Placeholder: use normalized coordinate (will be replaced with proper interpolation)
318
324
  scalar = 0.0 # Will be calculated by interpolator
319
325
  blended_value += delta * scalar
@@ -573,7 +579,7 @@ module Fontisan
573
579
  def callsubr
574
580
  return if @local_subrs.nil? || @stack.empty?
575
581
 
576
- subr_index = @stack.pop
582
+ @stack.pop
577
583
  # Implement subroutine call (placeholder)
578
584
  @stack.clear
579
585
  end
@@ -581,7 +587,7 @@ module Fontisan
581
587
  def callgsubr
582
588
  return if @global_subrs.nil? || @stack.empty?
583
589
 
584
- subr_index = @stack.pop
590
+ @stack.pop
585
591
  # Implement global subroutine call (placeholder)
586
592
  @stack.clear
587
593
  end
@@ -63,14 +63,14 @@ module Fontisan
63
63
 
64
64
  blends << {
65
65
  base: base,
66
- deltas: deltas
66
+ deltas: deltas,
67
67
  }
68
68
  end
69
69
 
70
70
  {
71
71
  num_values: num_values,
72
72
  num_axes: num_axes,
73
- blends: blends
73
+ blends: blends,
74
74
  }
75
75
  end
76
76
 
@@ -90,7 +90,7 @@ module Fontisan
90
90
  {
91
91
  base: value[0],
92
92
  deltas: value[1..num_axes],
93
- num_axes: num_axes
93
+ num_axes: num_axes,
94
94
  }
95
95
  end
96
96
 
@@ -149,7 +149,8 @@ module Fontisan
149
149
  else
150
150
  # Try as single blend value
151
151
  blend_data = parse_blend_value(key, num_axes: num_axes)
152
- result[key] = blend_data ? apply_blend(blend_data, scalars) : value
152
+ result[key] =
153
+ blend_data ? apply_blend(blend_data, scalars) : value
153
154
  end
154
155
  else
155
156
  # Non-blend value, copy as-is
@@ -186,7 +187,7 @@ module Fontisan
186
187
  # Hint with blend data - normalize and flatten for DICT storage
187
188
  normalized_value = {
188
189
  base: value[:base] || value["base"],
189
- deltas: value[:deltas] || value["deltas"]
190
+ deltas: value[:deltas] || value["deltas"],
190
191
  }
191
192
  result[key] = flatten_blend(normalized_value, num_axes: num_axes)
192
193
  else
@@ -243,4 +244,4 @@ module Fontisan
243
244
  end
244
245
  end
245
246
  end
246
- end
247
+ end
@@ -121,7 +121,7 @@ module Fontisan
121
121
  def active_regions(coordinates)
122
122
  scalars = calculate_scalars(coordinates)
123
123
  scalars.each_with_index.select { |scalar, _| scalar.positive? }
124
- .map(&:last)
124
+ .map(&:last)
125
125
  end
126
126
 
127
127
  # Get scalar for specific region index
@@ -197,4 +197,4 @@ module Fontisan
197
197
  end
198
198
  end
199
199
  end
200
- end
200
+ end
@@ -31,6 +31,9 @@ module Fontisan
31
31
  # builder = CFF2TableBuilder.new(reader, hint_set)
32
32
  # new_cff2 = builder.build
33
33
  class TableBuilder < Tables::Cff::TableBuilder
34
+ # CFF2-specific operators not supported by CFF DictBuilder
35
+ INVALID_CFF_KEYS = [24].freeze # operator 24 = vstore (CFF2 only)
36
+
34
37
  # @return [CFF2TableReader] CFF2 table reader
35
38
  attr_reader :reader
36
39
 
@@ -79,7 +82,7 @@ module Fontisan
79
82
  top_dict: top_dict_hash,
80
83
  charstrings: charstrings_data,
81
84
  private_dict: private_dict_data,
82
- vstore: vstore_data
85
+ vstore: vstore_data,
83
86
  )
84
87
  end
85
88
 
@@ -134,7 +137,8 @@ module Fontisan
134
137
 
135
138
  # Create rebuilder with stem count
136
139
  stem_count = calculate_stem_count
137
- rebuilder = Cff::CharStringRebuilder.new(charstrings_index, stem_count: stem_count)
140
+ rebuilder = Cff::CharStringRebuilder.new(charstrings_index,
141
+ stem_count: stem_count)
138
142
 
139
143
  # Modify each glyph with hints
140
144
  hinted_glyph_ids.each do |glyph_id|
@@ -179,14 +183,14 @@ module Fontisan
179
183
  # Count stems from blue zones (hstem)
180
184
  hstem_count = 0
181
185
  blue_values = font_hints["blue_values"] || font_hints[:blue_values]
182
- if blue_values && blue_values.is_a?(Array)
186
+ if blue_values.is_a?(Array)
183
187
  hstem_count = blue_values.size / 2
184
188
  end
185
189
 
186
190
  # Count stems from stem snap (vstem)
187
191
  vstem_count = 0
188
192
  stem_snap_h = font_hints["stem_snap_h"] || font_hints[:stem_snap_h]
189
- if stem_snap_h && stem_snap_h.is_a?(Array)
193
+ if stem_snap_h.is_a?(Array)
190
194
  vstem_count = stem_snap_h.size
191
195
  end
192
196
 
@@ -365,7 +369,7 @@ module Fontisan
365
369
  # Extract Variable Store bytes unchanged
366
370
  # For simplicity, extract from vstore_offset to end of table
367
371
  # In production, we'd parse structure to get exact size
368
- @reader.data[vstore_offset..-1]
372
+ @reader.data[vstore_offset..]
369
373
  end
370
374
 
371
375
  # Rebuild complete CFF2 table
@@ -376,7 +380,8 @@ module Fontisan
376
380
  # @param private_dict [String, nil] Private DICT
377
381
  # @param vstore [String, nil] Variable Store
378
382
  # @return [String] Complete CFF2 table binary
379
- def rebuild_cff2_table(header:, top_dict:, charstrings:, private_dict:, vstore:)
383
+ def rebuild_cff2_table(header:, top_dict:, charstrings:, private_dict:,
384
+ vstore:)
380
385
  output = StringIO.new("".b)
381
386
 
382
387
  # 1. Write Header
@@ -387,7 +392,7 @@ module Fontisan
387
392
  header_size: header.size,
388
393
  charstrings: charstrings,
389
394
  private_dict: private_dict,
390
- vstore: vstore
395
+ vstore: vstore,
391
396
  )
392
397
 
393
398
  # 3. Build Top DICT with updated offsets
@@ -416,7 +421,8 @@ module Fontisan
416
421
  # @param private_dict [String, nil] Private DICT data
417
422
  # @param vstore [String, nil] Variable Store data
418
423
  # @return [Hash] Section offsets
419
- def calculate_cff2_offsets(header_size:, charstrings:, private_dict:, vstore:)
424
+ def calculate_cff2_offsets(header_size:, charstrings:, private_dict:,
425
+ vstore:)
420
426
  # Start after header
421
427
  offset = header_size
422
428
 
@@ -446,7 +452,7 @@ module Fontisan
446
452
  charstrings: charstrings_offset,
447
453
  private_dict: private_dict_offset,
448
454
  private_dict_size: private_dict_size,
449
- vstore: vstore_offset
455
+ vstore: vstore_offset,
450
456
  }
451
457
  end
452
458
 
@@ -531,23 +537,23 @@ module Fontisan
531
537
  18 => :private,
532
538
  19 => :subrs,
533
539
  20 => :default_width_x,
534
- 21 => :nominal_width_x
535
- # Note: operator 24 (vstore) is CFF2-specific and handled separately
540
+ 21 => :nominal_width_x,
541
+ # Note: operator 24 (vstore) is CFF2-specific and handled separately
536
542
  }
537
543
 
538
544
  result = {}
539
545
  dict.each do |key, value|
540
546
  # Skip vstore (operator 24) - CFF2 specific, not in CFF DictBuilder
541
- next if key == 24 || key == :vstore
547
+ next if INVALID_CFF_KEYS.include?(key)
542
548
 
543
549
  # Convert string keys to symbols for DictBuilder
544
- if key.is_a?(String)
545
- symbol_key = key.to_sym
546
- elsif key.is_a?(Integer)
547
- symbol_key = operator_map[key] || key
548
- else
549
- symbol_key = key
550
- end
550
+ symbol_key = if key.is_a?(String)
551
+ key.to_sym
552
+ elsif key.is_a?(Integer)
553
+ operator_map[key] || key
554
+ else
555
+ key
556
+ end
551
557
 
552
558
  result[symbol_key] = value
553
559
  end
@@ -571,4 +577,4 @@ module Fontisan
571
577
  end
572
578
  end
573
579
  end
574
- end
580
+ end