fontisan 0.2.4 → 0.2.6
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 +168 -32
- data/README.adoc +673 -1091
- data/lib/fontisan/cli.rb +94 -13
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- 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/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -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.rb +78 -6
- metadata +13 -12
- 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
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/fontisan/binary/structures.rb
|
|
128
128
|
- lib/fontisan/cli.rb
|
|
129
129
|
- lib/fontisan/collection/builder.rb
|
|
130
|
+
- lib/fontisan/collection/dfont_builder.rb
|
|
130
131
|
- lib/fontisan/collection/offset_calculator.rb
|
|
131
132
|
- lib/fontisan/collection/table_analyzer.rb
|
|
132
133
|
- lib/fontisan/collection/table_deduplicator.rb
|
|
@@ -156,10 +157,10 @@ files:
|
|
|
156
157
|
- lib/fontisan/config/scripts.yml
|
|
157
158
|
- lib/fontisan/config/subset_profiles.yml
|
|
158
159
|
- lib/fontisan/config/svg_settings.yml
|
|
159
|
-
- lib/fontisan/config/validation_rules.yml
|
|
160
160
|
- lib/fontisan/config/variable_settings.yml
|
|
161
161
|
- lib/fontisan/config/woff2_settings.yml
|
|
162
162
|
- lib/fontisan/constants.rb
|
|
163
|
+
- lib/fontisan/converters/collection_converter.rb
|
|
163
164
|
- lib/fontisan/converters/conversion_strategy.rb
|
|
164
165
|
- lib/fontisan/converters/format_converter.rb
|
|
165
166
|
- lib/fontisan/converters/outline_converter.rb
|
|
@@ -167,6 +168,7 @@ files:
|
|
|
167
168
|
- lib/fontisan/converters/table_copier.rb
|
|
168
169
|
- lib/fontisan/converters/woff2_encoder.rb
|
|
169
170
|
- lib/fontisan/converters/woff_writer.rb
|
|
171
|
+
- lib/fontisan/dfont_collection.rb
|
|
170
172
|
- lib/fontisan/error.rb
|
|
171
173
|
- lib/fontisan/export/exporter.rb
|
|
172
174
|
- lib/fontisan/export/table_serializer.rb
|
|
@@ -238,6 +240,7 @@ files:
|
|
|
238
240
|
- lib/fontisan/optimizers/subroutine_generator.rb
|
|
239
241
|
- lib/fontisan/optimizers/subroutine_optimizer.rb
|
|
240
242
|
- lib/fontisan/outline_extractor.rb
|
|
243
|
+
- lib/fontisan/parsers/dfont_parser.rb
|
|
241
244
|
- lib/fontisan/parsers/tag.rb
|
|
242
245
|
- lib/fontisan/pipeline/format_detector.rb
|
|
243
246
|
- lib/fontisan/pipeline/output_writer.rb
|
|
@@ -322,15 +325,13 @@ files:
|
|
|
322
325
|
- lib/fontisan/utilities/brotli_wrapper.rb
|
|
323
326
|
- lib/fontisan/utilities/checksum_calculator.rb
|
|
324
327
|
- lib/fontisan/utils/thread_pool.rb
|
|
325
|
-
- lib/fontisan/validation/
|
|
326
|
-
- lib/fontisan/
|
|
327
|
-
- lib/fontisan/
|
|
328
|
-
- lib/fontisan/
|
|
329
|
-
- lib/fontisan/
|
|
330
|
-
- lib/fontisan/
|
|
331
|
-
- lib/fontisan/
|
|
332
|
-
- lib/fontisan/validation/woff2_table_validator.rb
|
|
333
|
-
- lib/fontisan/validation/woff2_validator.rb
|
|
328
|
+
- lib/fontisan/validation/collection_validator.rb
|
|
329
|
+
- lib/fontisan/validators/basic_validator.rb
|
|
330
|
+
- lib/fontisan/validators/font_book_validator.rb
|
|
331
|
+
- lib/fontisan/validators/opentype_validator.rb
|
|
332
|
+
- lib/fontisan/validators/profile_loader.rb
|
|
333
|
+
- lib/fontisan/validators/validator.rb
|
|
334
|
+
- lib/fontisan/validators/web_font_validator.rb
|
|
334
335
|
- lib/fontisan/variable/axis_normalizer.rb
|
|
335
336
|
- lib/fontisan/variable/delta_applicator.rb
|
|
336
337
|
- lib/fontisan/variable/glyph_delta_processor.rb
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# Font Validation Rules Configuration
|
|
2
|
-
#
|
|
3
|
-
# This file defines validation rules for different font types and validation levels.
|
|
4
|
-
# The rules determine which checks are performed and their severity thresholds.
|
|
5
|
-
|
|
6
|
-
# Required tables for different font types
|
|
7
|
-
required_tables:
|
|
8
|
-
# Tables required for all fonts
|
|
9
|
-
all:
|
|
10
|
-
- head
|
|
11
|
-
- name
|
|
12
|
-
- cmap
|
|
13
|
-
- maxp
|
|
14
|
-
- hhea
|
|
15
|
-
- hmtx
|
|
16
|
-
- post
|
|
17
|
-
- OS/2
|
|
18
|
-
|
|
19
|
-
# Additional tables required for TrueType fonts (.ttf)
|
|
20
|
-
truetype:
|
|
21
|
-
- glyf
|
|
22
|
-
- loca
|
|
23
|
-
|
|
24
|
-
# Additional tables required for OpenType/CFF fonts (.otf)
|
|
25
|
-
opentype_cff:
|
|
26
|
-
- "CFF " # or CFF2 for CFF2 format (note: CFF has trailing space per OpenType spec)
|
|
27
|
-
|
|
28
|
-
# Additional tables required for variable fonts
|
|
29
|
-
variable:
|
|
30
|
-
- fvar
|
|
31
|
-
- gvar # For TrueType variable fonts
|
|
32
|
-
- HVAR # Horizontal metrics variations (optional but recommended)
|
|
33
|
-
- MVAR # Metrics variations (optional)
|
|
34
|
-
|
|
35
|
-
# Validation levels define which checks to perform
|
|
36
|
-
validation_levels:
|
|
37
|
-
# Strict: All checks must pass, no warnings allowed
|
|
38
|
-
strict:
|
|
39
|
-
check_required_tables: true
|
|
40
|
-
check_table_checksums: true
|
|
41
|
-
check_head_checksum_adjustment: true
|
|
42
|
-
check_glyph_consistency: true
|
|
43
|
-
check_cmap_references: true
|
|
44
|
-
check_hmtx_consistency: true
|
|
45
|
-
check_name_consistency: true
|
|
46
|
-
check_variable_consistency: true
|
|
47
|
-
check_table_offsets: true
|
|
48
|
-
check_table_ordering: false # Not critical for correctness
|
|
49
|
-
allow_warnings: false
|
|
50
|
-
allow_info: true
|
|
51
|
-
|
|
52
|
-
# Standard: Most checks enabled, warnings allowed
|
|
53
|
-
standard:
|
|
54
|
-
check_required_tables: true
|
|
55
|
-
check_table_checksums: true
|
|
56
|
-
check_head_checksum_adjustment: true
|
|
57
|
-
check_glyph_consistency: true
|
|
58
|
-
check_cmap_references: true
|
|
59
|
-
check_hmtx_consistency: true
|
|
60
|
-
check_name_consistency: true
|
|
61
|
-
check_variable_consistency: true
|
|
62
|
-
check_table_offsets: true
|
|
63
|
-
check_table_ordering: false
|
|
64
|
-
allow_warnings: true
|
|
65
|
-
allow_info: true
|
|
66
|
-
|
|
67
|
-
# Lenient: Basic checks only, many warnings allowed
|
|
68
|
-
lenient:
|
|
69
|
-
check_required_tables: true
|
|
70
|
-
check_table_checksums: false # Skip checksum validation
|
|
71
|
-
check_head_checksum_adjustment: false
|
|
72
|
-
check_glyph_consistency: true
|
|
73
|
-
check_cmap_references: false # Skip cmap validation
|
|
74
|
-
check_hmtx_consistency: true
|
|
75
|
-
check_name_consistency: false
|
|
76
|
-
check_variable_consistency: false
|
|
77
|
-
check_table_offsets: true
|
|
78
|
-
check_table_ordering: false
|
|
79
|
-
allow_warnings: true
|
|
80
|
-
allow_info: true
|
|
81
|
-
|
|
82
|
-
# Error messages templates
|
|
83
|
-
error_messages:
|
|
84
|
-
missing_table: "Missing required table: %{table}"
|
|
85
|
-
invalid_table_checksum: "Table '%{table}' checksum mismatch (expected: %{expected}, got: %{actual})"
|
|
86
|
-
invalid_head_checksum: "Invalid head table checksum adjustment"
|
|
87
|
-
glyph_count_mismatch: "Glyph count mismatch: maxp=%{maxp}, actual=%{actual}"
|
|
88
|
-
invalid_cmap_reference: "cmap references non-existent glyph ID %{glyph_id}"
|
|
89
|
-
hmtx_count_mismatch: "hmtx entries (%{hmtx}) don't match glyph count (%{glyph_count})"
|
|
90
|
-
invalid_table_offset: "Table '%{table}' has invalid offset: %{offset}"
|
|
91
|
-
variable_table_missing: "Variable font missing required table: %{table}"
|
|
92
|
-
fvar_gvar_mismatch: "fvar axis count (%{fvar}) doesn't match gvar axis count (%{gvar})"
|
|
93
|
-
|
|
94
|
-
# Warning messages templates
|
|
95
|
-
warning_messages:
|
|
96
|
-
table_checksum_mismatch: "Table '%{table}' checksum mismatch"
|
|
97
|
-
suboptimal_table_order: "Tables not in optimal order for performance"
|
|
98
|
-
missing_optional_table: "Optional table '%{table}' not present (recommended for %{reason})"
|
|
99
|
-
name_inconsistency: "Name table inconsistency: %{issue}"
|
|
100
|
-
large_font_size: "Font file size (%{size} bytes) is unusually large"
|
|
101
|
-
|
|
102
|
-
# Info messages templates
|
|
103
|
-
info_messages:
|
|
104
|
-
optimization_opportunity: "Font could benefit from %{optimization}"
|
|
105
|
-
subsetting_recommended: "Font contains %{glyph_count} glyphs; subsetting may reduce size"
|
|
106
|
-
compression_recommended: "Font could benefit from WOFF2 compression"
|
|
107
|
-
|
|
108
|
-
# Checksum validation settings
|
|
109
|
-
checksum_validation:
|
|
110
|
-
# Magic number for checksum adjustment calculation
|
|
111
|
-
magic: 0xB1B0AFBA
|
|
112
|
-
|
|
113
|
-
# Tables exempt from checksum validation (some tables have dynamic content)
|
|
114
|
-
skip_tables:
|
|
115
|
-
- DSIG # Digital signature table (changes with signing)
|
|
116
|
-
|
|
117
|
-
# Consistency checks configuration
|
|
118
|
-
consistency_checks:
|
|
119
|
-
# Maximum acceptable glyph ID for cmap validation
|
|
120
|
-
max_glyph_id_multiplier: 2.0 # Allow up to 2x maxp.numGlyphs for safety
|
|
121
|
-
|
|
122
|
-
# Minimum acceptable glyph count
|
|
123
|
-
min_glyph_count: 1
|
|
124
|
-
|
|
125
|
-
# Maximum acceptable glyph count (sanity check)
|
|
126
|
-
max_glyph_count: 65536
|
|
127
|
-
|
|
128
|
-
# Structure validation settings
|
|
129
|
-
structure_validation:
|
|
130
|
-
# Minimum valid offset (after header and directory)
|
|
131
|
-
min_table_offset: 12
|
|
132
|
-
|
|
133
|
-
# Table alignment requirement (4-byte alignment)
|
|
134
|
-
table_alignment: 4
|
|
135
|
-
|
|
136
|
-
# Maximum reasonable table size (100MB)
|
|
137
|
-
max_table_size: 104857600
|
|
138
|
-
|
|
139
|
-
# Variable font specific validation
|
|
140
|
-
variable_validation:
|
|
141
|
-
# Check that all required variation tables are consistent
|
|
142
|
-
check_axis_consistency: true
|
|
143
|
-
|
|
144
|
-
# Check that variation regions are valid
|
|
145
|
-
check_region_validity: true
|
|
146
|
-
|
|
147
|
-
# Check that deltas are within reasonable bounds
|
|
148
|
-
max_delta_value: 32767
|
|
149
|
-
min_delta_value: -32768
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../utilities/checksum_calculator"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Validation
|
|
7
|
-
# ChecksumValidator validates font file and table checksums
|
|
8
|
-
#
|
|
9
|
-
# This validator checks that the head table checksum adjustment is correct
|
|
10
|
-
# and validates individual table checksums to ensure file integrity.
|
|
11
|
-
#
|
|
12
|
-
# Single Responsibility: Checksum validation and file integrity
|
|
13
|
-
#
|
|
14
|
-
# @example Validating checksums
|
|
15
|
-
# validator = ChecksumValidator.new(rules)
|
|
16
|
-
# issues = validator.validate(font, font_path)
|
|
17
|
-
class ChecksumValidator
|
|
18
|
-
# Initialize checksum validator
|
|
19
|
-
#
|
|
20
|
-
# @param rules [Hash] Validation rules configuration
|
|
21
|
-
def initialize(rules)
|
|
22
|
-
@rules = rules
|
|
23
|
-
@checksum_config = rules["checksum_validation"] || {}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Validate font checksums
|
|
27
|
-
#
|
|
28
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
29
|
-
# @param font_path [String] Path to the font file
|
|
30
|
-
# @return [Array<Hash>] Array of validation issues
|
|
31
|
-
def validate(font, font_path)
|
|
32
|
-
issues = []
|
|
33
|
-
|
|
34
|
-
# Check head table checksum adjustment if enabled
|
|
35
|
-
if should_check?("check_head_checksum_adjustment")
|
|
36
|
-
issues.concat(check_head_checksum_adjustment(font, font_path))
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Check individual table checksums if enabled
|
|
40
|
-
if should_check?("check_table_checksums")
|
|
41
|
-
issues.concat(check_table_checksums(font))
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
issues
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# Check if a validation should be performed
|
|
50
|
-
#
|
|
51
|
-
# @param check_name [String] The check name
|
|
52
|
-
# @return [Boolean] true if check should be performed
|
|
53
|
-
def should_check?(check_name)
|
|
54
|
-
@rules.dig("validation_levels", "standard", check_name)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Check head table checksum adjustment
|
|
58
|
-
#
|
|
59
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
60
|
-
# @param font_path [String] Path to the font file
|
|
61
|
-
# @return [Array<Hash>] Array of checksum issues
|
|
62
|
-
def check_head_checksum_adjustment(font, font_path)
|
|
63
|
-
issues = []
|
|
64
|
-
|
|
65
|
-
head_entry = font.head_table
|
|
66
|
-
return issues unless head_entry
|
|
67
|
-
|
|
68
|
-
# Calculate the checksum of the entire font file
|
|
69
|
-
begin
|
|
70
|
-
file_checksum = Utilities::ChecksumCalculator.calculate_file_checksum(font_path)
|
|
71
|
-
magic = @checksum_config["magic"] || Constants::CHECKSUM_ADJUSTMENT_MAGIC
|
|
72
|
-
|
|
73
|
-
# Read the actual checksum adjustment from head table
|
|
74
|
-
head_data = font.table_data[Constants::HEAD_TAG]
|
|
75
|
-
return issues unless head_data && head_data.bytesize >= 12
|
|
76
|
-
|
|
77
|
-
actual_adjustment = head_data.byteslice(8, 4).unpack1("N")
|
|
78
|
-
|
|
79
|
-
# The actual adjustment should be 0 when we calculate, since we zero it out
|
|
80
|
-
# So we need to check if the file checksum with zeroed adjustment equals magic
|
|
81
|
-
if file_checksum != magic
|
|
82
|
-
# Calculate what the adjustment should be
|
|
83
|
-
temp_checksum = (file_checksum - actual_adjustment) & 0xFFFFFFFF
|
|
84
|
-
correct_adjustment = (magic - temp_checksum) & 0xFFFFFFFF
|
|
85
|
-
|
|
86
|
-
if actual_adjustment != correct_adjustment
|
|
87
|
-
issues << {
|
|
88
|
-
severity: "error",
|
|
89
|
-
category: "checksum",
|
|
90
|
-
message: "Invalid head table checksum adjustment (expected: 0x#{correct_adjustment.to_s(16)}, got: 0x#{actual_adjustment.to_s(16)})",
|
|
91
|
-
location: "head table",
|
|
92
|
-
}
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
rescue StandardError => e
|
|
96
|
-
issues << {
|
|
97
|
-
severity: "error",
|
|
98
|
-
category: "checksum",
|
|
99
|
-
message: "Failed to validate head checksum adjustment: #{e.message}",
|
|
100
|
-
location: "head table",
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
issues
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Check individual table checksums
|
|
108
|
-
#
|
|
109
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
110
|
-
# @return [Array<Hash>] Array of table checksum issues
|
|
111
|
-
def check_table_checksums(font)
|
|
112
|
-
issues = []
|
|
113
|
-
|
|
114
|
-
skip_tables = @checksum_config["skip_tables"] || []
|
|
115
|
-
|
|
116
|
-
font.tables.each do |table_entry|
|
|
117
|
-
tag = table_entry.tag.to_s # Convert BinData field to string
|
|
118
|
-
|
|
119
|
-
# Skip tables that are exempt from checksum validation
|
|
120
|
-
next if skip_tables.include?(tag)
|
|
121
|
-
|
|
122
|
-
# Get table data
|
|
123
|
-
table_data = font.table_data[tag]
|
|
124
|
-
next unless table_data
|
|
125
|
-
|
|
126
|
-
# Calculate checksum for the table
|
|
127
|
-
calculated_checksum = calculate_table_checksum(table_data)
|
|
128
|
-
declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
|
|
129
|
-
|
|
130
|
-
# Special handling for head table (checksum adjustment field should be 0)
|
|
131
|
-
if tag == Constants::HEAD_TAG
|
|
132
|
-
# Zero out checksum adjustment field for calculation
|
|
133
|
-
modified_data = table_data.dup
|
|
134
|
-
modified_data[8, 4] = "\x00\x00\x00\x00"
|
|
135
|
-
calculated_checksum = calculate_table_checksum(modified_data)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
if calculated_checksum != declared_checksum
|
|
139
|
-
issues << {
|
|
140
|
-
severity: "warning",
|
|
141
|
-
category: "checksum",
|
|
142
|
-
message: "Table '#{tag}' checksum mismatch (expected: 0x#{declared_checksum.to_s(16)}, got: 0x#{calculated_checksum.to_s(16)})",
|
|
143
|
-
location: "#{tag} table",
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
issues
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Calculate checksum for table data
|
|
152
|
-
#
|
|
153
|
-
# @param data [String] The table data
|
|
154
|
-
# @return [Integer] The calculated checksum
|
|
155
|
-
def calculate_table_checksum(data)
|
|
156
|
-
sum = 0
|
|
157
|
-
# Pad to 4-byte boundary
|
|
158
|
-
padded_data = data + ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
159
|
-
|
|
160
|
-
# Sum all 32-bit values
|
|
161
|
-
(0...padded_data.bytesize).step(4) do |i|
|
|
162
|
-
value = padded_data.byteslice(i, 4).unpack1("N")
|
|
163
|
-
sum = (sum + value) & 0xFFFFFFFF
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
sum
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Validation
|
|
5
|
-
# ConsistencyValidator validates cross-table consistency
|
|
6
|
-
#
|
|
7
|
-
# This validator ensures that references between tables are valid,
|
|
8
|
-
# such as cmap glyph references, hmtx entry counts, and variable
|
|
9
|
-
# font table consistency.
|
|
10
|
-
#
|
|
11
|
-
# Single Responsibility: Cross-table data consistency validation
|
|
12
|
-
#
|
|
13
|
-
# @example Validating consistency
|
|
14
|
-
# validator = ConsistencyValidator.new(rules)
|
|
15
|
-
# issues = validator.validate(font)
|
|
16
|
-
class ConsistencyValidator
|
|
17
|
-
# Initialize consistency validator
|
|
18
|
-
#
|
|
19
|
-
# @param rules [Hash] Validation rules configuration
|
|
20
|
-
def initialize(rules)
|
|
21
|
-
@rules = rules
|
|
22
|
-
@consistency_config = rules["consistency_checks"] || {}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Validate font consistency
|
|
26
|
-
#
|
|
27
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
28
|
-
# @return [Array<Hash>] Array of validation issues
|
|
29
|
-
def validate(font)
|
|
30
|
-
issues = []
|
|
31
|
-
|
|
32
|
-
# Check hmtx consistency if enabled
|
|
33
|
-
issues.concat(check_hmtx_consistency(font)) if should_check?("check_hmtx_consistency")
|
|
34
|
-
|
|
35
|
-
# Check name table consistency if enabled
|
|
36
|
-
issues.concat(check_name_consistency(font)) if should_check?("check_name_consistency")
|
|
37
|
-
|
|
38
|
-
# Check variable font consistency if enabled
|
|
39
|
-
issues.concat(check_variable_consistency(font)) if should_check?("check_variable_consistency")
|
|
40
|
-
|
|
41
|
-
issues
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
# Check if a validation should be performed
|
|
47
|
-
#
|
|
48
|
-
# @param check_name [String] The check name
|
|
49
|
-
# @return [Boolean] true if check should be performed
|
|
50
|
-
def should_check?(check_name)
|
|
51
|
-
@rules.dig("validation_levels", "standard", check_name)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Check hmtx entry count matches glyph count
|
|
55
|
-
#
|
|
56
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
57
|
-
# @return [Array<Hash>] Array of hmtx consistency issues
|
|
58
|
-
def check_hmtx_consistency(font)
|
|
59
|
-
issues = []
|
|
60
|
-
|
|
61
|
-
hmtx = font.table(Constants::HMTX_TAG)
|
|
62
|
-
maxp = font.table(Constants::MAXP_TAG)
|
|
63
|
-
hhea = font.table(Constants::HHEA_TAG)
|
|
64
|
-
|
|
65
|
-
return issues unless hmtx && maxp && hhea
|
|
66
|
-
|
|
67
|
-
glyph_count = maxp.num_glyphs
|
|
68
|
-
num_of_long_hor_metrics = hhea.number_of_h_metrics
|
|
69
|
-
|
|
70
|
-
# Verify the structure makes sense
|
|
71
|
-
if num_of_long_hor_metrics > glyph_count
|
|
72
|
-
issues << {
|
|
73
|
-
severity: "error",
|
|
74
|
-
category: "consistency",
|
|
75
|
-
message: "hhea number_of_h_metrics (#{num_of_long_hor_metrics}) exceeds glyph count (#{glyph_count})",
|
|
76
|
-
location: "hhea/hmtx tables",
|
|
77
|
-
}
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if num_of_long_hor_metrics < 1
|
|
81
|
-
issues << {
|
|
82
|
-
severity: "error",
|
|
83
|
-
category: "consistency",
|
|
84
|
-
message: "hhea number_of_h_metrics is #{num_of_long_hor_metrics}, must be at least 1",
|
|
85
|
-
location: "hhea table",
|
|
86
|
-
}
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
issues
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Check name table for consistency issues
|
|
93
|
-
#
|
|
94
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
95
|
-
# @return [Array<Hash>] Array of name table issues
|
|
96
|
-
def check_name_consistency(font)
|
|
97
|
-
issues = []
|
|
98
|
-
|
|
99
|
-
name = font.table(Constants::NAME_TAG)
|
|
100
|
-
return issues unless name
|
|
101
|
-
|
|
102
|
-
# Check that required name IDs are present
|
|
103
|
-
required_name_ids = [
|
|
104
|
-
Tables::Name::FAMILY, # 1
|
|
105
|
-
Tables::Name::SUBFAMILY, # 2
|
|
106
|
-
Tables::Name::FULL_NAME, # 4
|
|
107
|
-
Tables::Name::VERSION, # 5
|
|
108
|
-
Tables::Name::POSTSCRIPT_NAME, # 6
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
required_name_ids.each do |name_id|
|
|
112
|
-
has_entry = name.name_records.any? do |record|
|
|
113
|
-
record.name_id == name_id
|
|
114
|
-
end
|
|
115
|
-
unless has_entry
|
|
116
|
-
issues << {
|
|
117
|
-
severity: "warning",
|
|
118
|
-
category: "consistency",
|
|
119
|
-
message: "Missing recommended name ID #{name_id}",
|
|
120
|
-
location: "name table",
|
|
121
|
-
}
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Check for duplicate entries (same platform/encoding/language/nameID)
|
|
126
|
-
seen = {}
|
|
127
|
-
name.name_records.each do |record|
|
|
128
|
-
key = [record.platform_id, record.encoding_id, record.language_id,
|
|
129
|
-
record.name_id]
|
|
130
|
-
if seen[key]
|
|
131
|
-
issues << {
|
|
132
|
-
severity: "warning",
|
|
133
|
-
category: "consistency",
|
|
134
|
-
message: "Duplicate name record: platform=#{record.platform_id}, encoding=#{record.encoding_id}, language=#{record.language_id}, nameID=#{record.name_id}",
|
|
135
|
-
location: "name table",
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
seen[key] = true
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
issues
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Check variable font table consistency
|
|
145
|
-
#
|
|
146
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
147
|
-
# @return [Array<Hash>] Array of variable font issues
|
|
148
|
-
def check_variable_consistency(font)
|
|
149
|
-
issues = []
|
|
150
|
-
|
|
151
|
-
# Only check if this is a variable font
|
|
152
|
-
return issues unless font.has_table?(Constants::FVAR_TAG)
|
|
153
|
-
|
|
154
|
-
fvar = font.table(Constants::FVAR_TAG)
|
|
155
|
-
return issues unless fvar
|
|
156
|
-
|
|
157
|
-
axis_count = fvar.axes.length
|
|
158
|
-
|
|
159
|
-
# For TrueType variable fonts, check gvar consistency
|
|
160
|
-
if font.has_table?(Constants::GVAR_TAG)
|
|
161
|
-
gvar = font.table(Constants::GVAR_TAG)
|
|
162
|
-
gvar_axis_count = gvar.axis_count
|
|
163
|
-
if gvar_axis_count != axis_count
|
|
164
|
-
issues << {
|
|
165
|
-
severity: "error",
|
|
166
|
-
category: "consistency",
|
|
167
|
-
message: "fvar axis count (#{axis_count}) doesn't match gvar axis count (#{gvar_axis_count})",
|
|
168
|
-
location: "fvar/gvar tables",
|
|
169
|
-
}
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Check that recommended variation tables are present
|
|
174
|
-
unless font.has_table?(Constants::GVAR_TAG) || font.has_table?(Constants::CFF2_TAG)
|
|
175
|
-
issues << {
|
|
176
|
-
severity: "error",
|
|
177
|
-
category: "consistency",
|
|
178
|
-
message: "Variable font missing gvar (TrueType) or CFF2 (CFF) table",
|
|
179
|
-
location: "variable font",
|
|
180
|
-
}
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Check for recommended metrics variation tables
|
|
184
|
-
unless font.has_table?(Constants::HVAR_TAG)
|
|
185
|
-
issues << {
|
|
186
|
-
severity: "info",
|
|
187
|
-
category: "consistency",
|
|
188
|
-
message: "Variable font missing HVAR table (recommended for better rendering)",
|
|
189
|
-
location: nil,
|
|
190
|
-
}
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
issues
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|