fontisan 0.2.7 → 0.2.8

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +106 -319
  4. data/Gemfile +1 -1
  5. data/README.adoc +81 -14
  6. data/Rakefile +12 -7
  7. data/benchmark/variation_quick_bench.rb +1 -1
  8. data/lib/fontisan/cli.rb +45 -13
  9. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  10. data/lib/fontisan/commands/convert_command.rb +2 -4
  11. data/lib/fontisan/commands/info_command.rb +3 -3
  12. data/lib/fontisan/commands/pack_command.rb +2 -1
  13. data/lib/fontisan/commands/validate_command.rb +157 -6
  14. data/lib/fontisan/converters/collection_converter.rb +22 -13
  15. data/lib/fontisan/converters/svg_generator.rb +2 -1
  16. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  17. data/lib/fontisan/converters/woff_writer.rb +3 -1
  18. data/lib/fontisan/font_loader.rb +7 -6
  19. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  20. data/lib/fontisan/hints/hint_converter.rb +1 -1
  21. data/lib/fontisan/hints/hint_validator.rb +13 -10
  22. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  23. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  24. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  25. data/lib/fontisan/models/font_report.rb +24 -0
  26. data/lib/fontisan/models/validation_report.rb +7 -2
  27. data/lib/fontisan/open_type_font.rb +2 -3
  28. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  29. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  30. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  31. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  32. data/lib/fontisan/tables/cblc.rb +8 -4
  33. data/lib/fontisan/tables/cff/index.rb +2 -0
  34. data/lib/fontisan/tables/cff.rb +6 -3
  35. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  36. data/lib/fontisan/tables/cff2.rb +1 -1
  37. data/lib/fontisan/tables/cmap.rb +5 -5
  38. data/lib/fontisan/tables/glyf.rb +8 -10
  39. data/lib/fontisan/tables/head.rb +3 -3
  40. data/lib/fontisan/tables/hhea.rb +4 -4
  41. data/lib/fontisan/tables/maxp.rb +2 -2
  42. data/lib/fontisan/tables/name.rb +1 -1
  43. data/lib/fontisan/tables/os2.rb +8 -8
  44. data/lib/fontisan/tables/post.rb +2 -2
  45. data/lib/fontisan/tables/sbix.rb +5 -4
  46. data/lib/fontisan/true_type_font.rb +2 -3
  47. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  48. data/lib/fontisan/validation/collection_validator.rb +4 -2
  49. data/lib/fontisan/validators/basic_validator.rb +11 -21
  50. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  51. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  52. data/lib/fontisan/validators/validator.rb +87 -66
  53. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  54. data/lib/fontisan/version.rb +1 -1
  55. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  56. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  57. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  58. data/lib/fontisan/woff2_font.rb +4 -2
  59. data/lib/fontisan/woff_font.rb +2 -2
  60. data/lib/fontisan.rb +2 -2
  61. data/scripts/compare_stack_aware.rb +1 -1
  62. data/scripts/measure_optimization.rb +1 -2
  63. metadata +4 -2
@@ -109,7 +109,7 @@ module Fontisan
109
109
  woff2_binary = assemble_woff2(header, entries, compressed_data)
110
110
 
111
111
  # Prepare result
112
- result = { woff2_binary: woff2_binary }
112
+ { woff2_binary: woff2_binary }
113
113
 
114
114
  # Optional validation
115
115
  # Temporarily disabled - will be reimplemented with new DSL framework
@@ -117,8 +117,6 @@ module Fontisan
117
117
  # validation_report = validate_encoding(woff2_binary, options)
118
118
  # result[:validation_report] = validation_report
119
119
  # end
120
-
121
- result
122
120
  end
123
121
 
124
122
  # Get list of supported conversions
@@ -185,14 +183,16 @@ module Fontisan
185
183
  end
186
184
 
187
185
  # Read table directory
188
- woff2.table_entries = Woff2Font.read_table_directory_from_io(io, woff2.header)
186
+ woff2.table_entries = Woff2Font.read_table_directory_from_io(io,
187
+ woff2.header)
189
188
 
190
189
  # Decompress tables
191
190
  woff2.decompressed_tables = Woff2Font.decompress_tables(io, woff2.header,
192
- woff2.table_entries)
191
+ woff2.table_entries)
193
192
 
194
193
  # Apply transformations
195
- Woff2Font.apply_transformations!(woff2.table_entries, woff2.decompressed_tables)
194
+ Woff2Font.apply_transformations!(woff2.table_entries,
195
+ woff2.decompressed_tables)
196
196
 
197
197
  woff2
198
198
  end
@@ -369,9 +369,11 @@ module Fontisan
369
369
  # Sort tables by tag for consistent output (same order as directory)
370
370
  sorted_tables = compressed_tables.sort_by { |tag, _| tag }
371
371
 
372
- sorted_tables.each do |_tag, table_info|
372
+ # rubocop:disable Style/HashEachMethods - sorted_tables is an Array, not a Hash
373
+ sorted_tables.each do |_, table_info|
373
374
  io.write(table_info[:compressed_data])
374
375
  end
376
+ # rubocop:enable Style/HashEachMethods
375
377
  end
376
378
 
377
379
  # Write metadata to output
@@ -72,7 +72,8 @@ module Fontisan
72
72
  when "wOF2"
73
73
  Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
74
74
  when Constants::DFONT_RESOURCE_HEADER
75
- extract_and_load_dfont(io, path, font_index, resolved_mode, resolved_lazy)
75
+ extract_and_load_dfont(io, path, font_index, resolved_mode,
76
+ resolved_lazy)
76
77
  else
77
78
  raise InvalidFontError,
78
79
  "Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
@@ -185,7 +186,7 @@ module Fontisan
185
186
  num_fonts = io.read(4).unpack1("N")
186
187
 
187
188
  # Read all font offsets
188
- font_offsets = num_fonts.times.map { io.read(4).unpack1("N") }
189
+ font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
189
190
 
190
191
  # Scan all fonts to determine collection type (not just first)
191
192
  truetype_count = 0
@@ -212,7 +213,7 @@ module Fontisan
212
213
  # Determine collection type based on what fonts are inside
213
214
  # If ANY font is OpenType, use OpenTypeCollection (more general format)
214
215
  # Only use TrueTypeCollection if ALL fonts are TrueType
215
- if opentype_count > 0
216
+ if opentype_count.positive?
216
217
  OpenTypeCollection.from_file(path)
217
218
  else
218
219
  # All fonts are TrueType
@@ -282,7 +283,7 @@ mode: LoadingModes::FULL, lazy: true)
282
283
  end
283
284
 
284
285
  # Read all font offsets
285
- font_offsets = num_fonts.times.map { io.read(4).unpack1("N") }
286
+ font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
286
287
 
287
288
  # Scan all fonts to determine collection type (not just first)
288
289
  truetype_count = 0
@@ -308,7 +309,7 @@ mode: LoadingModes::FULL, lazy: true)
308
309
 
309
310
  # If ANY font is OpenType, use OpenTypeCollection (more general format)
310
311
  # Only use TrueTypeCollection if ALL fonts are TrueType
311
- if opentype_count > 0
312
+ if opentype_count.positive?
312
313
  # OpenType Collection
313
314
  otc = OpenTypeCollection.from_file(path)
314
315
  File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
@@ -328,7 +329,7 @@ mode: LoadingModes::FULL, lazy: true)
328
329
  # @param lazy [Boolean] Lazy loading flag
329
330
  # @return [TrueTypeFont, OpenTypeFont] Loaded font
330
331
  # @api private
331
- def self.extract_and_load_dfont(io, path, font_index, mode, lazy)
332
+ def self.extract_and_load_dfont(io, _path, font_index, mode, lazy)
332
333
  require_relative "parsers/dfont_parser"
333
334
 
334
335
  # Extract SFNT data from resource fork
@@ -410,15 +410,16 @@ module Fontisan
410
410
 
411
411
  info.fonts.each_with_index do |font_info, index|
412
412
  # Show font index with offset
413
- if font_info.collection_offset
414
- lines << "Font #{index} (offset: #{font_info.collection_offset}):"
415
- else
416
- lines << "Font #{index}:"
417
- end
413
+ lines << if font_info.collection_offset
414
+ "Font #{index} (offset: #{font_info.collection_offset}):"
415
+ else
416
+ "Font #{index}:"
417
+ end
418
418
  lines << ""
419
419
 
420
420
  # Format each font using same structure as brief mode
421
- font_type_display = format_font_type_display(font_info.font_format, font_info.is_variable)
421
+ font_type_display = format_font_type_display(font_info.font_format,
422
+ font_info.is_variable)
422
423
  add_line(lines, "Font type", font_type_display)
423
424
  add_line(lines, "Family", font_info.family_name)
424
425
  add_line(lines, "Subfamily", font_info.subfamily_name)
@@ -426,7 +427,8 @@ module Fontisan
426
427
  add_line(lines, "PostScript name", font_info.postscript_name)
427
428
  add_line(lines, "Version", font_info.version)
428
429
  add_line(lines, "Vendor ID", font_info.vendor_id)
429
- add_line(lines, "Font revision", format_float(font_info.font_revision))
430
+ add_line(lines, "Font revision",
431
+ format_float(font_info.font_revision))
430
432
  add_line(lines, "Units per em", font_info.units_per_em)
431
433
 
432
434
  # Blank line between fonts (except after last)
@@ -467,15 +469,16 @@ module Fontisan
467
469
  # Each font's brief info
468
470
  info.fonts.each_with_index do |font_info, index|
469
471
  # Show font index with offset
470
- if font_info.collection_offset
471
- lines << "Font #{index} (offset: #{font_info.collection_offset}):"
472
- else
473
- lines << "Font #{index}:"
474
- end
472
+ lines << if font_info.collection_offset
473
+ "Font #{index} (offset: #{font_info.collection_offset}):"
474
+ else
475
+ "Font #{index}:"
476
+ end
475
477
  lines << ""
476
478
 
477
479
  # Format each font using same structure as individual fonts
478
- font_type_display = format_font_type_display(font_info.font_format, font_info.is_variable)
480
+ font_type_display = format_font_type_display(font_info.font_format,
481
+ font_info.is_variable)
479
482
  add_line(lines, "Font type", font_type_display)
480
483
  add_line(lines, "Family", font_info.family_name)
481
484
  add_line(lines, "Subfamily", font_info.subfamily_name)
@@ -483,7 +486,8 @@ module Fontisan
483
486
  add_line(lines, "PostScript name", font_info.postscript_name)
484
487
  add_line(lines, "Version", font_info.version)
485
488
  add_line(lines, "Vendor ID", font_info.vendor_id)
486
- add_line(lines, "Font revision", format_float(font_info.font_revision))
489
+ add_line(lines, "Font revision",
490
+ format_float(font_info.font_revision))
487
491
  add_line(lines, "Units per em", font_info.units_per_em)
488
492
 
489
493
  # Blank line between fonts (except after last)
@@ -244,7 +244,7 @@ module Fontisan
244
244
  # CVT values typically contain standard widths at the beginning
245
245
  if cvt && !cvt.empty?
246
246
  # First CVT value often represents standard horizontal stem
247
- hints[:std_hw] = cvt[0].abs if cvt.length > 0
247
+ hints[:std_hw] = cvt[0].abs if cvt.length.positive?
248
248
  # Second CVT value often represents standard vertical stem
249
249
  hints[:std_vw] = cvt[1].abs if cvt.length > 1
250
250
  end
@@ -40,7 +40,10 @@ module Fontisan
40
40
  # @param instructions [String] Binary instruction bytes
41
41
  # @return [Hash] Validation result with :valid, :errors, :warnings keys
42
42
  def validate_truetype_instructions(instructions)
43
- return { valid: true, errors: [], warnings: [] } if instructions.nil? || instructions.empty?
43
+ if instructions.nil? || instructions.empty?
44
+ return { valid: true, errors: [],
45
+ warnings: [] }
46
+ end
44
47
 
45
48
  errors = []
46
49
  warnings = []
@@ -114,7 +117,6 @@ module Fontisan
114
117
  if stack_depth != 0
115
118
  warnings << "Stack not neutral: #{stack_depth} value(s) remaining"
116
119
  end
117
-
118
120
  rescue StandardError => e
119
121
  errors << "Exception during validation: #{e.message}"
120
122
  end
@@ -162,14 +164,14 @@ module Fontisan
162
164
  end
163
165
 
164
166
  # Validate stem widths
165
- [:std_hw, :std_vw].each do |key|
167
+ %i[std_hw std_vw].each do |key|
166
168
  if hints[key] && hints[key] <= 0
167
169
  errors << "#{key} must be positive: #{hints[key]}"
168
170
  end
169
171
  end
170
172
 
171
173
  # Validate stem snaps
172
- [:stem_snap_h, :stem_snap_v].each do |key|
174
+ %i[stem_snap_h stem_snap_v].each do |key|
173
175
  if hints[key]
174
176
  if hints[key].length > MAX_STEM_SNAP
175
177
  errors << "#{key} exceeds maximum (#{MAX_STEM_SNAP}): #{hints[key].length}"
@@ -191,10 +193,8 @@ module Fontisan
191
193
  end
192
194
 
193
195
  # Validate language_group
194
- if hints[:language_group]
195
- unless [0, 1].include?(hints[:language_group])
196
- errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
197
- end
196
+ if hints[:language_group] && ![0, 1].include?(hints[:language_group])
197
+ errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
198
198
  end
199
199
 
200
200
  {
@@ -212,7 +212,10 @@ module Fontisan
212
212
  # @param instructions [String] Binary instruction bytes
213
213
  # @return [Hash] Result with :neutral, :stack_depth, :errors keys
214
214
  def validate_stack_neutrality(instructions)
215
- return { neutral: true, stack_depth: 0, errors: [] } if instructions.nil? || instructions.empty?
215
+ if instructions.nil? || instructions.empty?
216
+ return { neutral: true, stack_depth: 0,
217
+ errors: [] }
218
+ end
216
219
 
217
220
  errors = []
218
221
  stack_depth = 0
@@ -257,7 +260,7 @@ module Fontisan
257
260
  end
258
261
 
259
262
  {
260
- neutral: stack_depth == 0,
263
+ neutral: stack_depth.zero?,
261
264
  stack_depth: stack_depth,
262
265
  errors: errors,
263
266
  }
@@ -29,7 +29,7 @@ module Fontisan
29
29
  RCVT = 0x45 # Read CVT
30
30
  WCVTP = 0x44 # Write CVT (in Pixels)
31
31
  WCVTF = 0x70 # Write CVT (in FUnits)
32
- MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
32
+ MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
33
33
  SCVTCI = 0x1D # Set Control Value Table Cut In
34
34
  SSWCI = 0x1E # Set Single Width Cut In
35
35
  SSW = 0x1F # Set Single Width
@@ -106,7 +106,7 @@ module Fontisan
106
106
  # Pattern: value cvt_index WCVTP (stack top to bottom)
107
107
  if stack.length >= 2
108
108
  value = stack.pop
109
- cvt_index = stack.pop
109
+ stack.pop
110
110
  # Track CVT modifications (useful for understanding setup)
111
111
  end
112
112
 
@@ -168,7 +168,7 @@ module Fontisan
168
168
 
169
169
  {
170
170
  fpgm_size: size,
171
- has_functions: size > 0,
171
+ has_functions: size.positive?,
172
172
  complexity: complexity,
173
173
  }
174
174
  rescue StandardError
@@ -208,13 +208,19 @@ module Fontisan
208
208
  xheight_min = (450 * scale_factor).to_i
209
209
  xheight_max = (600 * scale_factor).to_i
210
210
  capheight_min = (650 * scale_factor).to_i
211
- capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
211
+ capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
212
212
 
213
213
  # Group CVT values by typical alignment zones
214
- descender_values = cvt.select { |v| v < descender_max && v > descender_min }
215
- baseline_values = cvt.select { |v| v >= -baseline_range && v <= baseline_range }
216
- xheight_values = cvt.select { |v| v >= xheight_min && v <= xheight_max }
217
- capheight_values = cvt.select { |v| v >= capheight_min && v <= capheight_max }
214
+ descender_values = cvt.select do |v|
215
+ v < descender_max && v > descender_min
216
+ end
217
+ baseline_values = cvt.select do |v|
218
+ v >= -baseline_range && v <= baseline_range
219
+ end
220
+ cvt.select { |v| v >= xheight_min && v <= xheight_max }
221
+ capheight_values = cvt.select do |v|
222
+ v >= capheight_min && v <= capheight_max
223
+ end
218
224
 
219
225
  # Build blue_values (baseline and top zones)
220
226
  blue_values = []
@@ -254,6 +260,7 @@ module Fontisan
254
260
  def estimate_complexity(bytes)
255
261
  return :simple if bytes.length < 50
256
262
  return :moderate if bytes.length < 200
263
+
257
264
  :complex
258
265
  end
259
266
  end
@@ -65,7 +65,7 @@ module Fontisan
65
65
  {
66
66
  fpgm: generate_fpgm(ps_params),
67
67
  prep: generate_prep(ps_params),
68
- cvt: generate_cvt(ps_params)
68
+ cvt: generate_cvt(ps_params),
69
69
  }
70
70
  end
71
71
 
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "font_report"
4
+ require_relative "validation_report"
5
+ require "lutaml/model"
6
+
7
+ module Fontisan
8
+ module Models
9
+ # CollectionValidationReport aggregates validation results for all fonts
10
+ # in a TTC/OTC/dfont collection.
11
+ #
12
+ # Provides collection-level summary statistics and per-font validation
13
+ # details with clear formatting.
14
+ class CollectionValidationReport < Lutaml::Model::Serializable
15
+ attribute :collection_path, :string
16
+ attribute :collection_type, :string
17
+ attribute :num_fonts, :integer
18
+ attribute :font_reports, FontReport, collection: true,
19
+ initialize_empty: true
20
+ attribute :valid, :boolean, default: -> { true }
21
+
22
+ key_value do
23
+ map "collection_path", to: :collection_path
24
+ map "collection_type", to: :collection_type
25
+ map "num_fonts", to: :num_fonts
26
+ map "font_reports", to: :font_reports
27
+ map "valid", to: :valid
28
+ end
29
+
30
+ # Add a font report to the collection
31
+ #
32
+ # @param font_report [FontReport] The font report to add
33
+ # @return [void]
34
+ def add_font_report(font_report)
35
+ font_reports << font_report
36
+ # Mark that we're no longer using the default value
37
+ value_set_for(:font_reports)
38
+ # Update overall validity
39
+ self.valid = valid && font_report.report.valid?
40
+ end
41
+
42
+ # Get overall validation status for the collection
43
+ #
44
+ # @return [String] "valid", "invalid", or "valid_with_warnings"
45
+ def overall_status
46
+ return "invalid" unless font_reports.all? { |fr| fr.report.valid? }
47
+ return "valid_with_warnings" if font_reports.any? do |fr|
48
+ fr.report.has_warnings?
49
+ end
50
+
51
+ "valid"
52
+ end
53
+
54
+ # Generate text summary with collection header and per-font sections
55
+ #
56
+ # @return [String] Formatted validation report
57
+ def text_summary
58
+ lines = []
59
+ lines << "Collection: #{collection_path}"
60
+ lines << "Type: #{collection_type}"
61
+ lines << "Fonts: #{num_fonts}"
62
+ lines << ""
63
+ lines << "Summary:"
64
+ lines << " Total Errors: #{total_errors}"
65
+ lines << " Total Warnings: #{total_warnings}"
66
+ lines << " Total Info: #{total_info}"
67
+
68
+ if font_reports.any?
69
+ lines << ""
70
+ font_reports.each do |font_report|
71
+ lines << "=== Font #{font_report.font_index}: #{font_report.font_name} ==="
72
+ # Indent each line of the font's report
73
+ font_lines = font_report.report.text_summary.split("\n")
74
+ lines.concat(font_lines)
75
+ lines << "" unless font_report == font_reports.last
76
+ end
77
+ end
78
+
79
+ lines.join("\n")
80
+ end
81
+
82
+ # Calculate total errors across all fonts
83
+ #
84
+ # @return [Integer] Total error count
85
+ def total_errors
86
+ font_reports.sum { |fr| fr.report.summary.errors }
87
+ end
88
+
89
+ # Calculate total warnings across all fonts
90
+ #
91
+ # @return [Integer] Total warning count
92
+ def total_warnings
93
+ font_reports.sum { |fr| fr.report.summary.warnings }
94
+ end
95
+
96
+ # Calculate total info messages across all fonts
97
+ #
98
+ # @return [Integer] Total info count
99
+ def total_info
100
+ font_reports.sum { |fr| fr.report.summary.info }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validation_report"
4
+ require "lutaml/model"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # FontReport wraps a single font's validation report with collection context
9
+ #
10
+ # Used within CollectionValidationReport to associate validation results
11
+ # with a specific font index and name in the collection.
12
+ class FontReport < Lutaml::Model::Serializable
13
+ attribute :font_index, :integer
14
+ attribute :font_name, :string
15
+ attribute :report, ValidationReport
16
+
17
+ key_value do
18
+ map "font_index", to: :font_index
19
+ map "font_name", to: :font_name
20
+ map "report", to: :report
21
+ end
22
+ end
23
+ end
24
+ end
@@ -101,7 +101,9 @@ module Fontisan
101
101
  attribute :status, :string
102
102
  attribute :use_case, :string
103
103
  attribute :checks_performed, :string, collection: true, default: -> { [] }
104
- attribute :check_results, CheckResult, collection: true, default: -> { [] }
104
+ attribute :check_results, CheckResult, collection: true, default: -> {
105
+ []
106
+ }
105
107
 
106
108
  yaml do
107
109
  map "font_path", to: :font_path
@@ -340,7 +342,9 @@ module Fontisan
340
342
  # @param field_name [String, Symbol] Field name
341
343
  # @return [Array<CheckResult>] Array of check results for the field
342
344
  def field_issues(table_tag, field_name)
343
- check_results.select { |cr| cr.table == table_tag.to_s && cr.field == field_name.to_s }
345
+ check_results.select do |cr|
346
+ cr.table == table_tag.to_s && cr.field == field_name.to_s
347
+ end
344
348
  end
345
349
 
346
350
  # Check filtering methods
@@ -374,6 +378,7 @@ module Fontisan
374
378
  # @return [Float] Failure rate (0.0 to 1.0)
375
379
  def failure_rate
376
380
  return 0.0 if check_results.empty?
381
+
377
382
  failed_checks.length.to_f / check_results.length
378
383
  end
379
384
 
@@ -581,10 +581,9 @@ module Fontisan
581
581
  # @param path [String] Path to the OTF file
582
582
  # @return [void]
583
583
  def update_checksum_adjustment_in_file(path)
584
- # Use tempfile-based checksum calculation for Windows compatibility
585
- # This keeps the tempfile alive until we're done with the checksum
586
584
  File.open(path, "r+b") do |io|
587
- checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
585
+ # Calculate checksum directly from IO to avoid Windows Tempfile issues
586
+ checksum = Utilities::ChecksumCalculator.calculate_checksum_from_io(io)
588
587
 
589
588
  # Calculate adjustment
590
589
  adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
@@ -151,7 +151,7 @@ module Fontisan
151
151
  # @param charstring [String] CharString to search
152
152
  # @param pattern [Pattern] pattern to find
153
153
  # @return [Array<Integer>] array of start positions
154
- def find_pattern_positions(charstring, pattern, glyph_id = nil)
154
+ def find_pattern_positions(charstring, pattern, _glyph_id = nil)
155
155
  positions = []
156
156
  offset = 0
157
157
 
@@ -32,7 +32,9 @@ module Fontisan
32
32
  selected = []
33
33
  # Sort by savings (descending), then by length (descending), then by min glyph ID,
34
34
  # then by byte values for complete determinism across platforms
35
- remaining = @patterns.sort_by { |p| [-p.savings, -p.length, p.glyphs.min, p.bytes.bytes] }
35
+ remaining = @patterns.sort_by do |p|
36
+ [-p.savings, -p.length, p.glyphs.min, p.bytes.bytes]
37
+ end
36
38
 
37
39
  remaining.each do |pattern|
38
40
  break if selected.length >= @max_subrs
@@ -53,7 +55,9 @@ module Fontisan
53
55
  def optimize_ordering(subroutines)
54
56
  # Higher frequency = lower ID (shorter encoding)
55
57
  # Use same comprehensive sort keys as optimize_selection for consistency
56
- subroutines.sort_by { |subr| [-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes] }
58
+ subroutines.sort_by do |subr|
59
+ [-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes]
60
+ end
57
61
  end
58
62
 
59
63
  # Check if nesting would be beneficial
@@ -30,6 +30,8 @@ module Fontisan
30
30
  # mapping = Fontisan::Subset::GlyphMapping.new([0, 5, 10])
31
31
  # mapping.old_id(1) # => 5
32
32
  class GlyphMapping
33
+ include Enumerable
34
+
33
35
  # @return [Hash<Integer, Integer>] mapping from old GIDs to new GIDs
34
36
  attr_reader :old_to_new
35
37
 
@@ -152,7 +152,7 @@ module Fontisan
152
152
  # Build new hmtx data
153
153
  data = String.new(encoding: Encoding::BINARY)
154
154
 
155
- mapping.each do |old_id, _new_id|
155
+ mapping.old_ids.each do |old_id|
156
156
  metric = table.metric_for(old_id)
157
157
  next unless metric
158
158
 
@@ -319,7 +319,7 @@ module Fontisan
319
319
  current_offset = 0
320
320
 
321
321
  # Process glyphs in mapping order
322
- mapping.each do |old_id, _new_id|
322
+ mapping.old_ids.each do |old_id|
323
323
  @loca_offsets << current_offset
324
324
 
325
325
  # Get offset and size from original loca
@@ -86,9 +86,12 @@ module Fontisan
86
86
  size = new
87
87
 
88
88
  io = StringIO.new(data)
89
- size.instance_variable_set(:@index_subtable_array_offset, io.read(4).unpack1("N"))
90
- size.instance_variable_set(:@index_tables_size, io.read(4).unpack1("N"))
91
- size.instance_variable_set(:@number_of_index_subtables, io.read(4).unpack1("N"))
89
+ size.instance_variable_set(:@index_subtable_array_offset,
90
+ io.read(4).unpack1("N"))
91
+ size.instance_variable_set(:@index_tables_size,
92
+ io.read(4).unpack1("N"))
93
+ size.instance_variable_set(:@number_of_index_subtables,
94
+ io.read(4).unpack1("N"))
92
95
  size.instance_variable_set(:@color_ref, io.read(4).unpack1("N"))
93
96
 
94
97
  # Parse hori and vert metrics (12 bytes each)
@@ -98,7 +101,8 @@ module Fontisan
98
101
  size.instance_variable_set(:@vert, SbitLineMetrics.read(vert_data))
99
102
 
100
103
  # Parse remaining fields
101
- size.instance_variable_set(:@start_glyph_index, io.read(2).unpack1("n"))
104
+ size.instance_variable_set(:@start_glyph_index,
105
+ io.read(2).unpack1("n"))
102
106
  size.instance_variable_set(:@end_glyph_index, io.read(2).unpack1("n"))
103
107
  size.instance_variable_set(:@ppem_x, io.read(1).unpack1("C"))
104
108
  size.instance_variable_set(:@ppem_y, io.read(1).unpack1("C"))
@@ -35,6 +35,8 @@ module Fontisan
35
35
  # puts index[0] # => first item data
36
36
  # index.each { |item| puts item }
37
37
  class Index
38
+ include Enumerable
39
+
38
40
  # @return [Integer] Number of items in the INDEX
39
41
  attr_reader :count
40
42
 
@@ -299,7 +299,8 @@ module Fontisan
299
299
  io.seek(absolute_offset)
300
300
  Index.new(io, start_offset: absolute_offset)
301
301
  rescue StandardError => e
302
- raise CorruptedTableError, "Failed to parse Local Subr INDEX: #{e.message}"
302
+ raise CorruptedTableError,
303
+ "Failed to parse Local Subr INDEX: #{e.message}"
303
304
  end
304
305
 
305
306
  # Get the CharStrings INDEX for a specific font
@@ -320,7 +321,8 @@ module Fontisan
320
321
  io.seek(charstrings_offset)
321
322
  CharstringsIndex.new(io, start_offset: charstrings_offset)
322
323
  rescue StandardError => e
323
- raise CorruptedTableError, "Failed to parse CharStrings INDEX: #{e.message}"
324
+ raise CorruptedTableError,
325
+ "Failed to parse CharStrings INDEX: #{e.message}"
324
326
  end
325
327
 
326
328
  # Get a CharString for a specific glyph
@@ -355,7 +357,8 @@ module Fontisan
355
357
  local_subr_index,
356
358
  )
357
359
  rescue StandardError => e
358
- raise CorruptedTableError, "Failed to get CharString for glyph #{glyph_index}: #{e.message}"
360
+ raise CorruptedTableError,
361
+ "Failed to get CharString for glyph #{glyph_index}: #{e.message}"
359
362
  end
360
363
 
361
364
  # Get the number of glyphs in a font
@@ -51,7 +51,7 @@ module Fontisan
51
51
  # Check if this is blend data
52
52
  # Format: base1 delta1_1 ... delta1_N base2 delta2_1 ... delta2_N ...
53
53
  # The array must be divisible by (num_axes + 1)
54
- return nil unless value.size % (num_axes + 1) == 0
54
+ return nil unless (value.size % (num_axes + 1)).zero?
55
55
 
56
56
  num_values = value.size / (num_axes + 1)
57
57
  blends = []
@@ -121,7 +121,7 @@ module Fontisan
121
121
  def charstrings
122
122
  return [] unless @charstrings_index
123
123
 
124
- @charstrings_index.count.times.map do |glyph_id|
124
+ Array.new(@charstrings_index.count) do |glyph_id|
125
125
  charstring_for_glyph(glyph_id)
126
126
  end.compact
127
127
  end