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
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
|
|
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
|
|
32
|
-
def info(
|
|
33
|
-
command = Commands::InfoCommand.new(
|
|
34
|
-
|
|
35
|
-
output_result(
|
|
36
|
-
rescue Errno::ENOENT
|
|
37
|
-
|
|
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,
|
|
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 :
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
option :
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
# -
|
|
215
|
-
# -
|
|
216
|
-
# -
|
|
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
|
-
#
|
|
219
|
-
#
|
|
220
|
-
# to
|
|
221
|
-
#
|
|
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
|
|
229
|
-
# fontisan convert
|
|
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
|
|
232
|
-
# fontisan convert
|
|
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
|
|
236
|
-
# fontisan convert font.ttf --to
|
|
268
|
+
# @example Convert without validation
|
|
269
|
+
# fontisan convert font.ttf --to otf --output font.otf --no-validate
|
|
237
270
|
def convert(font_file)
|
|
238
|
-
|
|
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,
|
|
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:
|
|
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
|