fontisan 0.2.4 → 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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +150 -30
  3. data/README.adoc +497 -242
  4. data/lib/fontisan/cli.rb +67 -6
  5. data/lib/fontisan/commands/validate_command.rb +107 -151
  6. data/lib/fontisan/converters/woff2_encoder.rb +7 -29
  7. data/lib/fontisan/models/validation_report.rb +227 -0
  8. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  9. data/lib/fontisan/tables/cmap.rb +82 -2
  10. data/lib/fontisan/tables/glyf.rb +118 -0
  11. data/lib/fontisan/tables/head.rb +60 -0
  12. data/lib/fontisan/tables/hhea.rb +74 -0
  13. data/lib/fontisan/tables/maxp.rb +60 -0
  14. data/lib/fontisan/tables/name.rb +76 -0
  15. data/lib/fontisan/tables/os2.rb +113 -0
  16. data/lib/fontisan/tables/post.rb +57 -0
  17. data/lib/fontisan/validators/basic_validator.rb +85 -0
  18. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  19. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  20. data/lib/fontisan/validators/profile_loader.rb +139 -0
  21. data/lib/fontisan/validators/validator.rb +484 -0
  22. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  23. data/lib/fontisan/version.rb +1 -1
  24. data/lib/fontisan.rb +78 -6
  25. metadata +7 -11
  26. data/lib/fontisan/config/validation_rules.yml +0 -149
  27. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  28. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  29. data/lib/fontisan/validation/structure_validator.rb +0 -198
  30. data/lib/fontisan/validation/table_validator.rb +0 -158
  31. data/lib/fontisan/validation/validator.rb +0 -152
  32. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
  33. data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
  34. data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
  35. data/lib/fontisan/validation/woff2_validator.rb +0 -248
data/lib/fontisan/cli.rb CHANGED
@@ -350,13 +350,62 @@ module Fontisan
350
350
  handle_error(e)
351
351
  end
352
352
 
353
- desc "validate FONT_FILE", "Validate font file structure and checksums"
354
- option :verbose, type: :boolean, default: false,
355
- desc: "Show detailed validation information"
353
+ desc "validate FONT_FILE", "Validate font file"
354
+ long_desc <<-DESC
355
+ Validate font file against quality checks and standards.
356
+
357
+ Test lists (-t/--test-list):
358
+ indexability - Fast indexing validation
359
+ usability - Installation compatibility
360
+ production - Comprehensive quality (default)
361
+ web - Web font readiness
362
+ spec_compliance - OpenType spec compliance
363
+ default - Production profile (alias)
364
+
365
+ Return values (with -R/--return-value-results):
366
+ 0 No results
367
+ 1 Execution errors
368
+ 2 Fatal errors found
369
+ 3 Major errors found
370
+ 4 Minor errors found
371
+ 5 Spec violations found
372
+ DESC
373
+
374
+ option :exclude, aliases: "-e", type: :array, desc: "Tests to exclude"
375
+ option :list, aliases: "-l", type: :boolean, desc: "List available tests"
376
+ option :output, aliases: "-o", type: :string, desc: "Output file"
377
+ option :full_report, aliases: "-r", type: :boolean, desc: "Full report"
378
+ option :return_value_results, aliases: "-R", type: :boolean, desc: "Use return value for results"
379
+ option :summary_report, aliases: "-S", type: :boolean, desc: "Summary report"
380
+ option :test_list, aliases: "-t", type: :string, default: "default", desc: "Tests to execute"
381
+ option :table_report, aliases: "-T", type: :boolean, desc: "Tabular report"
382
+ option :verbose, aliases: "-v", type: :boolean, desc: "Verbose output"
383
+ option :suppress_warnings, aliases: "-W", type: :boolean, desc: "Suppress warnings"
384
+
356
385
  def validate(font_file)
357
- command = Commands::ValidateCommand.new(font_file,
358
- verbose: options[:verbose])
359
- exit command.run
386
+ if options[:list]
387
+ list_available_tests
388
+ return
389
+ end
390
+
391
+ cmd = Commands::ValidateCommand.new(
392
+ input: font_file,
393
+ profile: options[:test_list],
394
+ exclude: options[:exclude] || [],
395
+ output: options[:output],
396
+ format: options[:format].to_sym,
397
+ full_report: options[:full_report],
398
+ summary_report: options[:summary_report],
399
+ table_report: options[:table_report],
400
+ verbose: options[:verbose],
401
+ suppress_warnings: options[:suppress_warnings],
402
+ return_value_results: options[:return_value_results]
403
+ )
404
+
405
+ exit cmd.run
406
+ rescue => e
407
+ error "Validation failed: #{e.message}"
408
+ exit 1
360
409
  end
361
410
 
362
411
  desc "export FONT_FILE", "Export font to TTX/YAML/JSON format"
@@ -555,5 +604,17 @@ module Fontisan
555
604
  warn message unless options[:quiet]
556
605
  exit 1
557
606
  end
607
+
608
+ # List available validation tests/profiles
609
+ #
610
+ # @return [void]
611
+ def list_available_tests
612
+ require_relative "validators/profile_loader"
613
+ profiles = Validators::ProfileLoader.all_profiles
614
+ puts "Available validation profiles:"
615
+ profiles.each do |profile_name, config|
616
+ puts " #{profile_name.to_s.ljust(20)} - #{config[:description]}"
617
+ end
618
+ end
558
619
  end
559
620
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_command"
4
- require_relative "../validation/validator"
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
- # levels and output formats.
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
- # level: :standard,
20
- # format: :text
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 level [Symbol] Validation level (:strict, :standard, :lenient)
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 verbose [Boolean] Show all issues (default: true)
30
- # @param quiet [Boolean] Only return exit code, no output (default: false)
31
- def initialize(input:, level: :standard, format: :text, verbose: true,
32
- quiet: false)
33
- super()
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
- @level = level.to_sym
36
- @format = format.to_sym
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
- @quiet = quiet
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, 1 = errors, 2 = warnings only)
68
+ # @return [Integer] Exit code (0 = valid, 2 = fatal, 3 = errors, 4 = warnings, 5 = info)
44
69
  def run
45
- validate_params!
46
-
47
- # Load font
48
- font = load_font
49
- return 1 unless font
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
- # Add variable font validation if applicable
58
- validate_variable_font(font, report) if font.has_table?("fvar")
77
+ mode = profile_config[:loading_mode].to_sym
59
78
 
60
- # Output results unless quiet mode
61
- output_report(report) unless @quiet
79
+ font = FontLoader.load(@input, mode: mode)
62
80
 
63
- # Return appropriate exit code
64
- determine_exit_code(report)
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
- private
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 errors.any?
83
- puts "\nVariable font validation:" if @verbose && !@quiet
84
- errors.each do |error|
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
- # Validate command parameters
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
- valid_levels = %i[strict standard lenient]
112
- unless valid_levels.include?(@level)
113
- raise ArgumentError,
114
- "Invalid level: #{@level}. Must be one of: #{valid_levels.join(', ')}"
115
- end
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
- # Load the font file
125
- #
126
- # @return [TrueTypeFont, OpenTypeFont, nil] The loaded font or nil on error
127
- def load_font
128
- FontLoader.load(@input)
129
- rescue StandardError => e
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
- # Output validation report in requested format
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
- # Output report in text format
113
+ # Generate output based on requested format
150
114
  #
151
- # @param report [Models::ValidationReport] The validation report
152
- # @return [void]
153
- def output_text(report)
154
- if @verbose
155
- puts report.text_summary
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
- # Compact output: just status and error/warning counts
158
- status = report.valid ? "VALID" : "INVALID"
159
- puts "#{status}: #{report.summary.errors} errors, #{report.summary.warnings} warnings"
160
-
161
- # Show errors only in non-verbose mode
162
- report.errors.each do |error|
163
- puts " [ERROR] #{error.message}"
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
- # - 0: Valid (no errors, or only warnings in lenient mode)
190
- # - 1: Has errors
191
- # - 2: Has warnings only (no errors)
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 [Models::ValidationReport] The validation report
149
+ # @param report [ValidationReport] The validation report
194
150
  # @return [Integer] Exit code
195
- def determine_exit_code(report)
196
- if report.has_errors?
197
- 1
198
- elsif report.has_warnings?
199
- 2
200
- else
201
- 0
202
- end
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
@@ -6,7 +6,8 @@ require_relative "../woff2/directory"
6
6
  require_relative "../woff2/table_transformer"
7
7
  require_relative "../utilities/brotli_wrapper"
8
8
  require_relative "../utilities/checksum_calculator"
9
- require_relative "../validation/woff2_validator"
9
+ # Validation temporarily disabled - will be reimplemented with new DSL framework in Week 3+
10
+ # require_relative "../validation/woff2_validator"
10
11
  require "yaml"
11
12
  require "stringio"
12
13
 
@@ -111,10 +112,11 @@ module Fontisan
111
112
  result = { woff2_binary: woff2_binary }
112
113
 
113
114
  # Optional validation
114
- if options[:validate]
115
- validation_report = validate_encoding(woff2_binary, options)
116
- result[:validation_report] = validation_report
117
- end
115
+ # Temporarily disabled - will be reimplemented with new DSL framework
116
+ # if options[:validate]
117
+ # validation_report = validate_encoding(woff2_binary, options)
118
+ # result[:validation_report] = validation_report
119
+ # end
118
120
 
119
121
  result
120
122
  end
@@ -162,30 +164,6 @@ module Fontisan
162
164
 
163
165
  private
164
166
 
165
- # Validate encoded WOFF2 binary
166
- #
167
- # @param woff2_binary [String] Encoded WOFF2 data
168
- # @param options [Hash] Validation options
169
- # @return [Models::ValidationReport] Validation report
170
- def validate_encoding(woff2_binary, options)
171
- # Load the encoded WOFF2 from memory
172
- io = StringIO.new(woff2_binary)
173
- woff2_font = Woff2Font.from_file_io(io, "encoded.woff2")
174
-
175
- # Run validation
176
- validation_level = options[:validation_level] || :standard
177
- validator = Validation::Woff2Validator.new(level: validation_level)
178
- validator.validate(woff2_font, "encoded.woff2")
179
- rescue StandardError => e
180
- # If validation fails, create a report with the error
181
- report = Models::ValidationReport.new(
182
- font_path: "encoded.woff2",
183
- valid: false,
184
- )
185
- report.add_error("woff2_validation", "Validation failed: #{e.message}", nil)
186
- report
187
- end
188
-
189
167
  # Helper method to load WOFF2 from StringIO
190
168
  #
191
169
  # This is added to Woff2Font to support in-memory validation