fontisan 0.2.3 → 0.2.5
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 +221 -49
- data/README.adoc +519 -5
- data/Rakefile +20 -7
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/base_command.rb +2 -19
- data/lib/fontisan/commands/convert_command.rb +16 -13
- data/lib/fontisan/commands/info_command.rb +88 -0
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +58 -20
- data/lib/fontisan/converters/outline_converter.rb +6 -3
- data/lib/fontisan/converters/svg_generator.rb +45 -0
- data/lib/fontisan/converters/woff2_encoder.rb +84 -13
- data/lib/fontisan/models/bitmap_glyph.rb +123 -0
- data/lib/fontisan/models/bitmap_strike.rb +94 -0
- data/lib/fontisan/models/color_glyph.rb +57 -0
- data/lib/fontisan/models/color_layer.rb +53 -0
- data/lib/fontisan/models/color_palette.rb +60 -0
- data/lib/fontisan/models/font_info.rb +26 -0
- data/lib/fontisan/models/svg_glyph.rb +89 -0
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/open_type_font.rb +6 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
- data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
- data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
- data/lib/fontisan/pipeline/output_writer.rb +2 -2
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cbdt.rb +169 -0
- data/lib/fontisan/tables/cblc.rb +290 -0
- data/lib/fontisan/tables/cff.rb +6 -12
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/colr.rb +291 -0
- data/lib/fontisan/tables/cpal.rb +281 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/tables/sbix.rb +379 -0
- data/lib/fontisan/tables/svg.rb +301 -0
- data/lib/fontisan/true_type_font.rb +6 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +40 -11
- data/lib/fontisan/woff2/table_transformer.rb +506 -73
- data/lib/fontisan/woff2_font.rb +29 -9
- data/lib/fontisan/woff_font.rb +17 -4
- data/lib/fontisan.rb +90 -6
- metadata +20 -9
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
|
@@ -58,30 +58,13 @@ module Fontisan
|
|
|
58
58
|
# Load the font using FontLoader.
|
|
59
59
|
#
|
|
60
60
|
# Uses FontLoader for automatic format detection and loading.
|
|
61
|
-
# Returns
|
|
61
|
+
# Returns TrueTypeFont, OpenTypeFont, WoffFont, or Woff2Font depending on file format.
|
|
62
62
|
#
|
|
63
|
-
# @return [TrueTypeFont, OpenTypeFont] The loaded font
|
|
63
|
+
# @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font
|
|
64
64
|
# @raise [Errno::ENOENT] if file does not exist
|
|
65
|
-
# @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
|
|
66
65
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
67
66
|
# @raise [Error] for other loading failures
|
|
68
67
|
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
68
|
# Brief mode uses metadata loading for 5x faster parsing
|
|
86
69
|
mode = @options[:brief] ? LoadingModes::METADATA : (@options[:mode] || LoadingModes::FULL)
|
|
87
70
|
|
|
@@ -51,22 +51,26 @@ module Fontisan
|
|
|
51
51
|
# @option options [Boolean] :verbose Verbose output
|
|
52
52
|
def initialize(font_path, options = {})
|
|
53
53
|
super(font_path, options)
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
# Convert string keys to symbols for Thor compatibility
|
|
56
|
+
opts = options.transform_keys(&:to_sym)
|
|
57
|
+
|
|
58
|
+
@output_path = opts[:output]
|
|
55
59
|
|
|
56
60
|
# Parse target format
|
|
57
|
-
@target_format = parse_target_format(
|
|
61
|
+
@target_format = parse_target_format(opts[:to])
|
|
58
62
|
|
|
59
63
|
# Parse coordinates if string provided
|
|
60
|
-
@coordinates = if
|
|
61
|
-
parse_coordinates(
|
|
62
|
-
elsif
|
|
63
|
-
|
|
64
|
+
@coordinates = if opts[:coordinates]
|
|
65
|
+
parse_coordinates(opts[:coordinates])
|
|
66
|
+
elsif opts[:instance_coordinates]
|
|
67
|
+
opts[:instance_coordinates]
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
@instance_index =
|
|
67
|
-
@preserve_variation =
|
|
68
|
-
@preserve_hints =
|
|
69
|
-
@validate = !
|
|
70
|
+
@instance_index = opts[:instance_index]
|
|
71
|
+
@preserve_variation = opts[:preserve_variation]
|
|
72
|
+
@preserve_hints = opts.fetch(:preserve_hints, false)
|
|
73
|
+
@validate = !opts[:no_validate]
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
# Execute the conversion
|
|
@@ -193,14 +197,13 @@ module Fontisan
|
|
|
193
197
|
when "svg"
|
|
194
198
|
:svg
|
|
195
199
|
when "woff"
|
|
196
|
-
|
|
197
|
-
"WOFF format conversion is not supported yet. Use woff2 instead."
|
|
200
|
+
:woff
|
|
198
201
|
when "woff2"
|
|
199
202
|
:woff2
|
|
200
203
|
else
|
|
201
204
|
raise ArgumentError,
|
|
202
205
|
"Unknown target format: #{format}. " \
|
|
203
|
-
"Supported: ttf, otf, svg, woff2"
|
|
206
|
+
"Supported: ttf, otf, svg, woff, woff2"
|
|
204
207
|
end
|
|
205
208
|
end
|
|
206
209
|
|
|
@@ -202,6 +202,9 @@ module Fontisan
|
|
|
202
202
|
populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
|
|
203
203
|
populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
|
|
204
204
|
populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
|
|
205
|
+
populate_color_info(info) if font.has_table?("COLR") && font.has_table?("CPAL")
|
|
206
|
+
populate_svg_info(info) if font.has_table?("SVG ")
|
|
207
|
+
populate_bitmap_info(info) if font.has_table?("CBLC") || font.has_table?("sbix")
|
|
205
208
|
end
|
|
206
209
|
|
|
207
210
|
# Populate FontInfo from the name table.
|
|
@@ -255,6 +258,91 @@ module Fontisan
|
|
|
255
258
|
info.units_per_em = head_table.units_per_em
|
|
256
259
|
end
|
|
257
260
|
|
|
261
|
+
# Populate FontInfo with color font information from COLR/CPAL tables
|
|
262
|
+
#
|
|
263
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
264
|
+
def populate_color_info(info)
|
|
265
|
+
colr_table = font.table("COLR")
|
|
266
|
+
cpal_table = font.table("CPAL")
|
|
267
|
+
|
|
268
|
+
return unless colr_table && cpal_table
|
|
269
|
+
|
|
270
|
+
info.is_color_font = true
|
|
271
|
+
info.color_glyphs = colr_table.num_color_glyphs
|
|
272
|
+
info.color_palettes = cpal_table.num_palettes
|
|
273
|
+
info.colors_per_palette = cpal_table.num_palette_entries
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
warn "Failed to populate color font info: #{e.message}"
|
|
276
|
+
info.is_color_font = false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Populate FontInfo with SVG table information
|
|
280
|
+
#
|
|
281
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
282
|
+
def populate_svg_info(info)
|
|
283
|
+
svg_table = font.table("SVG ")
|
|
284
|
+
|
|
285
|
+
return unless svg_table
|
|
286
|
+
|
|
287
|
+
info.has_svg_table = true
|
|
288
|
+
info.svg_glyph_count = svg_table.glyph_ids_with_svg.length
|
|
289
|
+
rescue StandardError => e
|
|
290
|
+
warn "Failed to populate SVG info: #{e.message}"
|
|
291
|
+
info.has_svg_table = false
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Populate FontInfo with bitmap table information (CBDT/CBLC, sbix)
|
|
295
|
+
#
|
|
296
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
297
|
+
def populate_bitmap_info(info)
|
|
298
|
+
bitmap_strikes = []
|
|
299
|
+
ppem_sizes = []
|
|
300
|
+
formats = []
|
|
301
|
+
|
|
302
|
+
# Check for CBDT/CBLC (Google format)
|
|
303
|
+
if font.has_table?("CBLC") && font.has_table?("CBDT")
|
|
304
|
+
cblc = font.table("CBLC")
|
|
305
|
+
info.has_bitmap_glyphs = true
|
|
306
|
+
ppem_sizes.concat(cblc.ppem_sizes)
|
|
307
|
+
|
|
308
|
+
cblc.strikes.each do |strike_rec|
|
|
309
|
+
bitmap_strikes << Models::BitmapStrike.new(
|
|
310
|
+
ppem: strike_rec.ppem,
|
|
311
|
+
start_glyph_id: strike_rec.start_glyph_index,
|
|
312
|
+
end_glyph_id: strike_rec.end_glyph_index,
|
|
313
|
+
bit_depth: strike_rec.bit_depth,
|
|
314
|
+
num_glyphs: strike_rec.glyph_range.size
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
formats << "PNG" # CBDT typically contains PNG data
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Check for sbix (Apple format)
|
|
321
|
+
if font.has_table?("sbix")
|
|
322
|
+
sbix = font.table("sbix")
|
|
323
|
+
info.has_bitmap_glyphs = true
|
|
324
|
+
ppem_sizes.concat(sbix.ppem_sizes)
|
|
325
|
+
formats.concat(sbix.supported_formats)
|
|
326
|
+
|
|
327
|
+
sbix.strikes.each do |strike|
|
|
328
|
+
bitmap_strikes << Models::BitmapStrike.new(
|
|
329
|
+
ppem: strike[:ppem],
|
|
330
|
+
start_glyph_id: 0,
|
|
331
|
+
end_glyph_id: strike[:num_glyphs] - 1,
|
|
332
|
+
bit_depth: 32, # sbix is typically 32-bit
|
|
333
|
+
num_glyphs: strike[:num_glyphs]
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
info.bitmap_strikes = bitmap_strikes unless bitmap_strikes.empty?
|
|
339
|
+
info.bitmap_ppem_sizes = ppem_sizes.uniq.sort
|
|
340
|
+
info.bitmap_formats = formats.uniq
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
warn "Failed to populate bitmap info: #{e.message}"
|
|
343
|
+
info.has_bitmap_glyphs = false
|
|
344
|
+
end
|
|
345
|
+
|
|
258
346
|
# Format OS/2 embedding permission flags into a human-readable string.
|
|
259
347
|
#
|
|
260
348
|
# @param flags [Integer] OS/2 fsType flags
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_command"
|
|
4
|
-
require_relative "../
|
|
5
|
-
require_relative "../validation/variable_font_validator"
|
|
4
|
+
require_relative "../validators/profile_loader"
|
|
6
5
|
require_relative "../font_loader"
|
|
7
6
|
|
|
8
7
|
module Fontisan
|
|
@@ -11,195 +10,152 @@ module Fontisan
|
|
|
11
10
|
#
|
|
12
11
|
# This command validates fonts against quality checks, structural integrity,
|
|
13
12
|
# and OpenType specification compliance. It supports different validation
|
|
14
|
-
#
|
|
13
|
+
# profiles and output formats, with ftxvalidator-compatible options.
|
|
15
14
|
#
|
|
16
|
-
# @example Validating a font
|
|
15
|
+
# @example Validating a font with default profile
|
|
16
|
+
# command = ValidateCommand.new(input: "font.ttf")
|
|
17
|
+
# exit_code = command.run
|
|
18
|
+
#
|
|
19
|
+
# @example Validating with specific profile
|
|
17
20
|
# command = ValidateCommand.new(
|
|
18
21
|
# input: "font.ttf",
|
|
19
|
-
#
|
|
20
|
-
# format: :
|
|
22
|
+
# profile: :web,
|
|
23
|
+
# format: :json
|
|
21
24
|
# )
|
|
22
25
|
# exit_code = command.run
|
|
23
26
|
class ValidateCommand < BaseCommand
|
|
24
27
|
# Initialize validate command
|
|
25
28
|
#
|
|
26
29
|
# @param input [String] Path to font file
|
|
27
|
-
# @param
|
|
30
|
+
# @param profile [Symbol, String, nil] Validation profile (default: :default)
|
|
31
|
+
# @param exclude [Array<String>] Tests to exclude
|
|
32
|
+
# @param output [String, nil] Output file path
|
|
28
33
|
# @param format [Symbol] Output format (:text, :yaml, :json)
|
|
29
|
-
# @param
|
|
30
|
-
# @param
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
# @param full_report [Boolean] Generate full detailed report
|
|
35
|
+
# @param summary_report [Boolean] Generate brief summary report
|
|
36
|
+
# @param table_report [Boolean] Generate tabular format report
|
|
37
|
+
# @param verbose [Boolean] Show verbose output
|
|
38
|
+
# @param suppress_warnings [Boolean] Suppress warning output
|
|
39
|
+
# @param return_value_results [Boolean] Use return values to indicate results
|
|
40
|
+
def initialize(
|
|
41
|
+
input:,
|
|
42
|
+
profile: nil,
|
|
43
|
+
exclude: [],
|
|
44
|
+
output: nil,
|
|
45
|
+
format: :text,
|
|
46
|
+
full_report: false,
|
|
47
|
+
summary_report: false,
|
|
48
|
+
table_report: false,
|
|
49
|
+
verbose: false,
|
|
50
|
+
suppress_warnings: false,
|
|
51
|
+
return_value_results: false
|
|
52
|
+
)
|
|
34
53
|
@input = input
|
|
35
|
-
@
|
|
36
|
-
@
|
|
54
|
+
@profile = profile || :default
|
|
55
|
+
@exclude = exclude
|
|
56
|
+
@output = output
|
|
57
|
+
@format = format
|
|
58
|
+
@full_report = full_report
|
|
59
|
+
@summary_report = summary_report
|
|
60
|
+
@table_report = table_report
|
|
37
61
|
@verbose = verbose
|
|
38
|
-
@
|
|
62
|
+
@suppress_warnings = suppress_warnings
|
|
63
|
+
@return_value_results = return_value_results
|
|
39
64
|
end
|
|
40
65
|
|
|
41
66
|
# Run the validation command
|
|
42
67
|
#
|
|
43
|
-
# @return [Integer] Exit code (0 = valid,
|
|
68
|
+
# @return [Integer] Exit code (0 = valid, 2 = fatal, 3 = errors, 4 = warnings, 5 = info)
|
|
44
69
|
def run
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Create validator
|
|
52
|
-
validator = Validation::Validator.new(level: @level)
|
|
53
|
-
|
|
54
|
-
# Run validation
|
|
55
|
-
report = validator.validate(font, @input)
|
|
70
|
+
# Load font with appropriate mode
|
|
71
|
+
profile_config = Validators::ProfileLoader.profile_info(@profile)
|
|
72
|
+
unless profile_config
|
|
73
|
+
puts "Error: Unknown profile '#{@profile}'" unless @suppress_warnings
|
|
74
|
+
return 1
|
|
75
|
+
end
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
validate_variable_font(font, report) if font.has_table?("fvar")
|
|
77
|
+
mode = profile_config[:loading_mode].to_sym
|
|
59
78
|
|
|
60
|
-
|
|
61
|
-
output_report(report) unless @quiet
|
|
79
|
+
font = FontLoader.load(@input, mode: mode)
|
|
62
80
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
rescue StandardError => e
|
|
66
|
-
puts "Error: #{e.message}" unless @quiet
|
|
67
|
-
puts e.backtrace.join("\n") if @verbose && !@quiet
|
|
68
|
-
1
|
|
69
|
-
end
|
|
81
|
+
# Select validator
|
|
82
|
+
validator = Validators::ProfileLoader.load(@profile)
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Validate variable font structure
|
|
74
|
-
#
|
|
75
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
76
|
-
# @param report [Models::ValidationReport] The validation report to update
|
|
77
|
-
# @return [void]
|
|
78
|
-
def validate_variable_font(font, report)
|
|
79
|
-
var_validator = Validation::VariableFontValidator.new(font)
|
|
80
|
-
errors = var_validator.validate
|
|
84
|
+
# Run validation
|
|
85
|
+
report = validator.validate(font)
|
|
81
86
|
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
puts " ERROR: #{error}" if @verbose && !@quiet
|
|
86
|
-
# Add to report if report supports adding errors
|
|
87
|
-
if report.respond_to?(:errors)
|
|
88
|
-
report.errors << { message: error,
|
|
89
|
-
category: "variable_font" }
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
elsif @verbose && !@quiet
|
|
93
|
-
puts "\n✓ Variable font structure valid"
|
|
87
|
+
# Filter excluded checks if specified
|
|
88
|
+
if @exclude.any?
|
|
89
|
+
report.check_results.reject! { |cr| @exclude.include?(cr.check_id) }
|
|
94
90
|
end
|
|
95
|
-
end
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# @raise [ArgumentError] if parameters are invalid
|
|
100
|
-
# @return [void]
|
|
101
|
-
def validate_params!
|
|
102
|
-
if @input.nil? || @input.empty?
|
|
103
|
-
raise ArgumentError,
|
|
104
|
-
"Input file is required"
|
|
105
|
-
end
|
|
106
|
-
unless File.exist?(@input)
|
|
107
|
-
raise ArgumentError,
|
|
108
|
-
"Input file does not exist: #{@input}"
|
|
109
|
-
end
|
|
92
|
+
# Generate output
|
|
93
|
+
output = generate_output(report)
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
valid_formats = %i[text yaml json]
|
|
118
|
-
unless valid_formats.include?(@format)
|
|
119
|
-
raise ArgumentError,
|
|
120
|
-
"Invalid format: #{@format}. Must be one of: #{valid_formats.join(', ')}"
|
|
95
|
+
# Write to file or stdout
|
|
96
|
+
if @output
|
|
97
|
+
File.write(@output, output)
|
|
98
|
+
puts "Validation report written to #{@output}" if @verbose && !@suppress_warnings
|
|
99
|
+
else
|
|
100
|
+
puts output unless @suppress_warnings
|
|
121
101
|
end
|
|
122
|
-
end
|
|
123
102
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
puts "Error loading font: #{e.message}" unless @quiet
|
|
131
|
-
nil
|
|
103
|
+
# Return exit code
|
|
104
|
+
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
|
|
132
109
|
end
|
|
133
110
|
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
# @param report [Models::ValidationReport] The validation report
|
|
137
|
-
# @return [void]
|
|
138
|
-
def output_report(report)
|
|
139
|
-
case @format
|
|
140
|
-
when :text
|
|
141
|
-
output_text(report)
|
|
142
|
-
when :yaml
|
|
143
|
-
output_yaml(report)
|
|
144
|
-
when :json
|
|
145
|
-
output_json(report)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
111
|
+
private
|
|
148
112
|
|
|
149
|
-
#
|
|
113
|
+
# Generate output based on requested format
|
|
150
114
|
#
|
|
151
|
-
# @param report [
|
|
152
|
-
# @return [
|
|
153
|
-
def
|
|
154
|
-
if @
|
|
155
|
-
|
|
115
|
+
# @param report [ValidationReport] The validation report
|
|
116
|
+
# @return [String] Formatted output
|
|
117
|
+
def generate_output(report)
|
|
118
|
+
if @table_report
|
|
119
|
+
report.to_table_format
|
|
120
|
+
elsif @summary_report
|
|
121
|
+
report.to_summary
|
|
122
|
+
elsif @full_report
|
|
123
|
+
report.to_text_report
|
|
156
124
|
else
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
125
|
+
# Default: format-specific output
|
|
126
|
+
case @format
|
|
127
|
+
when :yaml
|
|
128
|
+
require "yaml"
|
|
129
|
+
report.to_yaml
|
|
130
|
+
when :json
|
|
131
|
+
require "json"
|
|
132
|
+
report.to_json
|
|
133
|
+
else
|
|
134
|
+
report.text_summary
|
|
164
135
|
end
|
|
165
136
|
end
|
|
166
137
|
end
|
|
167
138
|
|
|
168
|
-
# Output report in YAML format
|
|
169
|
-
#
|
|
170
|
-
# @param report [Models::ValidationReport] The validation report
|
|
171
|
-
# @return [void]
|
|
172
|
-
def output_yaml(report)
|
|
173
|
-
require "yaml"
|
|
174
|
-
puts report.to_yaml
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Output report in JSON format
|
|
178
|
-
#
|
|
179
|
-
# @param report [Models::ValidationReport] The validation report
|
|
180
|
-
# @return [void]
|
|
181
|
-
def output_json(report)
|
|
182
|
-
require "json"
|
|
183
|
-
puts report.to_json
|
|
184
|
-
end
|
|
185
|
-
|
|
186
139
|
# Determine exit code based on validation results
|
|
187
140
|
#
|
|
188
|
-
# Exit codes:
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
141
|
+
# Exit codes (ftxvalidator compatible):
|
|
142
|
+
# 0 = No issues found
|
|
143
|
+
# 1 = Execution errors
|
|
144
|
+
# 2 = Fatal errors found
|
|
145
|
+
# 3 = Major errors found
|
|
146
|
+
# 4 = Minor errors (warnings) found
|
|
147
|
+
# 5 = Spec violations (info) found
|
|
192
148
|
#
|
|
193
|
-
# @param report [
|
|
149
|
+
# @param report [ValidationReport] The validation report
|
|
194
150
|
# @return [Integer] Exit code
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
151
|
+
def exit_code(report)
|
|
152
|
+
return 0 unless @return_value_results
|
|
153
|
+
|
|
154
|
+
return 2 if report.fatal_errors.any?
|
|
155
|
+
return 3 if report.errors_only.any?
|
|
156
|
+
return 4 if report.warnings_only.any?
|
|
157
|
+
return 5 if report.info_only.any?
|
|
158
|
+
0
|
|
203
159
|
end
|
|
204
160
|
end
|
|
205
161
|
end
|
|
@@ -78,26 +78,34 @@ conversions:
|
|
|
78
78
|
are architectural placeholders for future optimization.
|
|
79
79
|
|
|
80
80
|
# Phase 2 conversions (Milestone 2.3) - WOFF
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
- from: ttf
|
|
82
|
+
to: woff
|
|
83
|
+
strategy: woff_writer
|
|
84
|
+
description: "Compress TrueType to WOFF format with zlib"
|
|
85
|
+
status: implemented
|
|
86
|
+
notes: >
|
|
87
|
+
WOFF 1.0 encoding with zlib compression. Supports optional metadata
|
|
88
|
+
and private data blocks. Individual table compression for optimal
|
|
89
|
+
file size reduction.
|
|
90
|
+
|
|
91
|
+
- from: otf
|
|
92
|
+
to: woff
|
|
93
|
+
strategy: woff_writer
|
|
94
|
+
description: "Compress OpenType/CFF to WOFF format with zlib"
|
|
95
|
+
status: implemented
|
|
96
|
+
notes: >
|
|
97
|
+
WOFF 1.0 encoding with zlib compression. Supports optional metadata
|
|
98
|
+
and private data blocks. Individual table compression for optimal
|
|
99
|
+
file size reduction.
|
|
100
|
+
|
|
101
|
+
- from: woff
|
|
102
|
+
to: woff
|
|
103
|
+
strategy: woff_recompress
|
|
104
|
+
description: "Copy WOFF font with re-compression"
|
|
105
|
+
status: implemented
|
|
106
|
+
notes: >
|
|
107
|
+
Same-format copy operation. Decompresses and re-compresses WOFF data,
|
|
108
|
+
useful for normalizing compression or updating metadata.
|
|
101
109
|
|
|
102
110
|
# Reverse WOFF conversions (WOFF → TTF/OTF)
|
|
103
111
|
- from: woff
|
|
@@ -139,6 +147,36 @@ conversions:
|
|
|
139
147
|
transformation. Note: SVG fonts are deprecated in favor of WOFF2, but
|
|
140
148
|
useful for fallback, conversion, and inspection purposes.
|
|
141
149
|
|
|
150
|
+
# WOFF2 decompression (reverse conversions)
|
|
151
|
+
- from: woff2
|
|
152
|
+
to: ttf
|
|
153
|
+
strategy: woff2_decoder
|
|
154
|
+
description: "Decompress WOFF2 to TrueType format"
|
|
155
|
+
status: implemented
|
|
156
|
+
notes: >
|
|
157
|
+
WOFF2 decompression with Brotli. Reverses table transformations
|
|
158
|
+
(glyf/loca, hmtx) if present. Preserves all font data and metadata.
|
|
159
|
+
Automatically detects font flavor from WOFF2 header.
|
|
160
|
+
|
|
161
|
+
- from: woff2
|
|
162
|
+
to: otf
|
|
163
|
+
strategy: woff2_decoder
|
|
164
|
+
description: "Decompress WOFF2 to OpenType/CFF format"
|
|
165
|
+
status: implemented
|
|
166
|
+
notes: >
|
|
167
|
+
WOFF2 decompression with Brotli. Reverses table transformations
|
|
168
|
+
if present. Preserves all font data and metadata. Automatically
|
|
169
|
+
detects font flavor from WOFF2 header.
|
|
170
|
+
|
|
171
|
+
- from: woff2
|
|
172
|
+
to: woff2
|
|
173
|
+
strategy: woff2_recompress
|
|
174
|
+
description: "Copy WOFF2 font with re-compression"
|
|
175
|
+
status: implemented
|
|
176
|
+
notes: >
|
|
177
|
+
Same-format copy operation. Decompresses and re-compresses WOFF2 data
|
|
178
|
+
with Brotli, useful for normalizing compression or updating metadata.
|
|
179
|
+
|
|
142
180
|
# Conversion compatibility matrix
|
|
143
181
|
#
|
|
144
182
|
# This section documents which source features are preserved in conversions.
|
|
@@ -415,11 +415,14 @@ module Fontisan
|
|
|
415
415
|
# If we have local subroutines, add Subrs offset
|
|
416
416
|
# Subrs offset is relative to Private DICT start
|
|
417
417
|
if local_subrs.any?
|
|
418
|
-
#
|
|
418
|
+
# Add a placeholder Subrs offset first to get accurate size
|
|
419
|
+
private_dict_hash[:subrs] = 0
|
|
420
|
+
|
|
421
|
+
# Calculate size of Private DICT with Subrs entry
|
|
419
422
|
temp_private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
|
|
420
423
|
subrs_offset = temp_private_dict_data.bytesize
|
|
421
424
|
|
|
422
|
-
#
|
|
425
|
+
# Update with actual Subrs offset
|
|
423
426
|
private_dict_hash[:subrs] = subrs_offset
|
|
424
427
|
end
|
|
425
428
|
|
|
@@ -729,7 +732,7 @@ module Fontisan
|
|
|
729
732
|
if glyph_patterns.empty?
|
|
730
733
|
charstring
|
|
731
734
|
else
|
|
732
|
-
rewriter.rewrite(charstring, glyph_patterns)
|
|
735
|
+
rewriter.rewrite(charstring, glyph_patterns, glyph_id)
|
|
733
736
|
end
|
|
734
737
|
end
|
|
735
738
|
|
|
@@ -99,6 +99,51 @@ module Fontisan
|
|
|
99
99
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
|
+
# Generate SVG for a color glyph from COLR/CPAL tables
|
|
103
|
+
#
|
|
104
|
+
# @param glyph_id [Integer] Glyph ID
|
|
105
|
+
# @param colr_table [Tables::Colr] COLR table
|
|
106
|
+
# @param cpal_table [Tables::Cpal] CPAL table
|
|
107
|
+
# @param extractor [OutlineExtractor] Outline extractor for layer glyphs
|
|
108
|
+
# @param palette_index [Integer] Palette index to use (default: 0)
|
|
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, palette_index: 0)
|
|
111
|
+
# Get layers for this glyph
|
|
112
|
+
layers = colr_table.layers_for_glyph(glyph_id)
|
|
113
|
+
return nil if layers.empty?
|
|
114
|
+
|
|
115
|
+
# Get palette colors
|
|
116
|
+
palette = cpal_table.palette(palette_index)
|
|
117
|
+
return nil unless palette
|
|
118
|
+
|
|
119
|
+
# Generate SVG for each layer
|
|
120
|
+
svg_paths = []
|
|
121
|
+
layers.each do |layer|
|
|
122
|
+
# Get outline for layer glyph
|
|
123
|
+
outline = extractor.extract(layer.glyph_id)
|
|
124
|
+
next unless outline
|
|
125
|
+
|
|
126
|
+
# Get color for this layer
|
|
127
|
+
color = if layer.uses_foreground_color?
|
|
128
|
+
"currentColor" # Use CSS currentColor for foreground
|
|
129
|
+
else
|
|
130
|
+
palette[layer.palette_index] || "#000000FF"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Convert outline to SVG path
|
|
134
|
+
path_data = outline.to_svg_path
|
|
135
|
+
next if path_data.empty?
|
|
136
|
+
|
|
137
|
+
# Create colored path element
|
|
138
|
+
svg_paths << %(<path d="#{path_data}" fill="#{color}" />)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
svg_paths.empty? ? nil : svg_paths.join("\n")
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
warn "Failed to generate color glyph #{glyph_id}: #{e.message}"
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
102
147
|
# Extract glyph data from font
|
|
103
148
|
#
|
|
104
149
|
# @param font [TrueTypeFont, OpenTypeFont] Source font
|