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
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_command"
4
- require_relative "../converters/format_converter"
5
- require_relative "../font_writer"
4
+ require_relative "../pipeline/transformation_pipeline"
6
5
 
7
6
  module Fontisan
8
7
  module Commands
9
8
  # Command for converting fonts between formats
10
9
  #
11
10
  # [`ConvertCommand`](lib/fontisan/commands/convert_command.rb) provides
12
- # CLI interface for font format conversion operations. It supports:
11
+ # CLI interface for font format conversion operations using the universal
12
+ # transformation pipeline. It supports:
13
13
  # - Same-format operations (copy/optimize)
14
- # - TTF ↔ OTF outline format conversion (foundation)
15
- # - Future: WOFF/WOFF2 compression, SVG export
14
+ # - TTF ↔ OTF outline format conversion
15
+ # - Variable font operations (preserve/instance generation)
16
+ # - WOFF/WOFF2 compression
16
17
  #
17
- # The command uses [`FormatConverter`](lib/fontisan/converters/format_converter.rb)
18
+ # The command uses [`TransformationPipeline`](lib/fontisan/pipeline/transformation_pipeline.rb)
18
19
  # to orchestrate conversions with appropriate strategies.
19
20
  #
20
21
  # @example Convert TTF to OTF
@@ -25,11 +26,12 @@ module Fontisan
25
26
  # )
26
27
  # command.run
27
28
  #
28
- # @example Copy/optimize same format
29
+ # @example Generate instance at coordinates
29
30
  # command = ConvertCommand.new(
30
- # 'input.ttf',
31
+ # 'variable.ttf',
31
32
  # to: 'ttf',
32
- # output: 'optimized.ttf'
33
+ # output: 'bold.ttf',
34
+ # coordinates: 'wght=700,wdth=100'
33
35
  # )
34
36
  # command.run
35
37
  class ConvertCommand < BaseCommand
@@ -37,24 +39,34 @@ module Fontisan
37
39
  #
38
40
  # @param font_path [String] Path to input font file
39
41
  # @param options [Hash] Conversion options
40
- # @option options [String] :to Target format (ttf, otf, woff2, svg)
42
+ # @option options [String] :to Target format (ttf, otf, woff, woff2)
41
43
  # @option options [String] :output Output file path (required)
42
44
  # @option options [Integer] :font_index Index for TTC/OTC (default: 0)
43
- # @option options [Boolean] :optimize Enable subroutine optimization (TTF→OTF only)
44
- # @option options [Integer] :min_pattern_length Minimum pattern length for subroutines
45
- # @option options [Integer] :max_subroutines Maximum number of subroutines
46
- # @option options [Boolean] :optimize_ordering Optimize subroutine ordering
45
+ # @option options [String] :coordinates Coordinate string (e.g., "wght=700,wdth=100")
46
+ # @option options [Hash] :instance_coordinates Axis coordinates hash (e.g., {"wght" => 700.0})
47
+ # @option options [Integer] :instance_index Named instance index
48
+ # @option options [Boolean] :preserve_variation Preserve variation data (default: auto)
49
+ # @option options [Boolean] :preserve_hints Preserve rendering hints (default: false)
50
+ # @option options [Boolean] :no_validate Skip output validation
51
+ # @option options [Boolean] :verbose Verbose output
47
52
  def initialize(font_path, options = {})
48
53
  super(font_path, options)
49
- @target_format = parse_target_format(options[:to])
50
54
  @output_path = options[:output]
51
- @converter = Converters::FormatConverter.new
52
55
 
53
- # Optimization options
54
- @optimize = options[:optimize] || false
55
- @min_pattern_length = options[:min_pattern_length] || 10
56
- @max_subroutines = options[:max_subroutines] || 65_535
57
- @optimize_ordering = options[:optimize_ordering] != false
56
+ # Parse target format
57
+ @target_format = parse_target_format(options[:to])
58
+
59
+ # Parse coordinates if string provided
60
+ @coordinates = if options[:coordinates]
61
+ parse_coordinates(options[:coordinates])
62
+ elsif options[:instance_coordinates]
63
+ options[:instance_coordinates]
64
+ end
65
+
66
+ @instance_index = options[:instance_index]
67
+ @preserve_variation = options[:preserve_variation]
68
+ @preserve_hints = options.fetch(:preserve_hints, false)
69
+ @validate = !options[:no_validate]
58
70
  end
59
71
 
60
72
  # Execute the conversion
@@ -65,65 +77,62 @@ module Fontisan
65
77
  def run
66
78
  validate_options!
67
79
 
68
- puts "Converting #{File.basename(font_path)} to #{@target_format}..."
80
+ puts "Converting #{File.basename(font_path)} to #{@target_format}..." unless @options[:quiet]
69
81
 
70
- # Build converter options
71
- converter_options = {
82
+ # Build pipeline options
83
+ pipeline_options = {
72
84
  target_format: @target_format,
73
- optimize_subroutines: @optimize,
74
- min_pattern_length: @min_pattern_length,
75
- max_subroutines: @max_subroutines,
76
- optimize_ordering: @optimize_ordering,
77
- verbose: options[:verbose],
85
+ validate: @validate,
86
+ verbose: @options[:verbose],
78
87
  }
79
88
 
80
- # Perform conversion with options
81
- result = @converter.convert(font, @target_format, converter_options)
89
+ # Add variation options if specified
90
+ pipeline_options[:coordinates] = @coordinates if @coordinates
91
+ pipeline_options[:instance_index] = @instance_index if @instance_index
92
+ unless @preserve_variation.nil?
93
+ pipeline_options[:preserve_variation] =
94
+ @preserve_variation
95
+ end
82
96
 
83
- # Handle special formats that return complete binary/text
84
- if @target_format == :woff && result.is_a?(String)
85
- # WOFF returns complete binary
86
- File.binwrite(@output_path, result)
87
- elsif @target_format == :woff2 && result.is_a?(Hash) && result[:woff2_binary]
88
- File.binwrite(@output_path, result[:woff2_binary])
89
- elsif @target_format == :svg && result.is_a?(Hash) && result[:svg_xml]
90
- File.write(@output_path, result[:svg_xml])
91
- else
92
- # Standard table-based conversion
93
- tables = result
97
+ # Add hint preservation option
98
+ pipeline_options[:preserve_hints] = @preserve_hints if @preserve_hints
94
99
 
95
- # Determine sfnt version for output
96
- sfnt_version = determine_sfnt_version(@target_format)
100
+ # Use TransformationPipeline for universal conversion
101
+ pipeline = Pipeline::TransformationPipeline.new(
102
+ font_path,
103
+ @output_path,
104
+ pipeline_options,
105
+ )
97
106
 
98
- # Write output font
99
- FontWriter.write_to_file(tables, @output_path,
100
- sfnt_version: sfnt_version)
107
+ result = pipeline.transform
101
108
 
102
- # Display optimization results if available
103
- display_optimization_results(tables) if @optimize && options[:verbose]
104
- end
109
+ # Display results
110
+ unless @options[:quiet]
111
+ output_size = File.size(@output_path)
112
+ input_size = File.size(font_path)
105
113
 
106
- output_size = File.size(@output_path)
107
- input_size = File.size(font_path)
114
+ puts "Conversion complete!"
115
+ puts " Input: #{font_path} (#{format_size(input_size)})"
116
+ puts " Output: #{@output_path} (#{format_size(output_size)})"
117
+ puts " Format: #{result[:details][:source_format]} → #{result[:details][:target_format]}"
108
118
 
109
- puts "Conversion complete!"
110
- puts " Input: #{font_path} (#{format_size(input_size)})"
111
- puts " Output: #{@output_path} (#{format_size(output_size)})"
119
+ if result[:details][:variation_preserved]
120
+ puts " Variation: Preserved (#{result[:details][:variation_strategy]})"
121
+ elsif result[:details][:variation_strategy] != :preserve
122
+ puts " Variation: Instance generated (#{result[:details][:variation_strategy]})"
123
+ end
124
+ end
112
125
 
113
126
  {
114
127
  success: true,
115
128
  input_path: font_path,
116
129
  output_path: @output_path,
117
- source_format: detect_source_format,
118
- target_format: @target_format,
119
- input_size: input_size,
120
- output_size: output_size,
130
+ source_format: result[:details][:source_format],
131
+ target_format: result[:details][:target_format],
132
+ input_size: File.size(font_path),
133
+ output_size: File.size(@output_path),
134
+ variation_strategy: result[:details][:variation_strategy],
121
135
  }
122
- rescue NotImplementedError
123
- # Let NotImplementedError propagate for tests that expect it
124
- raise
125
- rescue Converters::ConversionStrategy => e
126
- handle_conversion_error(e)
127
136
  rescue ArgumentError
128
137
  # Let ArgumentError propagate for validation errors
129
138
  raise
@@ -131,26 +140,28 @@ module Fontisan
131
140
  raise Error, "Conversion failed: #{e.message}"
132
141
  end
133
142
 
134
- # Get list of supported conversions
135
- #
136
- # @return [Array<Hash>] List of supported conversions
137
- def self.supported_conversions
138
- converter = Converters::FormatConverter.new
139
- converter.all_conversions
140
- end
143
+ private
141
144
 
142
- # Check if a conversion is supported
145
+ # Parse coordinates string to hash
146
+ #
147
+ # Parses strings like "wght=700,wdth=100" into {"wght" => 700.0, "wdth" => 100.0}
143
148
  #
144
- # @param source [Symbol] Source format
145
- # @param target [Symbol] Target format
146
- # @return [Boolean] True if supported
147
- def self.supported?(source, target)
148
- converter = Converters::FormatConverter.new
149
- converter.supported?(source, target)
149
+ # @param coord_string [String] Coordinate string
150
+ # @return [Hash] Parsed coordinates
151
+ def parse_coordinates(coord_string)
152
+ coords = {}
153
+ coord_string.split(",").each do |pair|
154
+ key, value = pair.split("=")
155
+ next unless key && value
156
+
157
+ coords[key.strip] = value.to_f
158
+ end
159
+ coords
160
+ rescue StandardError => e
161
+ raise ArgumentError,
162
+ "Invalid coordinates format '#{coord_string}': #{e.message}"
150
163
  end
151
164
 
152
- private
153
-
154
165
  # Validate command options
155
166
  #
156
167
  # @raise [ArgumentError] If required options are missing
@@ -164,18 +175,6 @@ module Fontisan
164
175
  raise ArgumentError,
165
176
  "Target format is required. Use --to option."
166
177
  end
167
-
168
- # Check if conversion is supported
169
- source_format = detect_source_format
170
- unless @converter.supported?(source_format, @target_format)
171
- available = @converter.supported_targets(source_format)
172
- message = "Conversion from #{source_format} to #{@target_format} " \
173
- "is not supported."
174
- if available.any?
175
- message += " Available targets: #{available.join(', ')}"
176
- end
177
- raise ArgumentError, message
178
- end
179
178
  end
180
179
 
181
180
  # Parse target format from string/symbol
@@ -191,46 +190,17 @@ module Fontisan
191
190
  :ttf
192
191
  when "otf", "opentype", "cff"
193
192
  :otf
193
+ when "svg"
194
+ :svg
194
195
  when "woff"
195
- :woff
196
+ raise ArgumentError,
197
+ "WOFF format conversion is not supported yet. Use woff2 instead."
196
198
  when "woff2"
197
199
  :woff2
198
- when "svg"
199
- :svg
200
200
  else
201
201
  raise ArgumentError,
202
202
  "Unknown target format: #{format}. " \
203
- "Supported: ttf, otf, woff2, svg"
204
- end
205
- end
206
-
207
- # Detect source font format
208
- #
209
- # @return [Symbol] Source format
210
- def detect_source_format
211
- # Check for CFF/CFF2 tables (OpenType/CFF)
212
- if font.has_table?("CFF ") || font.has_table?("CFF2")
213
- :otf
214
- # Check for glyf table (TrueType)
215
- elsif font.has_table?("glyf")
216
- :ttf
217
- else
218
- :unknown
219
- end
220
- end
221
-
222
- # Determine sfnt version for target format
223
- #
224
- # @param format [Symbol] Target format
225
- # @return [Integer] sfnt version
226
- def determine_sfnt_version(format)
227
- case format
228
- when :otf
229
- 0x4F54544F # 'OTTO' for OpenType/CFF
230
- when :ttf
231
- 0x00010000 # 1.0 for TrueType
232
- else
233
- 0x00010000 # Default to TrueType
203
+ "Supported: ttf, otf, svg, woff2"
234
204
  end
235
205
  end
236
206
 
@@ -247,45 +217,6 @@ module Fontisan
247
217
  "#{(bytes / (1024.0 * 1024)).round(1)} MB"
248
218
  end
249
219
  end
250
-
251
- # Handle conversion errors with helpful messages
252
- #
253
- # @param error [StandardError] The error that occurred
254
- # @raise [Error] Wrapped error with helpful message
255
- def handle_conversion_error(error)
256
- message = "Conversion failed: #{error.message}"
257
-
258
- # Add helpful hints based on error type
259
- if error.is_a?(NotImplementedError)
260
- message += "\n\nNote: Some conversions are not yet fully " \
261
- "implemented. Check the conversion matrix configuration " \
262
- "for implementation status."
263
- end
264
-
265
- raise Error, message
266
- end
267
-
268
- # Display optimization results from subroutine generation
269
- #
270
- # @param tables [Hash] Table data with optimization metadata
271
- def display_optimization_results(tables)
272
- optimization = tables.instance_variable_get(:@subroutine_optimization)
273
- return unless optimization
274
-
275
- puts "\n=== Subroutine Optimization Results ==="
276
- puts " Patterns found: #{optimization[:pattern_count]}"
277
- puts " Patterns selected: #{optimization[:selected_count]}"
278
- puts " Subroutines generated: #{optimization[:local_subrs].length}"
279
- puts " Estimated bytes saved: #{optimization[:savings]}"
280
- puts " CFF bias: #{optimization[:bias]}"
281
-
282
- if optimization[:selected_count].zero?
283
- puts " Note: No beneficial patterns found for optimization"
284
- elsif optimization[:savings].positive?
285
- savings_kb = (optimization[:savings] / 1024.0).round(1)
286
- puts " Estimated space savings: #{savings_kb} KB"
287
- end
288
- end
289
220
  end
290
221
  end
291
222
  end
@@ -41,12 +41,70 @@ module Fontisan
41
41
 
42
42
  # Get collection information
43
43
  #
44
- # @return [Models::CollectionInfo] Collection metadata
44
+ # @return [Models::CollectionInfo, Models::CollectionBriefInfo] Collection metadata
45
45
  def collection_info
46
46
  collection = FontLoader.load_collection(@font_path)
47
47
 
48
48
  File.open(@font_path, "rb") do |io|
49
- collection.collection_info(io, @font_path)
49
+ if @options[:brief]
50
+ # Brief mode: load each font and populate brief info
51
+ brief_info = Models::CollectionBriefInfo.new
52
+ brief_info.collection_path = @font_path
53
+ brief_info.num_fonts = collection.num_fonts
54
+ brief_info.fonts = []
55
+
56
+ collection.num_fonts.times do |index|
57
+ # Load individual font from collection
58
+ font = FontLoader.load(@font_path, font_index: index, mode: LoadingModes::METADATA)
59
+
60
+ # Populate brief info for this font
61
+ info = Models::FontInfo.new
62
+
63
+ # Font format and variable status
64
+ info.font_format = case font
65
+ when TrueTypeFont
66
+ "truetype"
67
+ when OpenTypeFont
68
+ "cff"
69
+ else
70
+ "unknown"
71
+ end
72
+ info.is_variable = font.has_table?(Constants::FVAR_TAG)
73
+
74
+ # Collection offset (only populated for fonts in collections)
75
+ info.collection_offset = collection.font_offsets[index]
76
+
77
+ # Essential names
78
+ if font.has_table?(Constants::NAME_TAG)
79
+ name_table = font.table(Constants::NAME_TAG)
80
+ info.family_name = name_table.english_name(Tables::Name::FAMILY)
81
+ info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
82
+ info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
83
+ info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
84
+ info.version = name_table.english_name(Tables::Name::VERSION)
85
+ end
86
+
87
+ # Essential metrics
88
+ if font.has_table?(Constants::HEAD_TAG)
89
+ head = font.table(Constants::HEAD_TAG)
90
+ info.font_revision = head.font_revision
91
+ info.units_per_em = head.units_per_em
92
+ end
93
+
94
+ # Vendor ID
95
+ if font.has_table?(Constants::OS2_TAG)
96
+ os2_table = font.table(Constants::OS2_TAG)
97
+ info.vendor_id = os2_table.vendor_id
98
+ end
99
+
100
+ brief_info.fonts << info
101
+ end
102
+
103
+ brief_info
104
+ else
105
+ # Full mode: show detailed sharing statistics
106
+ collection.collection_info(io, @font_path)
107
+ end
50
108
  end
51
109
  end
52
110
 
@@ -56,9 +114,14 @@ module Fontisan
56
114
  def font_info
57
115
  info = Models::FontInfo.new
58
116
  populate_font_format(info)
59
- populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
60
- populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
61
- populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
117
+
118
+ # In brief mode, only populate essential fields for fast identification
119
+ if @options[:brief]
120
+ populate_brief_fields(info)
121
+ else
122
+ populate_full_fields(info)
123
+ end
124
+
62
125
  info
63
126
  end
64
127
 
@@ -80,6 +143,49 @@ module Fontisan
80
143
  info.is_variable = font.has_table?(Constants::FVAR_TAG)
81
144
  end
82
145
 
146
+ # Populate essential fields for brief mode (metadata tables only).
147
+ #
148
+ # Brief mode provides fast font identification by loading only 13 essential
149
+ # attributes from metadata tables (name, head, OS/2). This is 5x faster than
150
+ # full mode and optimized for font indexing systems.
151
+ #
152
+ # @param info [Models::FontInfo] FontInfo instance to populate
153
+ def populate_brief_fields(info)
154
+ # Essential names from name table
155
+ if font.has_table?(Constants::NAME_TAG)
156
+ name_table = font.table(Constants::NAME_TAG)
157
+ info.family_name = name_table.english_name(Tables::Name::FAMILY)
158
+ info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
159
+ info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
160
+ info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
161
+ info.version = name_table.english_name(Tables::Name::VERSION)
162
+ end
163
+
164
+ # Essential metrics from head table
165
+ if font.has_table?(Constants::HEAD_TAG)
166
+ head = font.table(Constants::HEAD_TAG)
167
+ info.font_revision = head.font_revision
168
+ info.units_per_em = head.units_per_em
169
+ end
170
+
171
+ # Vendor ID from OS/2 table
172
+ if font.has_table?(Constants::OS2_TAG)
173
+ os2_table = font.table(Constants::OS2_TAG)
174
+ info.vendor_id = os2_table.vendor_id
175
+ end
176
+ end
177
+
178
+ # Populate all fields for full mode.
179
+ #
180
+ # Full mode extracts comprehensive metadata from all available tables.
181
+ #
182
+ # @param info [Models::FontInfo] FontInfo instance to populate
183
+ def populate_full_fields(info)
184
+ populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
185
+ populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
186
+ populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
187
+ end
188
+
83
189
  # Populate FontInfo from the name table.
84
190
  #
85
191
  # @param info [Models::FontInfo] FontInfo instance to populate