fontisan 0.2.0 → 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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +119 -308
  3. data/README.adoc +1525 -1323
  4. data/Rakefile +45 -47
  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 +92 -34
  9. data/lib/fontisan/collection/builder.rb +82 -0
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  12. data/lib/fontisan/commands/base_command.rb +21 -2
  13. data/lib/fontisan/commands/convert_command.rb +96 -165
  14. data/lib/fontisan/commands/info_command.rb +111 -5
  15. data/lib/fontisan/commands/instance_command.rb +77 -85
  16. data/lib/fontisan/commands/validate_command.rb +28 -0
  17. data/lib/fontisan/config/validation_rules.yml +1 -1
  18. data/lib/fontisan/constants.rb +34 -24
  19. data/lib/fontisan/converters/format_converter.rb +154 -1
  20. data/lib/fontisan/converters/outline_converter.rb +101 -34
  21. data/lib/fontisan/converters/woff_writer.rb +9 -4
  22. data/lib/fontisan/font_loader.rb +14 -9
  23. data/lib/fontisan/font_writer.rb +9 -6
  24. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  25. data/lib/fontisan/hints/hint_converter.rb +131 -2
  26. data/lib/fontisan/hints/hint_validator.rb +284 -0
  27. data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
  28. data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
  29. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  30. data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
  31. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  32. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  33. data/lib/fontisan/loading_modes.rb +6 -4
  34. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +183 -12
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/open_type_font.rb +28 -10
  39. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  40. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  41. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  42. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  43. data/lib/fontisan/pipeline/output_writer.rb +159 -0
  44. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  45. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  46. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  47. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  48. data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
  49. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  51. data/lib/fontisan/tables/cff/charstring.rb +58 -3
  52. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  53. data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
  54. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  55. data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
  56. data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
  57. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  58. data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
  59. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  60. data/lib/fontisan/tables/cff.rb +2 -0
  61. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  62. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
  63. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  64. data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
  65. data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
  66. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  67. data/lib/fontisan/tables/cff2.rb +10 -5
  68. data/lib/fontisan/tables/cvar.rb +2 -41
  69. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  70. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  71. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  72. data/lib/fontisan/tables/gvar.rb +2 -41
  73. data/lib/fontisan/tables/name.rb +4 -4
  74. data/lib/fontisan/true_type_font.rb +27 -10
  75. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  76. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  77. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  78. data/lib/fontisan/validation/table_validator.rb +1 -1
  79. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  80. data/lib/fontisan/variation/cache.rb +3 -1
  81. data/lib/fontisan/variation/converter.rb +121 -13
  82. data/lib/fontisan/variation/delta_applier.rb +2 -1
  83. data/lib/fontisan/variation/inspector.rb +2 -1
  84. data/lib/fontisan/variation/instance_generator.rb +2 -1
  85. data/lib/fontisan/variation/instance_writer.rb +341 -0
  86. data/lib/fontisan/variation/optimizer.rb +6 -3
  87. data/lib/fontisan/variation/subsetter.rb +32 -10
  88. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  89. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  90. data/lib/fontisan/variation/variation_preserver.rb +291 -0
  91. data/lib/fontisan/version.rb +1 -1
  92. data/lib/fontisan/version.rb.orig +9 -0
  93. data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
  94. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  95. data/lib/fontisan/woff2_font.rb +489 -468
  96. data/lib/fontisan/woff_font.rb +16 -11
  97. data/lib/fontisan.rb +54 -2
  98. data/scripts/measure_optimization.rb +15 -7
  99. metadata +37 -2
data/lib/fontisan/cli.rb CHANGED
@@ -25,16 +25,24 @@ module Fontisan
25
25
  desc: "Suppress non-error output",
26
26
  aliases: "-q"
27
27
 
28
- desc "info FONT_FILE", "Display font information"
28
+ desc "info PATH", "Display font information"
29
+ option :brief, type: :boolean, default: false,
30
+ desc: "Brief mode - only essential info (5x faster, uses metadata loading)",
31
+ aliases: "-b"
29
32
  # Extract and display comprehensive font metadata.
30
33
  #
31
- # @param font_file [String] Path to the font file
32
- def info(font_file)
33
- command = Commands::InfoCommand.new(font_file, options)
34
- result = command.run
35
- output_result(result)
36
- rescue Errno::ENOENT, Error => e
37
- handle_error(e)
34
+ # @param path [String] Path to the font file or collection
35
+ def info(path)
36
+ command = Commands::InfoCommand.new(path, options)
37
+ info = command.run
38
+ output_result(info) unless options[:quiet]
39
+ rescue Errno::ENOENT
40
+ if options[:verbose]
41
+ raise
42
+ else
43
+ warn "File not found: #{path}" unless options[:quiet]
44
+ exit 1
45
+ end
38
46
  end
39
47
 
40
48
  desc "ls FILE", "List contents (fonts in collection or font summary)"
@@ -195,47 +203,82 @@ module Fontisan
195
203
 
196
204
  desc "convert FONT_FILE", "Convert font to different format"
197
205
  option :to, type: :string, required: true,
198
- desc: "Target format (ttf, otf, woff2, svg)",
206
+ desc: "Target format (ttf, otf, woff, woff2)",
199
207
  aliases: "-t"
200
208
  option :output, type: :string, required: true,
201
209
  desc: "Output file path",
202
210
  aliases: "-o"
203
- option :optimize, type: :boolean, default: false,
204
- desc: "Optimize CFF with subroutines (TTF→OTF only)"
205
- option :min_pattern_length, type: :numeric, default: 10,
206
- desc: "Minimum pattern length for subroutines"
207
- option :max_subroutines, type: :numeric, default: 65_535,
208
- desc: "Maximum number of subroutines"
209
- option :optimize_ordering, type: :boolean, default: true,
210
- desc: "Optimize subroutine ordering by frequency"
211
- # Convert a font to a different format.
211
+ option :coordinates, type: :string,
212
+ desc: "Instance coordinates (e.g., wght=700,wdth=100)",
213
+ aliases: "-c"
214
+ option :instance_index, type: :numeric,
215
+ desc: "Named instance index",
216
+ aliases: "-n"
217
+ option :preserve_variation, type: :boolean,
218
+ desc: "Force variation preservation (auto-detected by default)"
219
+ option :no_validate, type: :boolean, default: false,
220
+ desc: "Skip output validation"
221
+ option :preserve_hints, type: :boolean, default: false,
222
+ desc: "Preserve rendering hints during conversion (TTF→OTF preservations may be limited)"
223
+ option :wght, type: :numeric,
224
+ desc: "Weight axis value (alternative to --coordinates)"
225
+ option :wdth, type: :numeric,
226
+ desc: "Width axis value (alternative to --coordinates)"
227
+ option :slnt, type: :numeric,
228
+ desc: "Slant axis value (alternative to --coordinates)"
229
+ option :ital, type: :numeric,
230
+ desc: "Italic axis value (alternative to --coordinates)"
231
+ option :opsz, type: :numeric,
232
+ desc: "Optical size axis value (alternative to --coordinates)"
233
+ # Convert a font to a different format using the universal transformation pipeline.
212
234
  #
213
235
  # Supported conversions:
214
- # - Same format (ttf→ttf, otf→otf): Copy/optimize
215
- # - TTF ↔ OTF: Outline format conversion (foundation)
216
- # - Future: WOFF2 compression, SVG export
236
+ # - TTF OTF: Outline format conversion
237
+ # - WOFF/WOFF2: Web font packaging
238
+ # - Variable fonts: Automatic variation preservation or instance generation
239
+ #
240
+ # Variable Font Operations:
241
+ # The pipeline automatically detects whether variation data can be preserved based on
242
+ # source and target formats. For same outline family (TTF→WOFF or OTF→WOFF2), variation
243
+ # is preserved automatically. For cross-family conversions (TTF↔OTF), an instance is
244
+ # generated unless --preserve-variation is explicitly set.
217
245
  #
218
- # Subroutine Optimization (--optimize):
219
- # When converting TTF→OTF, you can enable automatic CFF subroutine generation
220
- # to reduce file size. This analyzes repeated byte patterns across glyphs and
221
- # creates shared subroutines, typically saving 30-50% in CFF table size.
246
+ # Instance Generation:
247
+ # Use --coordinates to specify exact axis values (e.g., wght=700,wdth=100) or
248
+ # --instance-index to use a named instance. Individual axis options (--wght, --wdth)
249
+ # are also supported for convenience.
222
250
  #
223
251
  # @param font_file [String] Path to the font file
224
252
  #
225
253
  # @example Convert TTF to OTF
226
254
  # fontisan convert font.ttf --to otf --output font.otf
227
255
  #
228
- # @example Convert with optimization
229
- # fontisan convert font.ttf --to otf --output font.otf --optimize --verbose
256
+ # @example Generate bold instance at specific coordinates
257
+ # fontisan convert variable.ttf --to ttf --output bold.ttf --coordinates "wght=700,wdth=100"
258
+ #
259
+ # @example Generate bold instance using individual axis options
260
+ # fontisan convert variable.ttf --to ttf --output bold.ttf --wght 700
261
+ #
262
+ # @example Use named instance
263
+ # fontisan convert variable.ttf --to woff2 --output bold.woff2 --instance-index 0
230
264
  #
231
- # @example Convert with custom optimization parameters
232
- # fontisan convert font.ttf --to otf --output font.otf --optimize \
233
- # --min-pattern-length 15 --max-subroutines 10000
265
+ # @example Force variation preservation (if compatible)
266
+ # fontisan convert variable.ttf --to woff2 --output variable.woff2 --preserve-variation
234
267
  #
235
- # @example Copy/optimize TTF
236
- # fontisan convert font.ttf --to ttf --output optimized.ttf
268
+ # @example Convert without validation
269
+ # fontisan convert font.ttf --to otf --output font.otf --no-validate
237
270
  def convert(font_file)
238
- command = Commands::ConvertCommand.new(font_file, options)
271
+ # Build instance coordinates from axis options
272
+ instance_coords = build_instance_coordinates(options)
273
+
274
+ # Merge coordinates into options
275
+ convert_options = options.to_h.dup
276
+ if instance_coords.any?
277
+ convert_options[:instance_coordinates] =
278
+ instance_coords
279
+ end
280
+
281
+ command = Commands::ConvertCommand.new(font_file, convert_options)
239
282
  command.run
240
283
  rescue Errno::ENOENT, Error => e
241
284
  handle_error(e)
@@ -311,7 +354,8 @@ module Fontisan
311
354
  option :verbose, type: :boolean, default: false,
312
355
  desc: "Show detailed validation information"
313
356
  def validate(font_file)
314
- command = Commands::ValidateCommand.new(font_file, verbose: options[:verbose])
357
+ command = Commands::ValidateCommand.new(font_file,
358
+ verbose: options[:verbose])
315
359
  exit command.run
316
360
  end
317
361
 
@@ -444,6 +488,20 @@ module Fontisan
444
488
 
445
489
  private
446
490
 
491
+ # Build instance coordinates from CLI axis options
492
+ #
493
+ # @param options [Hash] CLI options
494
+ # @return [Hash] Coordinates hash
495
+ def build_instance_coordinates(options)
496
+ coords = {}
497
+ coords["wght"] = options[:wght].to_f if options[:wght]
498
+ coords["wdth"] = options[:wdth].to_f if options[:wdth]
499
+ coords["slnt"] = options[:slnt].to_f if options[:slnt]
500
+ coords["ital"] = options[:ital].to_f if options[:ital]
501
+ coords["opsz"] = options[:opsz].to_f if options[:opsz]
502
+ coords
503
+ end
504
+
447
505
  # Output the result in the requested format.
448
506
  #
449
507
  # @param result [Object] The result object to output
@@ -164,6 +164,9 @@ module Fontisan
164
164
  raise Error, "Format mismatch: #{incompatible.join(', ')}"
165
165
  end
166
166
 
167
+ # Check variable font compatibility
168
+ validate_variation_compatibility! if variable_fonts_in_collection?
169
+
167
170
  # Check all fonts have required tables
168
171
  @fonts.each_with_index do |font, index|
169
172
  required_tables = %w[head hhea maxp]
@@ -177,6 +180,26 @@ module Fontisan
177
180
  true
178
181
  end
179
182
 
183
+ # Check if collection contains variable fonts
184
+ #
185
+ # @return [Boolean] true if any font has fvar table
186
+ def variable_fonts_in_collection?
187
+ @fonts.any? { |font| font.has_table?("fvar") }
188
+ end
189
+
190
+ # Validate variable font compatibility
191
+ #
192
+ # Ensures all variable fonts in the collection are compatible:
193
+ # - All must be same variation type (TrueType or CFF2)
194
+ # - All must have the same axes
195
+ #
196
+ # @return [void]
197
+ # @raise [Error] if variable fonts are incompatible
198
+ def validate_variation_compatibility!
199
+ validate_all_same_variation_type!
200
+ validate_same_axes!
201
+ end
202
+
180
203
  private
181
204
 
182
205
  # Load configuration from file
@@ -255,6 +278,65 @@ module Fontisan
255
278
 
256
279
  incompatible
257
280
  end
281
+
282
+ # Validate all variable fonts use same variation type
283
+ #
284
+ # @return [void]
285
+ # @raise [Error] if mixing TrueType and CFF2 variable fonts
286
+ def validate_all_same_variation_type!
287
+ variable_fonts = @fonts.select { |f| f.has_table?("fvar") }
288
+ return if variable_fonts.empty?
289
+
290
+ ttf_count = variable_fonts.count { |f| f.has_table?("glyf") }
291
+ otf_count = variable_fonts.count { |f| f.has_table?("CFF2") }
292
+
293
+ if ttf_count.positive? && otf_count.positive?
294
+ raise Error,
295
+ "Cannot mix TrueType and CFF2 variable fonts in collection"
296
+ end
297
+ end
298
+
299
+ # Validate all variable fonts have same axes
300
+ #
301
+ # @return [void]
302
+ # @raise [Error] if variable fonts have different axes
303
+ def validate_same_axes!
304
+ variable_fonts = @fonts.select { |f| f.has_table?("fvar") }
305
+ return if variable_fonts.size < 2
306
+
307
+ first_axes = extract_axes(variable_fonts.first)
308
+ variable_fonts.each_with_index do |font, index|
309
+ font_axes = extract_axes(font)
310
+ unless axes_match?(font_axes, first_axes)
311
+ raise Error,
312
+ "Variable font #{index} has different axes. " \
313
+ "Expected: #{first_axes.join(', ')}, " \
314
+ "Got: #{font_axes.join(', ')}"
315
+ end
316
+ end
317
+ end
318
+
319
+ # Extract axis tags from a font's fvar table
320
+ #
321
+ # @param font [TrueTypeFont, OpenTypeFont] Font to extract axes from
322
+ # @return [Array<String>] Sorted array of axis tags
323
+ def extract_axes(font)
324
+ return [] unless font.has_table?("fvar")
325
+
326
+ fvar_table = font.table("fvar")
327
+ return [] unless fvar_table.respond_to?(:axes)
328
+
329
+ fvar_table.axes.map(&:axis_tag).sort
330
+ end
331
+
332
+ # Check if two axis arrays match
333
+ #
334
+ # @param axes1 [Array<String>] First axis array
335
+ # @param axes2 [Array<String>] Second axis array
336
+ # @return [Boolean] true if axes match
337
+ def axes_match?(axes1, axes2)
338
+ axes1 == axes2
339
+ end
258
340
  end
259
341
  end
260
342
  end
@@ -165,6 +165,7 @@ module Fontisan
165
165
  end
166
166
  end
167
167
 
168
+ # rubocop:disable Style/CombinableLoops
168
169
  # First, assign offsets to shared tables
169
170
  # Shared tables are stored once and referenced by multiple fonts
170
171
  canonical_tables.each do |canonical_id, info|
@@ -182,6 +183,7 @@ module Fontisan
182
183
  @offsets[:table_offsets][canonical_id] = current_offset
183
184
  current_offset = align_offset(current_offset + info[:size])
184
185
  end
186
+ # rubocop:enable Style/CombinableLoops
185
187
  end
186
188
 
187
189
  # Align offset to TABLE_ALIGNMENT boundary
@@ -15,6 +15,12 @@ module Fontisan
15
15
  # sharing_map = deduplicator.build_sharing_map
16
16
  # canonical_tables = deduplicator.canonical_tables
17
17
  class TableDeduplicator
18
+ # Tables that can be shared in variable font collections if identical
19
+ VARIATION_SHAREABLE_TABLES = %w[fvar avar STAT HVAR VVAR MVAR].freeze
20
+
21
+ # Tables that must remain font-specific in variable fonts
22
+ VARIATION_FONT_SPECIFIC_TABLES = %w[gvar CFF2].freeze
23
+
18
24
  # Canonical tables (unique table data)
19
25
  # @return [Hash<String, Hash>] Map of table tag to canonical versions
20
26
  attr_reader :canonical_tables
@@ -62,6 +68,9 @@ module Fontisan
62
68
  # First pass: collect all unique tables
63
69
  collect_canonical_tables
64
70
 
71
+ # Handle variable font table deduplication
72
+ deduplicate_variation_tables if has_variable_fonts?
73
+
65
74
  # Second pass: build sharing map for each font
66
75
  build_font_sharing_references
67
76
 
@@ -115,6 +124,73 @@ module Fontisan
115
124
 
116
125
  private
117
126
 
127
+ # Check if any font is a variable font
128
+ #
129
+ # @return [Boolean] true if any font has fvar table
130
+ def has_variable_fonts?
131
+ @fonts.any? { |font| font.has_table?("fvar") }
132
+ end
133
+
134
+ # Deduplicate variable font tables
135
+ #
136
+ # Handles special logic for variable font tables:
137
+ # - Share tables that are identical across fonts (fvar, avar, etc.)
138
+ # - Keep font-specific tables separate (gvar, CFF2)
139
+ #
140
+ # @return [void]
141
+ def deduplicate_variation_tables
142
+ # Share tables that are identical across fonts
143
+ VARIATION_SHAREABLE_TABLES.each do |tag|
144
+ share_if_identical(tag)
145
+ end
146
+
147
+ # Never share font-specific variation tables
148
+ VARIATION_FONT_SPECIFIC_TABLES.each do |tag|
149
+ keep_separate(tag)
150
+ end
151
+ end
152
+
153
+ # Share table if identical across all fonts that have it
154
+ #
155
+ # @param tag [String] Table tag
156
+ # @return [void]
157
+ def share_if_identical(tag)
158
+ # Get all instances of this table
159
+ tables = @fonts.map { |f| f.table_data[tag] }.compact
160
+ return if tables.empty?
161
+
162
+ # Check if all instances are identical
163
+ nil if tables.uniq.length > 1
164
+
165
+ # All instances are identical, mark as shareable
166
+ # The normal deduplication logic will handle this
167
+ end
168
+
169
+ # Ensure table stays separate for each font
170
+ #
171
+ # @param tag [String] Table tag
172
+ # @return [void]
173
+ def keep_separate(tag)
174
+ # Mark each font's instance as non-shareable
175
+ @fonts.each_with_index do |font, _font_index|
176
+ next unless font.has_table?(tag)
177
+
178
+ # Find this font's canonical table for this tag
179
+ table_data = font.table_data[tag]
180
+ checksum = calculate_checksum(table_data)
181
+
182
+ # Ensure canonical table exists
183
+ @canonical_tables[tag] ||= {}
184
+ canonical_id = @checksum_to_canonical.dig(tag, checksum)
185
+
186
+ next unless canonical_id && @canonical_tables[tag][canonical_id]
187
+
188
+ # Mark as non-shareable
189
+ @canonical_tables[tag][canonical_id][:shared] = false
190
+ @canonical_tables[tag][canonical_id][:font_specific] = true
191
+ end
192
+ end
193
+
118
194
  # Collect all unique (canonical) tables across all fonts
119
195
  #
120
196
  # Identifies unique table content based on checksum and stores one
@@ -66,13 +66,32 @@ module Fontisan
66
66
  # @raise [InvalidFontError] for corrupted or unknown formats
67
67
  # @raise [Error] for other loading failures
68
68
  def load_font
69
+ # BaseCommand is for inspection - reject compressed formats first
70
+ # Check file signature before attempting to load
71
+ File.open(@font_path, "rb") do |io|
72
+ signature = io.read(4)
73
+
74
+ if signature == "wOFF"
75
+ raise UnsupportedFormatError,
76
+ "Unsupported font format: WOFF files must be decompressed first. " \
77
+ "Use ConvertCommand to convert WOFF to TTF/OTF."
78
+ elsif signature == "wOF2"
79
+ raise UnsupportedFormatError,
80
+ "Unsupported font format: WOFF2 files must be decompressed first. " \
81
+ "Use ConvertCommand to convert WOFF2 to TTF/OTF."
82
+ end
83
+ end
84
+
85
+ # Brief mode uses metadata loading for 5x faster parsing
86
+ mode = @options[:brief] ? LoadingModes::METADATA : (@options[:mode] || LoadingModes::FULL)
87
+
69
88
  # ConvertCommand and similar commands need all tables loaded upfront
70
89
  # Use mode and lazy from options, or sensible defaults
71
90
  FontLoader.load(
72
91
  @font_path,
73
92
  font_index: @options[:font_index] || 0,
74
- mode: @options[:mode] || LoadingModes::FULL,
75
- lazy: @options.key?(:lazy) ? @options[:lazy] : false
93
+ mode: mode,
94
+ lazy: @options.key?(:lazy) ? @options[:lazy] : false,
76
95
  )
77
96
  rescue Errno::ENOENT
78
97
  # Re-raise file not found as-is