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
@@ -3,6 +3,7 @@
3
3
  require_relative "base_command"
4
4
  require_relative "../validators/profile_loader"
5
5
  require_relative "../font_loader"
6
+ require_relative "../tables/name"
6
7
 
7
8
  module Fontisan
8
9
  module Commands
@@ -70,12 +71,40 @@ module Fontisan
70
71
  # Load font with appropriate mode
71
72
  profile_config = Validators::ProfileLoader.profile_info(@profile)
72
73
  unless profile_config
73
- puts "Error: Unknown profile '#{@profile}'" unless @suppress_warnings
74
+ unless @suppress_warnings
75
+ puts "Error: Unknown profile '#{@profile}'"
76
+ puts ""
77
+ puts "Available profiles:"
78
+ Validators::ProfileLoader.all_profiles.each do |name, config|
79
+ puts " #{name.to_s.ljust(20)} - #{config[:description]}"
80
+ end
81
+ puts ""
82
+ puts "Use --list to see all available profiles"
83
+ end
74
84
  return 1
75
85
  end
76
86
 
77
87
  mode = profile_config[:loading_mode].to_sym
78
88
 
89
+ # Check if input is a collection
90
+ if FontLoader.collection?(@input)
91
+ validate_collection(mode)
92
+ else
93
+ validate_single_font(mode)
94
+ end
95
+ rescue StandardError => e
96
+ puts "Error: #{e.message}" unless @suppress_warnings
97
+ puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
98
+ 1
99
+ end
100
+
101
+ private
102
+
103
+ # Validate a single font file
104
+ #
105
+ # @param mode [Symbol] Loading mode
106
+ # @return [Integer] Exit code
107
+ def validate_single_font(mode)
79
108
  font = FontLoader.load(@input, mode: mode)
80
109
 
81
110
  # Select validator
@@ -102,13 +131,134 @@ module Fontisan
102
131
 
103
132
  # Return exit code
104
133
  exit_code(report)
105
- rescue => e
106
- puts "Error: #{e.message}" unless @suppress_warnings
107
- puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
108
- 1
109
134
  end
110
135
 
111
- private
136
+ # Validate all fonts in a collection
137
+ #
138
+ # @param mode [Symbol] Loading mode
139
+ # @return [Integer] Exit code
140
+ def validate_collection(mode)
141
+ require_relative "../models/collection_validation_report"
142
+ require_relative "../models/font_report"
143
+
144
+ # Load collection metadata
145
+ collection = FontLoader.load_collection(@input)
146
+
147
+ # Create collection report
148
+ collection_report = Models::CollectionValidationReport.new(
149
+ collection_path: @input,
150
+ collection_type: collection.class.collection_format,
151
+ num_fonts: collection.num_fonts,
152
+ )
153
+
154
+ # Get validator
155
+ validator = Validators::ProfileLoader.load(@profile)
156
+
157
+ # Validate each font
158
+ collection.num_fonts.times do |index|
159
+ font = FontLoader.load(@input, font_index: index, mode: mode)
160
+ font_report = validator.validate(font)
161
+
162
+ # Set font_path to indicate collection file with index
163
+ # Fonts in collections don't have individual file paths
164
+ font_report.font_path = "#{@input}:#{index}"
165
+
166
+ # Extract font name
167
+ font_name = extract_font_name(font, index)
168
+
169
+ # Create and add font report
170
+ collection_report.add_font_report(
171
+ Models::FontReport.new(
172
+ font_index: index,
173
+ font_name: font_name,
174
+ report: font_report,
175
+ ),
176
+ )
177
+ rescue StandardError => e
178
+ # Create error report for failed font loading
179
+ # Use collection file path with index for fonts in collections
180
+ error_report = Models::ValidationReport.new(
181
+ font_path: "#{@input}:#{index}",
182
+ valid: false,
183
+ )
184
+ error_report.add_error("font_loading",
185
+ "Failed to load font #{index}: #{e.message}", nil)
186
+
187
+ collection_report.add_font_report(
188
+ Models::FontReport.new(
189
+ font_index: index,
190
+ font_name: "Font #{index}",
191
+ report: error_report,
192
+ ),
193
+ )
194
+ end
195
+
196
+ # Generate output based on format
197
+ output = case @format
198
+ when :yaml
199
+ collection_report.to_yaml
200
+ when :json
201
+ collection_report.to_json
202
+ else
203
+ collection_report.text_summary
204
+ end
205
+
206
+ # Write to file or stdout
207
+ if @output
208
+ File.write(@output, output)
209
+ puts "Validation report written to #{@output}" if @verbose && !@suppress_warnings
210
+ else
211
+ puts output unless @suppress_warnings
212
+ end
213
+
214
+ # Return exit code based on worst status
215
+ collection_exit_code(collection_report)
216
+ end
217
+
218
+ # Extract font name from font object
219
+ #
220
+ # @param font [TrueTypeFont, OpenTypeFont] Font object
221
+ # @param index [Integer] Font index (fallback)
222
+ # @return [String] Font name
223
+ def extract_font_name(font, index)
224
+ return "Font #{index}" unless font.respond_to?(:table)
225
+
226
+ name_table = font.table("name")
227
+ return "Font #{index}" unless name_table
228
+
229
+ full_name = name_table.english_name(Tables::Name::FULL_NAME)
230
+ return full_name if full_name && !full_name.empty?
231
+
232
+ postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
233
+ return postscript_name if postscript_name && !postscript_name.empty?
234
+
235
+ "Font #{index}"
236
+ end
237
+
238
+ # Calculate exit code for collection validation
239
+ #
240
+ # Uses worst status across all fonts
241
+ #
242
+ # @param report [CollectionValidationReport] Collection report
243
+ # @return [Integer] Exit code
244
+ def collection_exit_code(report)
245
+ return 0 unless @return_value_results
246
+
247
+ # Check for fatal errors first
248
+ return 2 if report.font_reports.any? do |fr|
249
+ fr.report.fatal_errors.any?
250
+ end
251
+ # Then check for errors
252
+ return 3 if report.font_reports.any? { |fr| fr.report.errors_only.any? }
253
+ # Then check for warnings
254
+ return 4 if report.font_reports.any? do |fr|
255
+ fr.report.warnings_only.any?
256
+ end
257
+ # Then check for info
258
+ return 5 if report.font_reports.any? { |fr| fr.report.info_only.any? }
259
+
260
+ 0
261
+ end
112
262
 
113
263
  # Generate output based on requested format
114
264
  #
@@ -155,6 +305,7 @@ module Fontisan
155
305
  return 3 if report.errors_only.any?
156
306
  return 4 if report.warnings_only.any?
157
307
  return 5 if report.info_only.any?
308
+
158
309
  0
159
310
  end
160
311
  end
@@ -59,11 +59,12 @@ module Fontisan
59
59
 
60
60
  verbose = options.fetch(:verbose, false)
61
61
  output_path = options[:output]
62
- target_format = options.fetch(:target_format, 'preserve').to_s
62
+ target_format = options.fetch(:target_format, "preserve").to_s
63
63
 
64
64
  # Validate target_format
65
65
  unless %w[preserve ttf otf].include?(target_format)
66
- raise ArgumentError, "Invalid target_format: #{target_format}. Must be 'preserve', 'ttf', or 'otf'"
66
+ raise ArgumentError,
67
+ "Invalid target_format: #{target_format}. Must be 'preserve', 'ttf', or 'otf'"
67
68
  end
68
69
 
69
70
  puts "Converting collection to #{target_type.to_s.upcase}..." if verbose
@@ -76,19 +77,22 @@ module Fontisan
76
77
  if source_type == target_type
77
78
  puts " Source and target formats are the same, copying collection..." if verbose
78
79
  FileUtils.cp(collection_path, output_path)
79
- return build_result(collection_path, output_path, source_type, target_type, fonts.size, [])
80
+ return build_result(collection_path, output_path, source_type,
81
+ target_type, fonts.size, [])
80
82
  end
81
83
 
82
84
  # Step 2: Convert - transform fonts if requested
83
85
  puts " Converting #{fonts.size} font(s)..." if verbose
84
- converted_fonts, conversions = convert_fonts(fonts, source_type, target_type, options.merge(target_format: target_format))
86
+ converted_fonts, conversions = convert_fonts(fonts, source_type,
87
+ target_type, options.merge(target_format: target_format))
85
88
 
86
89
  # Step 3: Repack - build target collection
87
90
  puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
88
91
  repack_fonts(converted_fonts, target_type, output_path, options)
89
92
 
90
93
  # Build result
91
- result = build_result(collection_path, output_path, source_type, target_type, fonts.size, conversions)
94
+ result = build_result(collection_path, output_path, source_type,
95
+ target_type, fonts.size, conversions)
92
96
 
93
97
  if verbose
94
98
  display_result(result)
@@ -111,7 +115,8 @@ module Fontisan
111
115
  end
112
116
 
113
117
  unless %i[ttc otc dfont].include?(target_type)
114
- raise ArgumentError, "Invalid target type: #{target_type}. Must be :ttc, :otc, or :dfont"
118
+ raise ArgumentError,
119
+ "Invalid target type: #{target_type}. Must be :ttc, :otc, or :dfont"
115
120
  end
116
121
 
117
122
  unless options[:output]
@@ -202,20 +207,21 @@ module Fontisan
202
207
  # @param target_type [Symbol] Target collection type
203
208
  # @param options [Hash] Conversion options
204
209
  # @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
205
- def convert_fonts(fonts, source_type, target_type, options)
210
+ def convert_fonts(fonts, _source_type, target_type, options)
206
211
  converted_fonts = []
207
212
  conversions = []
208
213
 
209
214
  # Determine if outline conversion is needed
210
- target_format = options.fetch(:target_format, 'preserve').to_s
215
+ target_format = options.fetch(:target_format, "preserve").to_s
211
216
 
212
217
  fonts.each_with_index do |font, index|
213
218
  source_format = detect_font_format(font)
214
- needs_conversion = outline_conversion_needed?(source_format, target_format)
219
+ needs_conversion = outline_conversion_needed?(source_format,
220
+ target_format)
215
221
 
216
222
  if needs_conversion
217
223
  # Convert outline format
218
- desired_format = target_format == 'preserve' ? source_format : target_format.to_sym
224
+ desired_format = target_format == "preserve" ? source_format : target_format.to_sym
219
225
  converter = FormatConverter.new
220
226
 
221
227
  begin
@@ -266,7 +272,7 @@ module Fontisan
266
272
  # @return [Boolean] true if conversion needed
267
273
  def outline_conversion_needed?(source_format, target_format)
268
274
  # 'preserve' means keep original format
269
- return false if target_format == 'preserve'
275
+ return false if target_format == "preserve"
270
276
 
271
277
  # Convert if target format differs from source
272
278
  target_format.to_sym != source_format
@@ -401,7 +407,8 @@ module Fontisan
401
407
  # @param num_fonts [Integer] Number of fonts
402
408
  # @param conversions [Array<Hash>] Conversion details
403
409
  # @return [Hash] Result
404
- def build_result(input, output, source_type, target_type, num_fonts, conversions)
410
+ def build_result(input, output, source_type, target_type, num_fonts,
411
+ conversions)
405
412
  {
406
413
  input: input,
407
414
  output: output,
@@ -425,7 +432,9 @@ module Fontisan
425
432
  puts "Fonts: #{result[:num_fonts]}"
426
433
 
427
434
  if result[:conversions].any?
428
- converted_count = result[:conversions].count { |c| c[:status] == :converted }
435
+ converted_count = result[:conversions].count do |c|
436
+ c[:status] == :converted
437
+ end
429
438
  if converted_count.positive?
430
439
  puts "Outline conversions: #{converted_count}"
431
440
  end
@@ -107,7 +107,8 @@ module Fontisan
107
107
  # @param extractor [OutlineExtractor] Outline extractor for layer glyphs
108
108
  # @param palette_index [Integer] Palette index to use (default: 0)
109
109
  # @return [String, nil] SVG path data with color layers, or nil if not color glyph
110
- def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor, palette_index: 0)
110
+ def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor,
111
+ palette_index: 0)
111
112
  # Get layers for this glyph
112
113
  layers = colr_table.layers_for_glyph(glyph_id)
113
114
  return nil if layers.empty?
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "parsers/dfont_parser"
4
4
  require_relative "error"
5
+ require_relative "collection/shared_logic"
5
6
 
6
7
  module Fontisan
7
8
  # DfontCollection represents an Apple dfont suitcase containing multiple fonts
@@ -23,6 +24,8 @@ module Fontisan
23
24
  # fonts.each { |font| puts font.class.name }
24
25
  # end
25
26
  class DfontCollection
27
+ include Collection::SharedLogic
28
+
26
29
  # Path to dfont file
27
30
  # @return [String]
28
31
  attr_reader :path
@@ -32,6 +35,22 @@ module Fontisan
32
35
  attr_reader :num_fonts
33
36
  alias font_count num_fonts
34
37
 
38
+ # Get font offsets (indices for dfont)
39
+ #
40
+ # dfont doesn't use byte offsets like TTC/OTC, so we return indices
41
+ #
42
+ # @return [Array<Integer>] Array of font indices
43
+ def font_offsets
44
+ (0...@num_fonts).to_a
45
+ end
46
+
47
+ # Get the collection format identifier
48
+ #
49
+ # @return [String] "dfont" for dfont collection
50
+ def self.collection_format
51
+ "dfont"
52
+ end
53
+
35
54
  # Load dfont collection from file
36
55
  #
37
56
  # @param path [String] Path to dfont file
@@ -65,6 +84,15 @@ module Fontisan
65
84
  File.exist?(@path) && @num_fonts.positive?
66
85
  end
67
86
 
87
+ # Get the collection version as a string
88
+ #
89
+ # dfont files don't have version numbers like TTC/OTC
90
+ #
91
+ # @return [String] Version string (always "N/A" for dfont)
92
+ def version_string
93
+ "N/A"
94
+ end
95
+
68
96
  # Extract all fonts from dfont
69
97
  #
70
98
  # @param io [IO] Open file handle
@@ -181,5 +209,61 @@ module Fontisan
181
209
 
182
210
  font
183
211
  end
212
+
213
+ # Get comprehensive collection metadata
214
+ #
215
+ # Returns a CollectionInfo model with header information and
216
+ # table sharing statistics for the dfont collection.
217
+ # This is the API method used by the `info` command for collections.
218
+ #
219
+ # @param io [IO] Open file handle to read fonts from
220
+ # @param path [String] Collection file path (for file size)
221
+ # @return [Models::CollectionInfo] Collection metadata
222
+ #
223
+ # @example Get collection info
224
+ # File.open("family.dfont", "rb") do |io|
225
+ # collection = DfontCollection.from_file("family.dfont")
226
+ # info = collection.collection_info(io, "family.dfont")
227
+ # puts "Format: #{info.collection_format}"
228
+ # end
229
+ def collection_info(io, path)
230
+ require_relative "models/collection_info"
231
+ require_relative "models/table_sharing_info"
232
+
233
+ # Calculate table sharing statistics
234
+ table_sharing = calculate_table_sharing(io)
235
+
236
+ # Get file size
237
+ file_size = path ? File.size(path) : 0
238
+
239
+ Models::CollectionInfo.new(
240
+ collection_path: path,
241
+ collection_format: self.class.collection_format,
242
+ ttc_tag: "dfnt", # dfont doesn't use ttcf tag
243
+ major_version: 0, # dfont doesn't have version
244
+ minor_version: 0,
245
+ num_fonts: @num_fonts,
246
+ font_offsets: font_offsets,
247
+ file_size_bytes: file_size,
248
+ table_sharing: table_sharing,
249
+ )
250
+ end
251
+
252
+ private
253
+
254
+ # Calculate table sharing statistics
255
+ #
256
+ # Analyzes which tables are shared between fonts and calculates
257
+ # space savings from deduplication.
258
+ #
259
+ # @param io [IO] Open file handle
260
+ # @return [Models::TableSharingInfo] Sharing statistics
261
+ def calculate_table_sharing(io)
262
+ # Extract all fonts
263
+ fonts = extract_fonts(io)
264
+
265
+ # Use shared logic for calculation
266
+ calculate_table_sharing_for_fonts(fonts)
267
+ end
184
268
  end
185
269
  end
@@ -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."
@@ -99,11 +100,10 @@ module Fontisan
99
100
  # Check for TTC/OTC signature
100
101
  return true if signature == Constants::TTC_TAG
101
102
 
102
- # Check for multi-font dfont (suitcase) - only if it's actually a dfont
103
+ # Check for dfont - dfont is a collection format even if it contains only one font
103
104
  if signature == Constants::DFONT_RESOURCE_HEADER
104
105
  require_relative "parsers/dfont_parser"
105
- # Verify it's a valid dfont and has multiple fonts
106
- return Parsers::DfontParser.dfont?(io) && Parsers::DfontParser.sfnt_count(io) > 1
106
+ return Parsers::DfontParser.dfont?(io)
107
107
  end
108
108
 
109
109
  false
@@ -186,7 +186,7 @@ module Fontisan
186
186
  num_fonts = io.read(4).unpack1("N")
187
187
 
188
188
  # Read all font offsets
189
- font_offsets = num_fonts.times.map { io.read(4).unpack1("N") }
189
+ font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
190
190
 
191
191
  # Scan all fonts to determine collection type (not just first)
192
192
  truetype_count = 0
@@ -213,7 +213,7 @@ module Fontisan
213
213
  # Determine collection type based on what fonts are inside
214
214
  # If ANY font is OpenType, use OpenTypeCollection (more general format)
215
215
  # Only use TrueTypeCollection if ALL fonts are TrueType
216
- if opentype_count > 0
216
+ if opentype_count.positive?
217
217
  OpenTypeCollection.from_file(path)
218
218
  else
219
219
  # All fonts are TrueType
@@ -283,7 +283,7 @@ mode: LoadingModes::FULL, lazy: true)
283
283
  end
284
284
 
285
285
  # Read all font offsets
286
- font_offsets = num_fonts.times.map { io.read(4).unpack1("N") }
286
+ font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
287
287
 
288
288
  # Scan all fonts to determine collection type (not just first)
289
289
  truetype_count = 0
@@ -309,7 +309,7 @@ mode: LoadingModes::FULL, lazy: true)
309
309
 
310
310
  # If ANY font is OpenType, use OpenTypeCollection (more general format)
311
311
  # Only use TrueTypeCollection if ALL fonts are TrueType
312
- if opentype_count > 0
312
+ if opentype_count.positive?
313
313
  # OpenType Collection
314
314
  otc = OpenTypeCollection.from_file(path)
315
315
  File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
@@ -329,7 +329,7 @@ mode: LoadingModes::FULL, lazy: true)
329
329
  # @param lazy [Boolean] Lazy loading flag
330
330
  # @return [TrueTypeFont, OpenTypeFont] Loaded font
331
331
  # @api private
332
- 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)
333
333
  require_relative "parsers/dfont_parser"
334
334
 
335
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