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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_command"
|
|
4
|
-
require_relative "../
|
|
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
|
|
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
|
|
15
|
-
# -
|
|
14
|
+
# - TTF ↔ OTF outline format conversion
|
|
15
|
+
# - Variable font operations (preserve/instance generation)
|
|
16
|
+
# - WOFF/WOFF2 compression
|
|
16
17
|
#
|
|
17
|
-
# The command uses [`
|
|
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
|
|
29
|
+
# @example Generate instance at coordinates
|
|
29
30
|
# command = ConvertCommand.new(
|
|
30
|
-
# '
|
|
31
|
+
# 'variable.ttf',
|
|
31
32
|
# to: 'ttf',
|
|
32
|
-
# output: '
|
|
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,
|
|
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 [
|
|
44
|
-
# @option options [
|
|
45
|
-
# @option options [Integer] :
|
|
46
|
-
# @option options [Boolean] :
|
|
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
|
-
#
|
|
54
|
-
@
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@
|
|
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
|
|
71
|
-
|
|
82
|
+
# Build pipeline options
|
|
83
|
+
pipeline_options = {
|
|
72
84
|
target_format: @target_format,
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
#
|
|
81
|
-
|
|
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
|
-
#
|
|
84
|
-
|
|
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
|
-
|
|
96
|
-
|
|
100
|
+
# Use TransformationPipeline for universal conversion
|
|
101
|
+
pipeline = Pipeline::TransformationPipeline.new(
|
|
102
|
+
font_path,
|
|
103
|
+
@output_path,
|
|
104
|
+
pipeline_options,
|
|
105
|
+
)
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
FontWriter.write_to_file(tables, @output_path,
|
|
100
|
-
sfnt_version: sfnt_version)
|
|
107
|
+
result = pipeline.transform
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
# Display results
|
|
110
|
+
unless @options[:quiet]
|
|
111
|
+
output_size = File.size(@output_path)
|
|
112
|
+
input_size = File.size(font_path)
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
118
|
-
target_format:
|
|
119
|
-
input_size:
|
|
120
|
-
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
|
-
|
|
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
|
-
#
|
|
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
|
|
145
|
-
# @
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|