fontisan 0.2.6 → 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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +107 -318
  4. data/Gemfile +1 -1
  5. data/README.adoc +127 -17
  6. data/Rakefile +12 -7
  7. data/benchmark/variation_quick_bench.rb +1 -1
  8. data/lib/fontisan/base_collection.rb +5 -33
  9. data/lib/fontisan/cli.rb +45 -13
  10. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  11. data/lib/fontisan/collection/shared_logic.rb +54 -0
  12. data/lib/fontisan/commands/convert_command.rb +2 -4
  13. data/lib/fontisan/commands/info_command.rb +3 -3
  14. data/lib/fontisan/commands/pack_command.rb +2 -1
  15. data/lib/fontisan/commands/validate_command.rb +157 -6
  16. data/lib/fontisan/converters/collection_converter.rb +22 -13
  17. data/lib/fontisan/converters/svg_generator.rb +2 -1
  18. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  19. data/lib/fontisan/converters/woff_writer.rb +3 -1
  20. data/lib/fontisan/dfont_collection.rb +84 -0
  21. data/lib/fontisan/font_loader.rb +9 -9
  22. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  23. data/lib/fontisan/hints/hint_converter.rb +1 -1
  24. data/lib/fontisan/hints/hint_validator.rb +13 -10
  25. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  26. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  27. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  28. data/lib/fontisan/models/font_report.rb +24 -0
  29. data/lib/fontisan/models/validation_report.rb +7 -2
  30. data/lib/fontisan/open_type_font.rb +2 -3
  31. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  32. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  33. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  34. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  35. data/lib/fontisan/tables/cblc.rb +8 -4
  36. data/lib/fontisan/tables/cff/index.rb +2 -0
  37. data/lib/fontisan/tables/cff.rb +6 -3
  38. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  39. data/lib/fontisan/tables/cff2.rb +1 -1
  40. data/lib/fontisan/tables/cmap.rb +5 -5
  41. data/lib/fontisan/tables/glyf.rb +8 -10
  42. data/lib/fontisan/tables/head.rb +3 -3
  43. data/lib/fontisan/tables/hhea.rb +4 -4
  44. data/lib/fontisan/tables/maxp.rb +2 -2
  45. data/lib/fontisan/tables/name.rb +1 -1
  46. data/lib/fontisan/tables/os2.rb +8 -8
  47. data/lib/fontisan/tables/post.rb +2 -2
  48. data/lib/fontisan/tables/sbix.rb +5 -4
  49. data/lib/fontisan/true_type_font.rb +2 -3
  50. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  51. data/lib/fontisan/validation/collection_validator.rb +4 -2
  52. data/lib/fontisan/validators/basic_validator.rb +11 -21
  53. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  54. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  55. data/lib/fontisan/validators/validator.rb +87 -66
  56. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  57. data/lib/fontisan/version.rb +1 -1
  58. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  59. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  60. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  61. data/lib/fontisan/woff2_font.rb +4 -2
  62. data/lib/fontisan/woff_font.rb +2 -2
  63. data/lib/fontisan.rb +2 -2
  64. data/scripts/compare_stack_aware.rb +1 -1
  65. data/scripts/measure_optimization.rb +1 -2
  66. metadata +5 -2
@@ -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
@@ -278,22 +278,20 @@ module Fontisan
278
278
  end
279
279
  end
280
280
 
281
- public
282
-
283
281
  # Validation helper: Check if version is valid
284
282
  #
285
283
  # cmap version should be 0
286
284
  #
287
285
  # @return [Boolean] True if version is 0
288
286
  def valid_version?
289
- version == 0
287
+ version.zero?
290
288
  end
291
289
 
292
290
  # Validation helper: Check if at least one subtable exists
293
291
  #
294
292
  # @return [Boolean] True if num_tables > 0
295
293
  def has_subtables?
296
- num_tables && num_tables > 0
294
+ num_tables&.positive?
297
295
  end
298
296
 
299
297
  # Validation helper: Check if Unicode mapping exists
@@ -357,7 +355,9 @@ module Fontisan
357
355
  mappings = unicode_mappings
358
356
  return true if mappings.nil? || mappings.empty?
359
357
 
360
- mappings.values.all? { |glyph_id| glyph_id >= 0 && glyph_id < max_glyph_id }
358
+ mappings.values.all? do |glyph_id|
359
+ glyph_id >= 0 && glyph_id < max_glyph_id
360
+ end
361
361
  end
362
362
  end
363
363
  end
@@ -166,7 +166,7 @@ module Fontisan
166
166
  # @param head [Head] Head table for context
167
167
  # @param num_glyphs [Integer] Total number of glyphs to check
168
168
  # @return [Boolean] True if all non-special glyphs have contours
169
- def no_empty_glyphs_except_special?(loca, head, num_glyphs)
169
+ def no_empty_glyphs_except_special?(loca, _head, num_glyphs)
170
170
  # Check glyphs 1 through num_glyphs-1 (.notdef at 0 can be empty)
171
171
  (1...num_glyphs).all? do |glyph_id|
172
172
  size = loca.size_of(glyph_id)
@@ -194,7 +194,7 @@ module Fontisan
194
194
 
195
195
  (0...num_glyphs).all? do |glyph_id|
196
196
  glyph = glyph_for(glyph_id, loca, head)
197
- next true if glyph.nil? # Empty glyphs are OK
197
+ next true if glyph.nil? # Empty glyphs are OK
198
198
 
199
199
  # Check if glyph bounds are within font bounds
200
200
  glyph.x_min >= font_x_min &&
@@ -218,7 +218,7 @@ module Fontisan
218
218
  def instructions_sound?(loca, head, num_glyphs)
219
219
  (0...num_glyphs).all? do |glyph_id|
220
220
  glyph = glyph_for(glyph_id, loca, head)
221
- next true if glyph.nil? # Empty glyphs are OK
221
+ next true if glyph.nil? # Empty glyphs are OK
222
222
 
223
223
  # Simple glyphs have instructions
224
224
  if glyph.respond_to?(:instruction_length)
@@ -242,7 +242,7 @@ module Fontisan
242
242
  # @return [Boolean] True if contour count is valid
243
243
  def valid_contour_count?(glyph_id, loca, head)
244
244
  glyph = glyph_for(glyph_id, loca, head)
245
- return true if glyph.nil? # Empty glyphs are OK
245
+ return true if glyph.nil? # Empty glyphs are OK
246
246
 
247
247
  # Simple glyphs: contours should be >= 0
248
248
  # Compound glyphs: numberOfContours = -1
@@ -265,12 +265,10 @@ module Fontisan
265
265
  # @return [Boolean] True if all glyphs can be accessed
266
266
  def all_glyphs_accessible?(loca, head, num_glyphs)
267
267
  (0...num_glyphs).all? do |glyph_id|
268
- begin
269
- glyph_for(glyph_id, loca, head)
270
- true
271
- rescue Fontisan::CorruptedTableError
272
- false
273
- end
268
+ glyph_for(glyph_id, loca, head)
269
+ true
270
+ rescue Fontisan::CorruptedTableError
271
+ false
274
272
  end
275
273
  rescue StandardError
276
274
  false
@@ -95,7 +95,7 @@ module Fontisan
95
95
  #
96
96
  # @return [Boolean] True if version is 1.0
97
97
  def valid_version?
98
- version_raw == 0x00010000 # Version 1.0
98
+ version_raw == 0x00010000 # Version 1.0
99
99
  end
100
100
 
101
101
  # Validation helper: Check if units per em is valid
@@ -130,7 +130,7 @@ module Fontisan
130
130
  #
131
131
  # @return [Boolean] True if format is 0 or 1
132
132
  def valid_index_to_loc_format?
133
- index_to_loc_format == 0 || index_to_loc_format == 1
133
+ [0, 1].include?(index_to_loc_format)
134
134
  end
135
135
 
136
136
  # Validation helper: Check if glyph_data_format is valid
@@ -139,7 +139,7 @@ module Fontisan
139
139
  #
140
140
  # @return [Boolean] True if format is 0
141
141
  def valid_glyph_data_format?
142
- glyph_data_format == 0
142
+ glyph_data_format.zero?
143
143
  end
144
144
 
145
145
  # Validate magic number and raise error if invalid
@@ -112,7 +112,7 @@ module Fontisan
112
112
  #
113
113
  # @return [Boolean] True if format is 0
114
114
  def valid_metric_data_format?
115
- metric_data_format == 0
115
+ metric_data_format.zero?
116
116
  end
117
117
 
118
118
  # Validation helper: Check if number of h metrics is valid
@@ -130,7 +130,7 @@ module Fontisan
130
130
  #
131
131
  # @return [Boolean] True if ascent/descent have correct signs
132
132
  def valid_ascent_descent?
133
- ascent > 0 && descent < 0
133
+ ascent.positive? && descent.negative?
134
134
  end
135
135
 
136
136
  # Validation helper: Check if line gap is non-negative
@@ -148,7 +148,7 @@ module Fontisan
148
148
  #
149
149
  # @return [Boolean] True if advance_width_max > 0
150
150
  def valid_advance_width_max?
151
- advance_width_max && advance_width_max > 0
151
+ advance_width_max&.positive?
152
152
  end
153
153
 
154
154
  # Validation helper: Check if caret slope is valid
@@ -168,7 +168,7 @@ module Fontisan
168
168
  #
169
169
  # @return [Boolean] True if x_max_extent > 0
170
170
  def valid_x_max_extent?
171
- x_max_extent > 0
171
+ x_max_extent.positive?
172
172
  end
173
173
 
174
174
  # Validate the table and raise error if invalid
@@ -179,9 +179,9 @@ module Fontisan
179
179
  #
180
180
  # @return [Boolean] True if maxZones is valid or not applicable
181
181
  def valid_max_zones?
182
- return true if version_0_5? # Not applicable for CFF
182
+ return true if version_0_5? # Not applicable for CFF
183
183
 
184
- max_zones && max_zones.between?(1, 2)
184
+ max_zones&.between?(1, 2)
185
185
  end
186
186
 
187
187
  # Validation helper: Check if all TrueType metrics are present
@@ -203,7 +203,7 @@ module Fontisan
203
203
  #
204
204
  # @return [Boolean] True if version is 0 or 1
205
205
  def valid_version?
206
- format == 0 || format == 1
206
+ [0, 1].include?(format)
207
207
  end
208
208
 
209
209
  # Validation helper: Check if encoding combinations are valid