fontisan 0.2.1 → 0.2.2

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +57 -385
  3. data/README.adoc +1483 -1435
  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/cli.rb +10 -3
  9. data/lib/fontisan/collection/builder.rb +2 -1
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/commands/base_command.rb +5 -2
  12. data/lib/fontisan/commands/convert_command.rb +6 -2
  13. data/lib/fontisan/commands/info_command.rb +111 -5
  14. data/lib/fontisan/commands/instance_command.rb +8 -7
  15. data/lib/fontisan/commands/validate_command.rb +4 -1
  16. data/lib/fontisan/constants.rb +24 -24
  17. data/lib/fontisan/converters/format_converter.rb +8 -4
  18. data/lib/fontisan/converters/outline_converter.rb +21 -16
  19. data/lib/fontisan/converters/woff_writer.rb +8 -3
  20. data/lib/fontisan/font_loader.rb +11 -4
  21. data/lib/fontisan/font_writer.rb +2 -0
  22. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  23. data/lib/fontisan/hints/hint_converter.rb +43 -47
  24. data/lib/fontisan/hints/hint_validator.rb +284 -0
  25. data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
  26. data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
  27. data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
  28. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  29. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  30. data/lib/fontisan/loading_modes.rb +4 -4
  31. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  32. data/lib/fontisan/models/font_export.rb +2 -2
  33. data/lib/fontisan/models/font_info.rb +3 -30
  34. data/lib/fontisan/models/hint.rb +22 -23
  35. data/lib/fontisan/models/outline.rb +4 -1
  36. data/lib/fontisan/models/validation_report.rb +1 -1
  37. data/lib/fontisan/open_type_font.rb +3 -1
  38. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  39. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  40. data/lib/fontisan/pipeline/output_writer.rb +8 -3
  41. data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
  42. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  43. data/lib/fontisan/tables/cff/charstring.rb +38 -12
  44. data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
  45. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
  46. data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
  47. data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
  48. data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
  49. data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
  50. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  51. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  52. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
  53. data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
  54. data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
  55. data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
  56. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
  57. data/lib/fontisan/tables/cff2.rb +1 -1
  58. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  59. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  60. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  61. data/lib/fontisan/tables/name.rb +4 -4
  62. data/lib/fontisan/true_type_font.rb +3 -1
  63. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  64. data/lib/fontisan/variation/cache.rb +3 -1
  65. data/lib/fontisan/variation/converter.rb +2 -1
  66. data/lib/fontisan/variation/delta_applier.rb +2 -1
  67. data/lib/fontisan/variation/inspector.rb +2 -1
  68. data/lib/fontisan/variation/instance_generator.rb +2 -1
  69. data/lib/fontisan/variation/optimizer.rb +6 -3
  70. data/lib/fontisan/variation/subsetter.rb +32 -10
  71. data/lib/fontisan/variation/variation_preserver.rb +4 -1
  72. data/lib/fontisan/version.rb +1 -1
  73. data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
  74. data/lib/fontisan/woff2_font.rb +31 -15
  75. data/lib/fontisan.rb +42 -2
  76. data/scripts/measure_optimization.rb +15 -7
  77. metadata +8 -2
@@ -36,37 +36,9 @@ module Fontisan
36
36
  attribute :font_revision, :float
37
37
  attribute :permissions, :string
38
38
  attribute :units_per_em, :integer
39
+ attribute :collection_offset, :integer
39
40
 
40
- json do
41
- map "font_format", to: :font_format
42
- map "is_variable", to: :is_variable
43
- map "family_name", to: :family_name
44
- map "subfamily_name", to: :subfamily_name
45
- map "full_name", to: :full_name
46
- map "postscript_name", to: :postscript_name
47
- map "postscript_cid_name", to: :postscript_cid_name
48
- map "preferred_family", to: :preferred_family
49
- map "preferred_subfamily", to: :preferred_subfamily
50
- map "mac_font_menu_name", to: :mac_font_menu_name
51
- map "version", to: :version
52
- map "unique_id", to: :unique_id
53
- map "description", to: :description
54
- map "designer", to: :designer
55
- map "designer_url", to: :designer_url
56
- map "manufacturer", to: :manufacturer
57
- map "vendor_url", to: :vendor_url
58
- map "vendor_id", to: :vendor_id
59
- map "trademark", to: :trademark
60
- map "copyright", to: :copyright
61
- map "license_description", to: :license_description
62
- map "license_url", to: :license_url
63
- map "sample_text", to: :sample_text
64
- map "font_revision", to: :font_revision
65
- map "permissions", to: :permissions
66
- map "units_per_em", to: :units_per_em
67
- end
68
-
69
- yaml do
41
+ key_value do
70
42
  map "font_format", to: :font_format
71
43
  map "is_variable", to: :is_variable
72
44
  map "family_name", to: :family_name
@@ -93,6 +65,7 @@ module Fontisan
93
65
  map "font_revision", to: :font_revision
94
66
  map "permissions", to: :permissions
95
67
  map "units_per_em", to: :units_per_em
68
+ map "collection_offset", to: :collection_offset
96
69
  end
97
70
  end
98
71
  end
@@ -76,7 +76,7 @@ module Fontisan
76
76
  {
77
77
  type: h.type,
78
78
  data: h.data,
79
- source_format: h.source_format
79
+ source_format: h.source_format,
80
80
  }
81
81
  end
82
82
  glyph_hints_hash[glyph_id.to_s] = hints_data
@@ -124,6 +124,7 @@ module Fontisan
124
124
  # Parse glyph hints JSON
125
125
  def parse_glyph_hints
126
126
  return {} if @glyph_hints.nil? || @glyph_hints.empty? || @glyph_hints == "{}"
127
+
127
128
  JSON.parse(@glyph_hints)
128
129
  rescue JSON::ParserError
129
130
  {}
@@ -202,7 +203,7 @@ module Fontisan
202
203
  when :interpolate
203
204
  # IUP instruction
204
205
  axis = data[:axis] || :y
205
- axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
206
+ axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
206
207
  when :shift
207
208
  # SHP instruction
208
209
  data[:instructions] || []
@@ -268,23 +269,22 @@ module Fontisan
268
269
 
269
270
  # Convert stem hint to TrueType instructions
270
271
  def convert_stem_to_truetype
271
- position = data[:position] || 0
272
- width = data[:width] || 0
272
+ data[:position] || 0
273
+ data[:width] || 0
273
274
  orientation = data[:orientation] || :vertical
274
275
 
275
276
  # TrueType uses MDAP (Move Direct Absolute Point) and MDRP (Move Direct Relative Point)
276
277
  # to control stem positioning
277
278
  instructions = []
278
279
 
279
- if orientation == :vertical
280
- # Vertical stem: use Y-axis instructions
281
- instructions << 0x2E # MDAP[rnd] - mark reference point
282
- instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
283
- else
284
- # Horizontal stem: use X-axis instructions
285
- instructions << 0x2F # MDAP[rnd]
286
- instructions << 0xC0 # MDRP[min,rnd,black]
287
- end
280
+ instructions << if orientation == :vertical
281
+ # Vertical stem: use Y-axis instructions
282
+ 0x2E # MDAP[rnd] - mark reference point
283
+ else
284
+ # Horizontal stem: use X-axis instructions
285
+ 0x2F # MDAP[rnd]
286
+ end
287
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
288
288
 
289
289
  instructions
290
290
  end
@@ -313,16 +313,15 @@ module Fontisan
313
313
  # Generate MDAP/MDRP pairs for each stem
314
314
  instructions = []
315
315
 
316
- stems.each do |stem|
317
- if orientation == :vertical
318
- # Vertical stem: use Y-axis instructions
319
- instructions << 0x2E # MDAP[rnd] - mark reference point
320
- instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
321
- else
322
- # Horizontal stem: use X-axis instructions
323
- instructions << 0x2F # MDAP[rnd]
324
- instructions << 0xC0 # MDRP[min,rnd,black]
325
- end
316
+ stems.each do |_stem|
317
+ instructions << if orientation == :vertical
318
+ # Vertical stem: use Y-axis instructions
319
+ 0x2E # MDAP[rnd] - mark reference point
320
+ else
321
+ # Horizontal stem: use X-axis instructions
322
+ 0x2F # MDAP[rnd]
323
+ end
324
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
326
325
  end
327
326
 
328
327
  instructions
@@ -121,7 +121,10 @@ module Fontisan
121
121
 
122
122
  # Get path from CharString
123
123
  path = charstring.path
124
- raise ArgumentError, "CharString has no path data" if path.nil? || path.empty?
124
+ if path.nil? || path.empty?
125
+ raise ArgumentError,
126
+ "CharString has no path data"
127
+ end
125
128
 
126
129
  commands = convert_cff_path_to_commands(path)
127
130
 
@@ -29,7 +29,7 @@ module Fontisan
29
29
  attribute :severity, :string
30
30
  attribute :category, :string
31
31
  attribute :message, :string
32
- attribute :location, :string, default: -> { nil }
32
+ attribute :location, :string, default: -> {}
33
33
 
34
34
  yaml do
35
35
  map "severity", to: :severity
@@ -211,7 +211,8 @@ module Fontisan
211
211
  batch_entries.each do |entry|
212
212
  relative_offset = entry.offset - batch_offset
213
213
  tag_key = entry.tag.dup.force_encoding("UTF-8")
214
- @table_data[tag_key] = batch_data[relative_offset, entry.table_length]
214
+ @table_data[tag_key] =
215
+ batch_data[relative_offset, entry.table_length]
215
216
  end
216
217
  end
217
218
 
@@ -286,6 +287,7 @@ module Fontisan
286
287
  # @return [Boolean] true if table is available in current mode
287
288
  def table_available?(tag)
288
289
  return false unless has_table?(tag)
290
+
289
291
  LoadingModes.table_allowed?(@loading_mode, tag)
290
292
  end
291
293
 
@@ -222,7 +222,8 @@ module Fontisan
222
222
  if @stack_aware
223
223
  tracker = @stack_trackers[glyph_id]
224
224
  next unless tracker
225
- next unless tracker.stack_neutral?(start_pos, start_pos + length)
225
+ next unless tracker.stack_neutral?(start_pos,
226
+ start_pos + length)
226
227
  end
227
228
 
228
229
  pattern_bytes = charstring[start_pos, length]
@@ -74,7 +74,7 @@ module Fontisan
74
74
  # 2. Analyze patterns
75
75
  analyzer = PatternAnalyzer.new(
76
76
  min_length: @min_pattern_length,
77
- stack_aware: true
77
+ stack_aware: true,
78
78
  )
79
79
  patterns = analyzer.analyze(charstrings)
80
80
 
@@ -69,7 +69,10 @@ module Fontisan
69
69
  # @return [Integer] Number of bytes written
70
70
  def write_svg(result)
71
71
  svg_xml = result[:svg_xml] || result["svg_xml"]
72
- raise ArgumentError, "SVG result must contain :svg_xml key" unless svg_xml
72
+ unless svg_xml
73
+ raise ArgumentError,
74
+ "SVG result must contain :svg_xml key"
75
+ end
73
76
 
74
77
  File.write(@output_path, svg_xml)
75
78
  end
@@ -80,7 +83,8 @@ module Fontisan
80
83
  # @return [Integer] Number of bytes written
81
84
  def write_sfnt(tables)
82
85
  sfnt_version = determine_sfnt_version
83
- FontWriter.write_to_file(tables, @output_path, sfnt_version: sfnt_version)
86
+ FontWriter.write_to_file(tables, @output_path,
87
+ sfnt_version: sfnt_version)
84
88
  end
85
89
 
86
90
  # Write WOFF format
@@ -145,7 +149,8 @@ module Fontisan
145
149
  when :otf
146
150
  OpenTypeFont.from_tables(tables)
147
151
  else
148
- raise ArgumentError, "Cannot determine font type for format: #{@format}"
152
+ raise ArgumentError,
153
+ "Cannot determine font type for format: #{@format}"
149
154
  end
150
155
  end
151
156
  end
@@ -294,7 +294,8 @@ module Fontisan
294
294
  when ".woff" then :woff
295
295
  when ".woff2" then :woff2
296
296
  else
297
- raise ArgumentError, "Cannot determine target format from extension: #{ext}"
297
+ raise ArgumentError,
298
+ "Cannot determine target format from extension: #{ext}"
298
299
  end
299
300
  end
300
301
 
@@ -304,7 +305,10 @@ module Fontisan
304
305
  def variation_options
305
306
  opts = {}
306
307
  opts[:coordinates] = @options[:coordinates] if @options[:coordinates]
307
- opts[:instance_index] = @options[:instance_index] if @options[:instance_index]
308
+ if @options[:instance_index]
309
+ opts[:instance_index] =
310
+ @options[:instance_index]
311
+ end
308
312
  opts
309
313
  end
310
314
 
@@ -344,7 +348,8 @@ module Fontisan
344
348
  when :otf
345
349
  OpenTypeFont.from_tables(tables)
346
350
  else
347
- raise ArgumentError, "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
351
+ raise ArgumentError,
352
+ "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
348
353
  end
349
354
  end
350
355
  end
@@ -122,11 +122,11 @@ module Fontisan
122
122
  data = table.to_binary_s.dup
123
123
 
124
124
  # Calculate new numberOfHMetrics
125
- new_num_h_metrics = if hmtx && hmtx.h_metrics
126
- hmtx.h_metrics.size
127
- else
128
- calculate_number_of_h_metrics
129
- end
125
+ new_num_h_metrics = if hmtx&.h_metrics
126
+ hmtx.h_metrics.size
127
+ else
128
+ calculate_number_of_h_metrics
129
+ end
130
130
 
131
131
  # Update numberOfHMetrics field (at offset 34, uint16)
132
132
  data[34, 2] = [new_num_h_metrics].pack("n")
@@ -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