fontisan 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +103 -0
- data/.rubocop_todo.yml +106 -319
- data/Gemfile +1 -1
- data/README.adoc +81 -14
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- data/lib/fontisan/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- data/lib/fontisan/commands/convert_command.rb +2 -4
- data/lib/fontisan/commands/info_command.rb +3 -3
- data/lib/fontisan/commands/pack_command.rb +2 -1
- data/lib/fontisan/commands/validate_command.rb +157 -6
- data/lib/fontisan/converters/collection_converter.rb +22 -13
- data/lib/fontisan/converters/svg_generator.rb +2 -1
- data/lib/fontisan/converters/woff2_encoder.rb +6 -6
- data/lib/fontisan/converters/woff_writer.rb +3 -1
- data/lib/fontisan/font_loader.rb +7 -6
- data/lib/fontisan/formatters/text_formatter.rb +18 -14
- data/lib/fontisan/hints/hint_converter.rb +1 -1
- data/lib/fontisan/hints/hint_validator.rb +13 -10
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
- data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
- data/lib/fontisan/models/collection_validation_report.rb +104 -0
- data/lib/fontisan/models/font_report.rb +24 -0
- data/lib/fontisan/models/validation_report.rb +7 -2
- data/lib/fontisan/open_type_font.rb +2 -3
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
- data/lib/fontisan/subset/glyph_mapping.rb +2 -0
- data/lib/fontisan/subset/table_subsetter.rb +2 -2
- data/lib/fontisan/tables/cblc.rb +8 -4
- data/lib/fontisan/tables/cff/index.rb +2 -0
- data/lib/fontisan/tables/cff.rb +6 -3
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/cmap.rb +5 -5
- data/lib/fontisan/tables/glyf.rb +8 -10
- data/lib/fontisan/tables/head.rb +3 -3
- data/lib/fontisan/tables/hhea.rb +4 -4
- data/lib/fontisan/tables/maxp.rb +2 -2
- data/lib/fontisan/tables/name.rb +1 -1
- data/lib/fontisan/tables/os2.rb +8 -8
- data/lib/fontisan/tables/post.rb +2 -2
- data/lib/fontisan/tables/sbix.rb +5 -4
- data/lib/fontisan/true_type_font.rb +2 -3
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
- data/lib/fontisan/validation/collection_validator.rb +4 -2
- data/lib/fontisan/validators/basic_validator.rb +11 -21
- data/lib/fontisan/validators/font_book_validator.rb +29 -50
- data/lib/fontisan/validators/opentype_validator.rb +24 -28
- data/lib/fontisan/validators/validator.rb +87 -66
- data/lib/fontisan/validators/web_font_validator.rb +16 -21
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
- data/lib/fontisan/woff2/table_transformer.rb +4 -2
- data/lib/fontisan/woff2_font.rb +4 -2
- data/lib/fontisan/woff_font.rb +2 -2
- data/lib/fontisan.rb +2 -2
- data/scripts/compare_stack_aware.rb +1 -1
- data/scripts/measure_optimization.rb +1 -2
- metadata +4 -2
data/README.adoc
CHANGED
|
@@ -1034,26 +1034,19 @@ structures, usability, instructions, and glyphs.
|
|
|
1034
1034
|
|
|
1035
1035
|
=== Predefined profiles
|
|
1036
1036
|
|
|
1037
|
-
Fontisan includes
|
|
1037
|
+
Fontisan includes validation profiles for different use cases:
|
|
1038
1038
|
|
|
1039
|
-
`indexability`:: Fast
|
|
1040
|
-
BasicValidator with 8 essential checks. Loading mode: metadata.
|
|
1039
|
+
`indexability`:: Fast font discovery and indexing (~5x faster, 8 checks, metadata-only)
|
|
1041
1040
|
|
|
1042
|
-
`usability`::
|
|
1043
|
-
26 checks including macOS Font Book compatibility. Loading mode: full.
|
|
1041
|
+
`usability`:: Font installation compatibility (26 checks, macOS Font Book focused)
|
|
1044
1042
|
|
|
1045
|
-
`production`:: Comprehensive quality checks
|
|
1046
|
-
profile). Uses OpenTypeValidator with 36 checks for OpenType spec compliance.
|
|
1047
|
-
Loading mode: full.
|
|
1043
|
+
`production`:: Comprehensive production quality (36 checks, OpenType spec compliance - default)
|
|
1048
1044
|
|
|
1049
|
-
`web`:: Web font embedding
|
|
1050
|
-
with 18 checks for web deployment. Loading mode: full.
|
|
1045
|
+
`web`:: Web font embedding readiness (18 checks for web deployment)
|
|
1051
1046
|
|
|
1052
|
-
`spec_compliance`:: Full OpenType specification compliance
|
|
1053
|
-
Uses OpenTypeValidator with info-level severity for comprehensive analysis.
|
|
1054
|
-
Loading mode: full.
|
|
1047
|
+
`spec_compliance`:: Full OpenType specification compliance (detailed analysis mode)
|
|
1055
1048
|
|
|
1056
|
-
`default`:: Alias for the production profile
|
|
1049
|
+
`default`:: Alias for the production profile
|
|
1057
1050
|
|
|
1058
1051
|
|
|
1059
1052
|
=== Command-line usage
|
|
@@ -1154,6 +1147,80 @@ production, web, spec_compliance, default)
|
|
|
1154
1147
|
|
|
1155
1148
|
-e, --exclude CHECKS:: Exclude specific checks (comma-separated list)
|
|
1156
1149
|
|
|
1150
|
+
=== Collection validation
|
|
1151
|
+
|
|
1152
|
+
The `validate` command automatically detects and validates font collections (TTC, OTC, dfont).
|
|
1153
|
+
When validating a collection, fontisan validates all fonts in the collection and displays
|
|
1154
|
+
per-font results with an overall summary.
|
|
1155
|
+
|
|
1156
|
+
.Validation profiles with collections
|
|
1157
|
+
[example]
|
|
1158
|
+
====
|
|
1159
|
+
All validation profiles work with font collections. The selected profile determines:
|
|
1160
|
+
|
|
1161
|
+
* **Which tables are loaded** - metadata vs full mode
|
|
1162
|
+
* **Which checks are performed** - number and type of validations
|
|
1163
|
+
* **Performance characteristics** - indexability is ~5x faster than production
|
|
1164
|
+
|
|
1165
|
+
For large collections (CJK fonts with 30+ fonts), use the `indexability` profile for fast
|
|
1166
|
+
validation when scanning for font discovery, or `production` for comprehensive quality
|
|
1167
|
+
checks.
|
|
1168
|
+
|
|
1169
|
+
[source,shell]
|
|
1170
|
+
----
|
|
1171
|
+
# Quick validation for indexing (metadata mode, 8 checks, ~5x faster)
|
|
1172
|
+
$ fontisan validate /path/to/font.ttc -t indexability
|
|
1173
|
+
|
|
1174
|
+
# Comprehensive validation (full mode, 37 checks)
|
|
1175
|
+
$ fontisan validate /path/to/font.ttc -t production
|
|
1176
|
+
|
|
1177
|
+
# Web font readiness validation
|
|
1178
|
+
$ fontisan validate /path/to/font.ttc -t web
|
|
1179
|
+
----
|
|
1180
|
+
|
|
1181
|
+
The collection validation output includes:
|
|
1182
|
+
|
|
1183
|
+
* *Collection header* - Path, type, and number of fonts
|
|
1184
|
+
* *Summary* - Total errors, warnings, and info across all fonts
|
|
1185
|
+
* *Per-font sections* - Individual validation results for each font with:
|
|
1186
|
+
** Font index and name
|
|
1187
|
+
** Font path in `collection.ttc:index` format
|
|
1188
|
+
** Individual font status (VALID/INVALID/VALID_WITH_WARNINGS)
|
|
1189
|
+
** Font-specific errors and warnings
|
|
1190
|
+
** Exit codes* - For collections, uses the "worst" status across all fonts:
|
|
1191
|
+
** 0 = All fonts valid
|
|
1192
|
+
** 2 = Any font has fatal errors
|
|
1193
|
+
** 3 = Any font has errors (and no fatal)
|
|
1194
|
+
** 4 = Any font has warnings (and no errors)
|
|
1195
|
+
** 5 = Any font has info issues (and no errors or warnings)
|
|
1196
|
+
====
|
|
1197
|
+
|
|
1198
|
+
.Validate a font collection
|
|
1199
|
+
[example]
|
|
1200
|
+
====
|
|
1201
|
+
[source,shell]
|
|
1202
|
+
----
|
|
1203
|
+
$ fontisan validate /path/to/font.ttc
|
|
1204
|
+
Collection: /path/to/font.ttc
|
|
1205
|
+
Type: TTC
|
|
1206
|
+
Fonts: 4
|
|
1207
|
+
|
|
1208
|
+
Summary:
|
|
1209
|
+
Total Errors: 14
|
|
1210
|
+
Total Warnings: 8
|
|
1211
|
+
Total Info: 0
|
|
1212
|
+
|
|
1213
|
+
=== Font 0: Lucida Grande ===
|
|
1214
|
+
Font: /path/to/font.ttc:0
|
|
1215
|
+
Status: INVALID
|
|
1216
|
+
...
|
|
1217
|
+
|
|
1218
|
+
=== Font 1: Lucida Grande Bold ===
|
|
1219
|
+
Font: /path/to/font.ttc:1
|
|
1220
|
+
Status: INVALID
|
|
1221
|
+
...
|
|
1222
|
+
----
|
|
1223
|
+
====
|
|
1157
1224
|
|
|
1158
1225
|
=== Ruby API usage
|
|
1159
1226
|
|
data/Rakefile
CHANGED
|
@@ -8,6 +8,7 @@ require "rubocop/rake_task"
|
|
|
8
8
|
|
|
9
9
|
RuboCop::RakeTask.new
|
|
10
10
|
|
|
11
|
+
# rubocop:disable Metrics/BlockLength
|
|
11
12
|
namespace :fixtures do
|
|
12
13
|
# Load centralized fixture configuration
|
|
13
14
|
require_relative "spec/support/fixture_fonts"
|
|
@@ -99,8 +100,13 @@ namespace :fixtures do
|
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
# Compute download task prerequisites (marker files for non-skipped fonts)
|
|
104
|
+
download_prerequisites = fonts.values.reject do |config|
|
|
105
|
+
config[:skip_download]
|
|
106
|
+
end.map { |config| config[:marker] }
|
|
107
|
+
|
|
102
108
|
desc "Download all test fixture fonts"
|
|
103
|
-
task download:
|
|
109
|
+
task download: download_prerequisites
|
|
104
110
|
|
|
105
111
|
desc "Clean downloaded fixtures"
|
|
106
112
|
task :clean do
|
|
@@ -112,17 +118,16 @@ namespace :fixtures do
|
|
|
112
118
|
FileUtils.rm_f(config[:marker])
|
|
113
119
|
puts "[fixtures:clean] Removed #{config[:marker]}"
|
|
114
120
|
end
|
|
115
|
-
|
|
121
|
+
elsif File.exist?(config[:target_dir])
|
|
116
122
|
# For archives, delete the entire target directory
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
puts "[fixtures:clean] Removed #{config[:target_dir]}"
|
|
121
|
-
end
|
|
123
|
+
puts "[fixtures:clean] Removing #{config[:target_dir]}..."
|
|
124
|
+
FileUtils.rm_rf(config[:target_dir])
|
|
125
|
+
puts "[fixtures:clean] Removed #{config[:target_dir]}"
|
|
122
126
|
end
|
|
123
127
|
end
|
|
124
128
|
end
|
|
125
129
|
end
|
|
130
|
+
# rubocop:enable Metrics/BlockLength
|
|
126
131
|
|
|
127
132
|
# RSpec task depends on fixtures
|
|
128
133
|
RSpec::Core::RakeTask.new(spec: "fixtures:download")
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -367,18 +367,27 @@ module Fontisan
|
|
|
367
367
|
handle_error(e)
|
|
368
368
|
end
|
|
369
369
|
|
|
370
|
-
desc "validate FONT_FILE", "Validate font file"
|
|
370
|
+
desc "validate [FONT_FILE]", "Validate font file"
|
|
371
371
|
long_desc <<-DESC
|
|
372
372
|
Validate font file against quality checks and standards.
|
|
373
373
|
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
Supports individual fonts (TTF/OTF) and font collections (TTC/OTC/dfont).
|
|
375
|
+
For collections, validates each font and provides a summary report.
|
|
376
|
+
|
|
377
|
+
Profiles (-p/--profile):
|
|
378
|
+
indexability - Fast indexing validation (metadata only, 8 checks)
|
|
376
379
|
usability - Installation compatibility
|
|
377
380
|
production - Comprehensive quality (default)
|
|
378
381
|
web - Web font readiness
|
|
379
382
|
spec_compliance - OpenType spec compliance
|
|
380
383
|
default - Production profile (alias)
|
|
381
384
|
|
|
385
|
+
Collection validation with profiles:
|
|
386
|
+
When validating TTC/OTC/dfont collections, the selected profile
|
|
387
|
+
determines which tables are loaded and which checks are performed
|
|
388
|
+
for each font in the collection. Use 'indexability' for quick
|
|
389
|
+
validation or 'production' for comprehensive quality checks.
|
|
390
|
+
|
|
382
391
|
Return values (with -R/--return-value-results):
|
|
383
392
|
0 No results
|
|
384
393
|
1 Execution errors
|
|
@@ -389,25 +398,41 @@ module Fontisan
|
|
|
389
398
|
DESC
|
|
390
399
|
|
|
391
400
|
option :exclude, aliases: "-e", type: :array, desc: "Tests to exclude"
|
|
392
|
-
option :list, aliases: "-l", type: :boolean, desc: "List available
|
|
401
|
+
option :list, aliases: "-l", type: :boolean, desc: "List available profiles"
|
|
393
402
|
option :output, aliases: "-o", type: :string, desc: "Output file"
|
|
394
403
|
option :full_report, aliases: "-r", type: :boolean, desc: "Full report"
|
|
395
|
-
option :return_value_results, aliases: "-R", type: :boolean,
|
|
396
|
-
|
|
397
|
-
option :
|
|
404
|
+
option :return_value_results, aliases: "-R", type: :boolean,
|
|
405
|
+
desc: "Use return value for results"
|
|
406
|
+
option :summary_report, aliases: "-S", type: :boolean,
|
|
407
|
+
desc: "Summary report"
|
|
408
|
+
option :profile, aliases: "-p", type: :string, default: "default",
|
|
409
|
+
desc: "Validation profile"
|
|
398
410
|
option :table_report, aliases: "-T", type: :boolean, desc: "Tabular report"
|
|
399
411
|
option :verbose, aliases: "-v", type: :boolean, desc: "Verbose output"
|
|
400
|
-
option :suppress_warnings, aliases: "-W", type: :boolean,
|
|
412
|
+
option :suppress_warnings, aliases: "-W", type: :boolean,
|
|
413
|
+
desc: "Suppress warnings"
|
|
401
414
|
|
|
402
|
-
def validate(
|
|
415
|
+
def validate(*font_files)
|
|
403
416
|
if options[:list]
|
|
404
417
|
list_available_tests
|
|
405
418
|
return
|
|
406
419
|
end
|
|
407
420
|
|
|
421
|
+
# Validate argument count
|
|
422
|
+
if font_files.empty?
|
|
423
|
+
raise(Thor::Error, "FONT_FILE is required unless using --list")
|
|
424
|
+
elsif font_files.size > 1
|
|
425
|
+
raise(Thor::Error,
|
|
426
|
+
"Too many arguments. validate accepts only one font file.\n" \
|
|
427
|
+
"To validate multiple files, run validate separately for each.\n" \
|
|
428
|
+
"To validate all fonts in a collection, use: fontisan validate collection.ttc")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
font_file = font_files.first
|
|
432
|
+
|
|
408
433
|
cmd = Commands::ValidateCommand.new(
|
|
409
434
|
input: font_file,
|
|
410
|
-
profile: options[:
|
|
435
|
+
profile: options[:profile],
|
|
411
436
|
exclude: options[:exclude] || [],
|
|
412
437
|
output: options[:output],
|
|
413
438
|
format: options[:format].to_sym,
|
|
@@ -416,11 +441,18 @@ module Fontisan
|
|
|
416
441
|
table_report: options[:table_report],
|
|
417
442
|
verbose: options[:verbose],
|
|
418
443
|
suppress_warnings: options[:suppress_warnings],
|
|
419
|
-
return_value_results: options[:return_value_results]
|
|
444
|
+
return_value_results: options[:return_value_results],
|
|
420
445
|
)
|
|
421
446
|
|
|
422
447
|
exit cmd.run
|
|
423
|
-
rescue => e
|
|
448
|
+
rescue Thor::Error => e
|
|
449
|
+
unless options[:quiet]
|
|
450
|
+
warn "ERROR: #{e.message}"
|
|
451
|
+
warn
|
|
452
|
+
help("validate")
|
|
453
|
+
end
|
|
454
|
+
exit 1
|
|
455
|
+
rescue StandardError => e
|
|
424
456
|
error "Validation failed: #{e.message}"
|
|
425
457
|
exit 1
|
|
426
458
|
end
|
|
@@ -546,7 +578,7 @@ module Fontisan
|
|
|
546
578
|
puts " Format: #{result[:format].upcase}"
|
|
547
579
|
puts " Fonts: #{result[:num_fonts]}"
|
|
548
580
|
puts " Size: #{format_size(result[:output_size] || result[:total_size])}"
|
|
549
|
-
if result[:space_savings]
|
|
581
|
+
if result[:space_savings]&.positive?
|
|
550
582
|
puts " Space saved: #{format_size(result[:space_savings])}"
|
|
551
583
|
puts " Sharing: #{result[:statistics][:sharing_percentage]}%"
|
|
552
584
|
end
|
|
@@ -86,7 +86,8 @@ module Fontisan
|
|
|
86
86
|
map_size = 28 + 2 + 8 + (sfnt_binaries.size * 12)
|
|
87
87
|
|
|
88
88
|
# Step 4: Build resource map
|
|
89
|
-
resource_map = build_resource_map(sfnt_binaries,
|
|
89
|
+
resource_map = build_resource_map(sfnt_binaries,
|
|
90
|
+
resource_data.bytesize, map_size)
|
|
90
91
|
|
|
91
92
|
# Step 5: Build header
|
|
92
93
|
header = build_header(resource_data.bytesize, resource_map.bytesize)
|
|
@@ -73,7 +73,7 @@ module Fontisan
|
|
|
73
73
|
@instance_index = opts[:instance_index]
|
|
74
74
|
@preserve_variation = opts[:preserve_variation]
|
|
75
75
|
@preserve_hints = opts.fetch(:preserve_hints, false)
|
|
76
|
-
@collection_target_format = opts.fetch(:target_format,
|
|
76
|
+
@collection_target_format = opts.fetch(:target_format, "preserve").to_s
|
|
77
77
|
@validate = !opts[:no_validate]
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -196,7 +196,7 @@ module Fontisan
|
|
|
196
196
|
output: @output_path,
|
|
197
197
|
target_format: @collection_target_format,
|
|
198
198
|
verbose: @options[:verbose],
|
|
199
|
-
}
|
|
199
|
+
},
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
# Display results
|
|
@@ -245,8 +245,6 @@ module Fontisan
|
|
|
245
245
|
:otc
|
|
246
246
|
when ".dfont"
|
|
247
247
|
:dfont
|
|
248
|
-
else
|
|
249
|
-
nil
|
|
250
248
|
end
|
|
251
249
|
end
|
|
252
250
|
end
|
|
@@ -311,7 +311,7 @@ module Fontisan
|
|
|
311
311
|
start_glyph_id: strike_rec.start_glyph_index,
|
|
312
312
|
end_glyph_id: strike_rec.end_glyph_index,
|
|
313
313
|
bit_depth: strike_rec.bit_depth,
|
|
314
|
-
num_glyphs: strike_rec.glyph_range.size
|
|
314
|
+
num_glyphs: strike_rec.glyph_range.size,
|
|
315
315
|
)
|
|
316
316
|
end
|
|
317
317
|
formats << "PNG" # CBDT typically contains PNG data
|
|
@@ -329,8 +329,8 @@ module Fontisan
|
|
|
329
329
|
ppem: strike[:ppem],
|
|
330
330
|
start_glyph_id: 0,
|
|
331
331
|
end_glyph_id: strike[:num_glyphs] - 1,
|
|
332
|
-
bit_depth: 32,
|
|
333
|
-
num_glyphs: strike[:num_glyphs]
|
|
332
|
+
bit_depth: 32, # sbix is typically 32-bit
|
|
333
|
+
num_glyphs: strike[:num_glyphs],
|
|
334
334
|
)
|
|
335
335
|
end
|
|
336
336
|
end
|
|
@@ -260,7 +260,8 @@ module Fontisan
|
|
|
260
260
|
# @return [Symbol] Parsed format (:ttc, :otc, or :dfont)
|
|
261
261
|
# @raise [ArgumentError] if format is invalid
|
|
262
262
|
def parse_format(format)
|
|
263
|
-
return format if format.is_a?(Symbol) && %i[ttc otc
|
|
263
|
+
return format if format.is_a?(Symbol) && %i[ttc otc
|
|
264
|
+
dfont].include?(format)
|
|
264
265
|
|
|
265
266
|
case format.to_s.downcase
|
|
266
267
|
when "ttc"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "base_command"
|
|
4
4
|
require_relative "../validators/profile_loader"
|
|
5
5
|
require_relative "../font_loader"
|
|
6
|
+
require_relative "../tables/name"
|
|
6
7
|
|
|
7
8
|
module Fontisan
|
|
8
9
|
module Commands
|
|
@@ -70,12 +71,40 @@ module Fontisan
|
|
|
70
71
|
# Load font with appropriate mode
|
|
71
72
|
profile_config = Validators::ProfileLoader.profile_info(@profile)
|
|
72
73
|
unless profile_config
|
|
73
|
-
|
|
74
|
+
unless @suppress_warnings
|
|
75
|
+
puts "Error: Unknown profile '#{@profile}'"
|
|
76
|
+
puts ""
|
|
77
|
+
puts "Available profiles:"
|
|
78
|
+
Validators::ProfileLoader.all_profiles.each do |name, config|
|
|
79
|
+
puts " #{name.to_s.ljust(20)} - #{config[:description]}"
|
|
80
|
+
end
|
|
81
|
+
puts ""
|
|
82
|
+
puts "Use --list to see all available profiles"
|
|
83
|
+
end
|
|
74
84
|
return 1
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
mode = profile_config[:loading_mode].to_sym
|
|
78
88
|
|
|
89
|
+
# Check if input is a collection
|
|
90
|
+
if FontLoader.collection?(@input)
|
|
91
|
+
validate_collection(mode)
|
|
92
|
+
else
|
|
93
|
+
validate_single_font(mode)
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
puts "Error: #{e.message}" unless @suppress_warnings
|
|
97
|
+
puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
|
|
98
|
+
1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Validate a single font file
|
|
104
|
+
#
|
|
105
|
+
# @param mode [Symbol] Loading mode
|
|
106
|
+
# @return [Integer] Exit code
|
|
107
|
+
def validate_single_font(mode)
|
|
79
108
|
font = FontLoader.load(@input, mode: mode)
|
|
80
109
|
|
|
81
110
|
# Select validator
|
|
@@ -102,13 +131,134 @@ module Fontisan
|
|
|
102
131
|
|
|
103
132
|
# Return exit code
|
|
104
133
|
exit_code(report)
|
|
105
|
-
rescue => e
|
|
106
|
-
puts "Error: #{e.message}" unless @suppress_warnings
|
|
107
|
-
puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
|
|
108
|
-
1
|
|
109
134
|
end
|
|
110
135
|
|
|
111
|
-
|
|
136
|
+
# Validate all fonts in a collection
|
|
137
|
+
#
|
|
138
|
+
# @param mode [Symbol] Loading mode
|
|
139
|
+
# @return [Integer] Exit code
|
|
140
|
+
def validate_collection(mode)
|
|
141
|
+
require_relative "../models/collection_validation_report"
|
|
142
|
+
require_relative "../models/font_report"
|
|
143
|
+
|
|
144
|
+
# Load collection metadata
|
|
145
|
+
collection = FontLoader.load_collection(@input)
|
|
146
|
+
|
|
147
|
+
# Create collection report
|
|
148
|
+
collection_report = Models::CollectionValidationReport.new(
|
|
149
|
+
collection_path: @input,
|
|
150
|
+
collection_type: collection.class.collection_format,
|
|
151
|
+
num_fonts: collection.num_fonts,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Get validator
|
|
155
|
+
validator = Validators::ProfileLoader.load(@profile)
|
|
156
|
+
|
|
157
|
+
# Validate each font
|
|
158
|
+
collection.num_fonts.times do |index|
|
|
159
|
+
font = FontLoader.load(@input, font_index: index, mode: mode)
|
|
160
|
+
font_report = validator.validate(font)
|
|
161
|
+
|
|
162
|
+
# Set font_path to indicate collection file with index
|
|
163
|
+
# Fonts in collections don't have individual file paths
|
|
164
|
+
font_report.font_path = "#{@input}:#{index}"
|
|
165
|
+
|
|
166
|
+
# Extract font name
|
|
167
|
+
font_name = extract_font_name(font, index)
|
|
168
|
+
|
|
169
|
+
# Create and add font report
|
|
170
|
+
collection_report.add_font_report(
|
|
171
|
+
Models::FontReport.new(
|
|
172
|
+
font_index: index,
|
|
173
|
+
font_name: font_name,
|
|
174
|
+
report: font_report,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
# Create error report for failed font loading
|
|
179
|
+
# Use collection file path with index for fonts in collections
|
|
180
|
+
error_report = Models::ValidationReport.new(
|
|
181
|
+
font_path: "#{@input}:#{index}",
|
|
182
|
+
valid: false,
|
|
183
|
+
)
|
|
184
|
+
error_report.add_error("font_loading",
|
|
185
|
+
"Failed to load font #{index}: #{e.message}", nil)
|
|
186
|
+
|
|
187
|
+
collection_report.add_font_report(
|
|
188
|
+
Models::FontReport.new(
|
|
189
|
+
font_index: index,
|
|
190
|
+
font_name: "Font #{index}",
|
|
191
|
+
report: error_report,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Generate output based on format
|
|
197
|
+
output = case @format
|
|
198
|
+
when :yaml
|
|
199
|
+
collection_report.to_yaml
|
|
200
|
+
when :json
|
|
201
|
+
collection_report.to_json
|
|
202
|
+
else
|
|
203
|
+
collection_report.text_summary
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Write to file or stdout
|
|
207
|
+
if @output
|
|
208
|
+
File.write(@output, output)
|
|
209
|
+
puts "Validation report written to #{@output}" if @verbose && !@suppress_warnings
|
|
210
|
+
else
|
|
211
|
+
puts output unless @suppress_warnings
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Return exit code based on worst status
|
|
215
|
+
collection_exit_code(collection_report)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Extract font name from font object
|
|
219
|
+
#
|
|
220
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font object
|
|
221
|
+
# @param index [Integer] Font index (fallback)
|
|
222
|
+
# @return [String] Font name
|
|
223
|
+
def extract_font_name(font, index)
|
|
224
|
+
return "Font #{index}" unless font.respond_to?(:table)
|
|
225
|
+
|
|
226
|
+
name_table = font.table("name")
|
|
227
|
+
return "Font #{index}" unless name_table
|
|
228
|
+
|
|
229
|
+
full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
230
|
+
return full_name if full_name && !full_name.empty?
|
|
231
|
+
|
|
232
|
+
postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
233
|
+
return postscript_name if postscript_name && !postscript_name.empty?
|
|
234
|
+
|
|
235
|
+
"Font #{index}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Calculate exit code for collection validation
|
|
239
|
+
#
|
|
240
|
+
# Uses worst status across all fonts
|
|
241
|
+
#
|
|
242
|
+
# @param report [CollectionValidationReport] Collection report
|
|
243
|
+
# @return [Integer] Exit code
|
|
244
|
+
def collection_exit_code(report)
|
|
245
|
+
return 0 unless @return_value_results
|
|
246
|
+
|
|
247
|
+
# Check for fatal errors first
|
|
248
|
+
return 2 if report.font_reports.any? do |fr|
|
|
249
|
+
fr.report.fatal_errors.any?
|
|
250
|
+
end
|
|
251
|
+
# Then check for errors
|
|
252
|
+
return 3 if report.font_reports.any? { |fr| fr.report.errors_only.any? }
|
|
253
|
+
# Then check for warnings
|
|
254
|
+
return 4 if report.font_reports.any? do |fr|
|
|
255
|
+
fr.report.warnings_only.any?
|
|
256
|
+
end
|
|
257
|
+
# Then check for info
|
|
258
|
+
return 5 if report.font_reports.any? { |fr| fr.report.info_only.any? }
|
|
259
|
+
|
|
260
|
+
0
|
|
261
|
+
end
|
|
112
262
|
|
|
113
263
|
# Generate output based on requested format
|
|
114
264
|
#
|
|
@@ -155,6 +305,7 @@ module Fontisan
|
|
|
155
305
|
return 3 if report.errors_only.any?
|
|
156
306
|
return 4 if report.warnings_only.any?
|
|
157
307
|
return 5 if report.info_only.any?
|
|
308
|
+
|
|
158
309
|
0
|
|
159
310
|
end
|
|
160
311
|
end
|
|
@@ -59,11 +59,12 @@ module Fontisan
|
|
|
59
59
|
|
|
60
60
|
verbose = options.fetch(:verbose, false)
|
|
61
61
|
output_path = options[:output]
|
|
62
|
-
target_format = options.fetch(:target_format,
|
|
62
|
+
target_format = options.fetch(:target_format, "preserve").to_s
|
|
63
63
|
|
|
64
64
|
# Validate target_format
|
|
65
65
|
unless %w[preserve ttf otf].include?(target_format)
|
|
66
|
-
raise ArgumentError,
|
|
66
|
+
raise ArgumentError,
|
|
67
|
+
"Invalid target_format: #{target_format}. Must be 'preserve', 'ttf', or 'otf'"
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
puts "Converting collection to #{target_type.to_s.upcase}..." if verbose
|
|
@@ -76,19 +77,22 @@ module Fontisan
|
|
|
76
77
|
if source_type == target_type
|
|
77
78
|
puts " Source and target formats are the same, copying collection..." if verbose
|
|
78
79
|
FileUtils.cp(collection_path, output_path)
|
|
79
|
-
return build_result(collection_path, output_path, source_type,
|
|
80
|
+
return build_result(collection_path, output_path, source_type,
|
|
81
|
+
target_type, fonts.size, [])
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
# Step 2: Convert - transform fonts if requested
|
|
83
85
|
puts " Converting #{fonts.size} font(s)..." if verbose
|
|
84
|
-
converted_fonts, conversions = convert_fonts(fonts, source_type,
|
|
86
|
+
converted_fonts, conversions = convert_fonts(fonts, source_type,
|
|
87
|
+
target_type, options.merge(target_format: target_format))
|
|
85
88
|
|
|
86
89
|
# Step 3: Repack - build target collection
|
|
87
90
|
puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
|
|
88
91
|
repack_fonts(converted_fonts, target_type, output_path, options)
|
|
89
92
|
|
|
90
93
|
# Build result
|
|
91
|
-
result = build_result(collection_path, output_path, source_type,
|
|
94
|
+
result = build_result(collection_path, output_path, source_type,
|
|
95
|
+
target_type, fonts.size, conversions)
|
|
92
96
|
|
|
93
97
|
if verbose
|
|
94
98
|
display_result(result)
|
|
@@ -111,7 +115,8 @@ module Fontisan
|
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
unless %i[ttc otc dfont].include?(target_type)
|
|
114
|
-
raise ArgumentError,
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"Invalid target type: #{target_type}. Must be :ttc, :otc, or :dfont"
|
|
115
120
|
end
|
|
116
121
|
|
|
117
122
|
unless options[:output]
|
|
@@ -202,20 +207,21 @@ module Fontisan
|
|
|
202
207
|
# @param target_type [Symbol] Target collection type
|
|
203
208
|
# @param options [Hash] Conversion options
|
|
204
209
|
# @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
|
|
205
|
-
def convert_fonts(fonts,
|
|
210
|
+
def convert_fonts(fonts, _source_type, target_type, options)
|
|
206
211
|
converted_fonts = []
|
|
207
212
|
conversions = []
|
|
208
213
|
|
|
209
214
|
# Determine if outline conversion is needed
|
|
210
|
-
target_format = options.fetch(:target_format,
|
|
215
|
+
target_format = options.fetch(:target_format, "preserve").to_s
|
|
211
216
|
|
|
212
217
|
fonts.each_with_index do |font, index|
|
|
213
218
|
source_format = detect_font_format(font)
|
|
214
|
-
needs_conversion = outline_conversion_needed?(source_format,
|
|
219
|
+
needs_conversion = outline_conversion_needed?(source_format,
|
|
220
|
+
target_format)
|
|
215
221
|
|
|
216
222
|
if needs_conversion
|
|
217
223
|
# Convert outline format
|
|
218
|
-
desired_format = target_format ==
|
|
224
|
+
desired_format = target_format == "preserve" ? source_format : target_format.to_sym
|
|
219
225
|
converter = FormatConverter.new
|
|
220
226
|
|
|
221
227
|
begin
|
|
@@ -266,7 +272,7 @@ module Fontisan
|
|
|
266
272
|
# @return [Boolean] true if conversion needed
|
|
267
273
|
def outline_conversion_needed?(source_format, target_format)
|
|
268
274
|
# 'preserve' means keep original format
|
|
269
|
-
return false if target_format ==
|
|
275
|
+
return false if target_format == "preserve"
|
|
270
276
|
|
|
271
277
|
# Convert if target format differs from source
|
|
272
278
|
target_format.to_sym != source_format
|
|
@@ -401,7 +407,8 @@ module Fontisan
|
|
|
401
407
|
# @param num_fonts [Integer] Number of fonts
|
|
402
408
|
# @param conversions [Array<Hash>] Conversion details
|
|
403
409
|
# @return [Hash] Result
|
|
404
|
-
def build_result(input, output, source_type, target_type, num_fonts,
|
|
410
|
+
def build_result(input, output, source_type, target_type, num_fonts,
|
|
411
|
+
conversions)
|
|
405
412
|
{
|
|
406
413
|
input: input,
|
|
407
414
|
output: output,
|
|
@@ -425,7 +432,9 @@ module Fontisan
|
|
|
425
432
|
puts "Fonts: #{result[:num_fonts]}"
|
|
426
433
|
|
|
427
434
|
if result[:conversions].any?
|
|
428
|
-
converted_count = result[:conversions].count
|
|
435
|
+
converted_count = result[:conversions].count do |c|
|
|
436
|
+
c[:status] == :converted
|
|
437
|
+
end
|
|
429
438
|
if converted_count.positive?
|
|
430
439
|
puts "Outline conversions: #{converted_count}"
|
|
431
440
|
end
|
|
@@ -107,7 +107,8 @@ module Fontisan
|
|
|
107
107
|
# @param extractor [OutlineExtractor] Outline extractor for layer glyphs
|
|
108
108
|
# @param palette_index [Integer] Palette index to use (default: 0)
|
|
109
109
|
# @return [String, nil] SVG path data with color layers, or nil if not color glyph
|
|
110
|
-
def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor,
|
|
110
|
+
def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor,
|
|
111
|
+
palette_index: 0)
|
|
111
112
|
# Get layers for this glyph
|
|
112
113
|
layers = colr_table.layers_for_glyph(glyph_id)
|
|
113
114
|
return nil if layers.empty?
|