fontisan 0.4.35 → 0.4.36
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 +15 -3
- data/.rubocop_todo.yml +23 -11
- data/TODO.improvements/README.md +1 -1
- data/lib/fontisan/audit/check_registry.rb +12 -2
- data/lib/fontisan/audit/checks/format_round_trip_check.rb +150 -0
- data/lib/fontisan/audit/checks/hinting_check.rb +163 -0
- data/lib/fontisan/audit/checks/opentype_conformance_check.rb +159 -0
- data/lib/fontisan/audit/checks/variable_font_check.rb +151 -0
- data/lib/fontisan/audit/checks/woff2_validation_check.rb +140 -0
- data/lib/fontisan/audit/checks.rb +8 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3957074ba3c679c22715fa1f1aabf4dbd3cc7334891f729d64988ec231237f3b
|
|
4
|
+
data.tar.gz: 18129617714b672947d8f39a88e7c06a7644e4ff12875c383321635a6e051377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3315d7c7f3cd824dadcf4ecddc63319a5661ac13ebf8383dfab8f2eda3fffd2713a1a039df922d58073cc0b99615d0d608de5641f67528e48f2aa70678e5b8f1
|
|
7
|
+
data.tar.gz: 1bd98a99dc9902aeae6e5ceb3c40f16d01c7a61c16ae2c94f1c97f212602de2675602ab736545869640500dc49ac5f4bef6302bf3b43c34593071e94b513c448
|
data/.rubocop.yml
CHANGED
|
@@ -116,7 +116,19 @@ Style/BitwisePredicate:
|
|
|
116
116
|
Enabled: false
|
|
117
117
|
|
|
118
118
|
# Allow short math-conventional parameter names in font geometry code.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
119
|
+
# Font geometry / binary parsing uses single-letter variables by domain
|
|
120
|
+
# convention: affine matrix (a,b,c,d,e,f), loop indices (i,j,k,m,n),
|
|
121
|
+
# coordinates (x,y,z), deltas (dx,dy). Renaming to 3+ chars adds noise
|
|
122
|
+
# without aiding readers familiar with the math.
|
|
121
123
|
Naming/MethodParameterName:
|
|
122
|
-
AllowedNames: ["
|
|
124
|
+
AllowedNames: ["a", "b", "c", "d", "e", "f", "i", "j", "k", "m", "n",
|
|
125
|
+
"p", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
|
126
|
+
"cp", "gid", "dx", "dy", "fd", "io", "id"]
|
|
127
|
+
|
|
128
|
+
# Allow spec-format identifiers that match OpenType spec terminology:
|
|
129
|
+
# cmap format4, format6, format12, format14 are the official names.
|
|
130
|
+
# Renaming to format_4 diverges from the spec and makes docs harder
|
|
131
|
+
# to cross-reference.
|
|
132
|
+
Naming/VariableNumber:
|
|
133
|
+
AllowedIdentifiers: ["format0", "format4", "format6", "format12", "format14",
|
|
134
|
+
"version1", "version2", "table1", "table2"]
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-07-11
|
|
3
|
+
# on 2026-07-11 08:52:22 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -32,7 +32,7 @@ Layout/IndentationWidth:
|
|
|
32
32
|
- 'lib/fontisan/commands/audit_command.rb'
|
|
33
33
|
- 'lib/fontisan/ufo/compile/glyf_loca.rb'
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 2053
|
|
36
36
|
# This cop supports safe autocorrection (--autocorrect).
|
|
37
37
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
38
38
|
# URISchemes: http, https
|
|
@@ -111,7 +111,7 @@ Lint/UselessConstantScoping:
|
|
|
111
111
|
- 'lib/fontisan/conversion_options.rb'
|
|
112
112
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
113
113
|
|
|
114
|
-
# Offense count:
|
|
114
|
+
# Offense count: 766
|
|
115
115
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
116
116
|
Metrics/AbcSize:
|
|
117
117
|
Enabled: false
|
|
@@ -134,12 +134,12 @@ Metrics/CollectionLiteralLength:
|
|
|
134
134
|
- 'lib/fontisan/type1/agl.rb'
|
|
135
135
|
- 'lib/fontisan/type1/encodings.rb'
|
|
136
136
|
|
|
137
|
-
# Offense count:
|
|
137
|
+
# Offense count: 383
|
|
138
138
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
139
139
|
Metrics/CyclomaticComplexity:
|
|
140
140
|
Enabled: false
|
|
141
141
|
|
|
142
|
-
# Offense count:
|
|
142
|
+
# Offense count: 1045
|
|
143
143
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
144
144
|
Metrics/MethodLength:
|
|
145
145
|
Max: 135
|
|
@@ -150,16 +150,28 @@ Metrics/ParameterLists:
|
|
|
150
150
|
Max: 39
|
|
151
151
|
MaxOptionalParameters: 4
|
|
152
152
|
|
|
153
|
-
# Offense count:
|
|
153
|
+
# Offense count: 294
|
|
154
154
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
155
155
|
Metrics/PerceivedComplexity:
|
|
156
156
|
Enabled: false
|
|
157
157
|
|
|
158
|
-
# Offense count:
|
|
158
|
+
# Offense count: 34
|
|
159
159
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
160
160
|
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
161
161
|
Naming/MethodParameterName:
|
|
162
|
-
|
|
162
|
+
Exclude:
|
|
163
|
+
- 'lib/fontisan.rb'
|
|
164
|
+
- 'lib/fontisan/conversion_options.rb'
|
|
165
|
+
- 'lib/fontisan/optimizers/subroutine_optimizer.rb'
|
|
166
|
+
- 'lib/fontisan/svg_to_glyf/geometry/affine_transform.rb'
|
|
167
|
+
- 'lib/fontisan/svg_to_glyf/geometry/transform_parser.rb'
|
|
168
|
+
- 'lib/fontisan/type1/ttf_to_type1_converter.rb'
|
|
169
|
+
- 'lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb'
|
|
170
|
+
- 'lib/fontisan/ufo/convert.rb'
|
|
171
|
+
- 'lib/fontisan/variation/optimizer.rb'
|
|
172
|
+
- 'lib/fontisan/woff2/glyf_loca_reconstruct.rb'
|
|
173
|
+
- 'lib/fontisan/woff2/glyf_loca_transform.rb'
|
|
174
|
+
- 'spec/fontisan/subset/table_strategy/cff2_spec.rb'
|
|
163
175
|
|
|
164
176
|
# Offense count: 13
|
|
165
177
|
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
|
@@ -221,7 +233,7 @@ RSpec/AnyInstance:
|
|
|
221
233
|
RSpec/ContextWording:
|
|
222
234
|
Enabled: false
|
|
223
235
|
|
|
224
|
-
# Offense count:
|
|
236
|
+
# Offense count: 42
|
|
225
237
|
# Configuration parameters: IgnoredMetadata.
|
|
226
238
|
RSpec/DescribeClass:
|
|
227
239
|
Enabled: false
|
|
@@ -237,7 +249,7 @@ RSpec/DescribeMethod:
|
|
|
237
249
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
238
250
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
239
251
|
|
|
240
|
-
# Offense count:
|
|
252
|
+
# Offense count: 1729
|
|
241
253
|
# Configuration parameters: CountAsOne.
|
|
242
254
|
RSpec/ExampleLength:
|
|
243
255
|
Max: 66
|
|
@@ -315,7 +327,7 @@ RSpec/MultipleDescribes:
|
|
|
315
327
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
316
328
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
317
329
|
|
|
318
|
-
# Offense count:
|
|
330
|
+
# Offense count: 2153
|
|
319
331
|
RSpec/MultipleExpectations:
|
|
320
332
|
Max: 19
|
|
321
333
|
|
data/TODO.improvements/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
11
11
|
- [x] ~~[15 — CFF/CFF2 subsetter strategy](15-cff-cff2-subsetter-strategy.md)~~ ✓ Done — CFF via UFO round-trip, CFF2 via standalone INDEX filter with VStore preservation
|
|
12
12
|
|
|
13
13
|
### P1 — High-value features
|
|
14
|
-
- [x] ~~[03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)~~ ✓ Done (Phase 1: identity, style, coverage, layout, provenance; Phase 2 axes: table directory
|
|
14
|
+
- [x] ~~[03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)~~ ✓ Done (Phase 1: identity, style, coverage, layout, provenance; Phase 2 axes: table directory, glyph names, cmap, OTS, collection integrity; Phase 3 axes: variable-font, hinting, WOFF2, format-round-trip, OpenType conformance foundation; CLI `fontisan audit --validate` with profiles: default, structural, ots, layout, variable, hinting, web, spec)
|
|
15
15
|
- [x] ~~[04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)~~ ✓ Done
|
|
16
16
|
- [x] ~~[05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)~~ ✓ Already working (discovered in PR #117 — Cff.build produces real Type 2 charstrings; stale TODO marker removed)
|
|
17
17
|
|
|
@@ -18,10 +18,15 @@ module Fontisan
|
|
|
18
18
|
class CheckRegistry
|
|
19
19
|
PROFILES = {
|
|
20
20
|
default: %i[table_directory glyph_names cmap ots_compatibility
|
|
21
|
-
collection_integrity
|
|
22
|
-
|
|
21
|
+
collection_integrity variable_font hinting woff2_validation
|
|
22
|
+
format_round_trip opentype_conformance],
|
|
23
|
+
structural: %i[table_directory collection_integrity opentype_conformance],
|
|
23
24
|
ots: %i[ots_compatibility],
|
|
24
25
|
layout: %i[glyph_names cmap],
|
|
26
|
+
variable: %i[variable_font],
|
|
27
|
+
hinting: %i[hinting],
|
|
28
|
+
web: %i[ots_compatibility woff2_validation],
|
|
29
|
+
spec: %i[opentype_conformance],
|
|
25
30
|
}.freeze
|
|
26
31
|
|
|
27
32
|
# @param profile [Symbol]
|
|
@@ -40,6 +45,11 @@ module Fontisan
|
|
|
40
45
|
when :cmap then Checks::CmapCheck
|
|
41
46
|
when :ots_compatibility then Checks::OtsCompatibilityCheck
|
|
42
47
|
when :collection_integrity then Checks::CollectionIntegrityCheck
|
|
48
|
+
when :variable_font then Checks::VariableFontCheck
|
|
49
|
+
when :hinting then Checks::HintingCheck
|
|
50
|
+
when :woff2_validation then Checks::Woff2ValidationCheck
|
|
51
|
+
when :format_round_trip then Checks::FormatRoundTripCheck
|
|
52
|
+
when :opentype_conformance then Checks::OpenTypeConformanceCheck
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Audit
|
|
7
|
+
module Checks
|
|
8
|
+
# Reports fidelity discrepancies when converting between TTF and
|
|
9
|
+
# OTF outline formats. A high-fidelity conversion preserves:
|
|
10
|
+
#
|
|
11
|
+
# - Glyph count (maxp.numGlyphs)
|
|
12
|
+
# - Codepoint coverage (cmap Unicode mappings)
|
|
13
|
+
# - Font identity (name table family + PostScript name)
|
|
14
|
+
#
|
|
15
|
+
# Any mismatch signals a bug in the outline converter or a data
|
|
16
|
+
# loss path. This check is the pure-Ruby replacement for `ttx`
|
|
17
|
+
# diff comparisons.
|
|
18
|
+
#
|
|
19
|
+
# Only converts in one direction (TTF→OTF for TrueType sources,
|
|
20
|
+
# OTF→TTF for CFF sources) — full round-trip would double the
|
|
21
|
+
# conversion surface without adding signal.
|
|
22
|
+
#
|
|
23
|
+
# @see Converters::OutlineConverter
|
|
24
|
+
class FormatRoundTripCheck < Check
|
|
25
|
+
# @param font [SfntFont]
|
|
26
|
+
# @return [Array<Models::ValidationReport::Issue>]
|
|
27
|
+
def self.call(font)
|
|
28
|
+
target = round_trip_target(font)
|
|
29
|
+
return [] unless target
|
|
30
|
+
|
|
31
|
+
converted_path = convert_to_temp(font, target)
|
|
32
|
+
return [] unless converted_path
|
|
33
|
+
|
|
34
|
+
converted = FontLoader.load(converted_path)
|
|
35
|
+
compare_fonts(font, converted, target)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
[issue(severity: :warning,
|
|
38
|
+
message: "Round-trip conversion to #{target.upcase} failed: " \
|
|
39
|
+
"#{e.class}: #{e.message}",
|
|
40
|
+
location: "round_trip.#{target}")]
|
|
41
|
+
ensure
|
|
42
|
+
cleanup_temp(converted_path) if converted_path
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.code
|
|
46
|
+
:format_round_trip
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Decide the conversion target based on the source outline type.
|
|
50
|
+
# @return [Symbol, nil] :otf, :ttf, or nil if not convertible
|
|
51
|
+
def self.round_trip_target(font)
|
|
52
|
+
return :otf if font.has_table?("glyf")
|
|
53
|
+
return :ttf if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
private_class_method :round_trip_target
|
|
58
|
+
|
|
59
|
+
def self.convert_to_temp(font, target)
|
|
60
|
+
converter = Converters::FormatConverter.new
|
|
61
|
+
tables = converter.convert(font, target)
|
|
62
|
+
return nil unless tables && !tables.empty?
|
|
63
|
+
|
|
64
|
+
dir = Dir.mktmpdir("fontisan-roundtrip-")
|
|
65
|
+
path = File.join(dir, "converted.#{target}")
|
|
66
|
+
sfnt = target == :otf ? 0x4F54544F : 0x00010000
|
|
67
|
+
FontWriter.write_to_file(
|
|
68
|
+
tables.transform_values { |t| t.is_a?(String) ? t : t.to_binary_s },
|
|
69
|
+
path,
|
|
70
|
+
sfnt_version: sfnt,
|
|
71
|
+
)
|
|
72
|
+
File.exist?(path) ? path : nil
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
private_class_method :convert_to_temp
|
|
77
|
+
|
|
78
|
+
def self.compare_fonts(source, converted, target)
|
|
79
|
+
issues = []
|
|
80
|
+
issues.concat(compare_glyph_count(source, converted, target))
|
|
81
|
+
issues.concat(compare_codepoints(source, converted, target))
|
|
82
|
+
issues
|
|
83
|
+
end
|
|
84
|
+
private_class_method :compare_fonts
|
|
85
|
+
|
|
86
|
+
def self.compare_glyph_count(source, converted, target)
|
|
87
|
+
src_count = glyph_count(source)
|
|
88
|
+
dst_count = glyph_count(converted)
|
|
89
|
+
return [] if src_count == dst_count
|
|
90
|
+
|
|
91
|
+
[issue(severity: :warning,
|
|
92
|
+
message: "Round-trip to #{target.upcase}: glyph count changed " \
|
|
93
|
+
"from #{src_count} to #{dst_count}",
|
|
94
|
+
location: "round_trip.#{target}.glyph_count")]
|
|
95
|
+
end
|
|
96
|
+
private_class_method :compare_glyph_count
|
|
97
|
+
|
|
98
|
+
def self.compare_codepoints(source, converted, target)
|
|
99
|
+
src_cps = codepoint_set(source)
|
|
100
|
+
dst_cps = codepoint_set(converted)
|
|
101
|
+
missing = src_cps - dst_cps
|
|
102
|
+
extra = dst_cps - src_cps
|
|
103
|
+
issues = []
|
|
104
|
+
unless missing.empty?
|
|
105
|
+
issues << issue(severity: :warning,
|
|
106
|
+
message: "Round-trip to #{target.upcase}: #{missing.size} " \
|
|
107
|
+
"codepoints lost (first: " \
|
|
108
|
+
"#{missing.first(5).map { |c| format('U+%04X', c) }.join(', ')})",
|
|
109
|
+
location: "round_trip.#{target}.codepoints.missing")
|
|
110
|
+
end
|
|
111
|
+
unless extra.empty?
|
|
112
|
+
issues << issue(severity: :info,
|
|
113
|
+
message: "Round-trip to #{target.upcase}: #{extra.size} " \
|
|
114
|
+
"codepoints gained (first: " \
|
|
115
|
+
"#{extra.first(5).map { |c| format('U+%04X', c) }.join(', ')})",
|
|
116
|
+
location: "round_trip.#{target}.codepoints.extra")
|
|
117
|
+
end
|
|
118
|
+
issues
|
|
119
|
+
end
|
|
120
|
+
private_class_method :compare_codepoints
|
|
121
|
+
|
|
122
|
+
def self.glyph_count(font)
|
|
123
|
+
return 0 unless font.has_table?("maxp")
|
|
124
|
+
|
|
125
|
+
font.table("maxp").num_glyphs.to_i
|
|
126
|
+
rescue StandardError
|
|
127
|
+
0
|
|
128
|
+
end
|
|
129
|
+
private_class_method :glyph_count
|
|
130
|
+
|
|
131
|
+
def self.codepoint_set(font)
|
|
132
|
+
return Set.new unless font.has_table?("cmap")
|
|
133
|
+
|
|
134
|
+
Set.new(font.table("cmap").unicode_mappings&.keys || [])
|
|
135
|
+
rescue StandardError
|
|
136
|
+
Set.new
|
|
137
|
+
end
|
|
138
|
+
private_class_method :codepoint_set
|
|
139
|
+
|
|
140
|
+
def self.cleanup_temp(path)
|
|
141
|
+
dir = File.dirname(path)
|
|
142
|
+
FileUtils.rm_rf(dir) if Dir.exist?(dir)
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
private_class_method :cleanup_temp
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Audits font hinting configuration. Hinting quality directly
|
|
7
|
+
# affects small-size screen rendering (Windows, Android, embedded
|
|
8
|
+
# systems).
|
|
9
|
+
#
|
|
10
|
+
# TrueType (glyf outlines):
|
|
11
|
+
# - fpgm (font program) presence recommended for hinted fonts
|
|
12
|
+
# - prep (control value program) presence recommended
|
|
13
|
+
# - cvt (control value table) presence recommended
|
|
14
|
+
# - maxp.maxStackElements and maxp.maxZones must be positive
|
|
15
|
+
# if any hinting instructions exist
|
|
16
|
+
# - gasp table recommended for controlling grid-fitting per ppem
|
|
17
|
+
#
|
|
18
|
+
# CFF (PostScript outlines):
|
|
19
|
+
# - Private DICT should have BlueValues or StdHW/StdVW for hinting
|
|
20
|
+
# - Subroutine usage (Local/Global subrs) recommended for size
|
|
21
|
+
#
|
|
22
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/ttinst#hinting-instructions
|
|
23
|
+
class HintingCheck < Check
|
|
24
|
+
# @param font [SfntFont]
|
|
25
|
+
# @return [Array<Models::ValidationReport::Issue>]
|
|
26
|
+
def self.call(font)
|
|
27
|
+
if font.has_table?("glyf")
|
|
28
|
+
validate_truetype_hinting(font)
|
|
29
|
+
elsif font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
30
|
+
validate_cff_hinting(font)
|
|
31
|
+
else
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.code
|
|
37
|
+
:hinting
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# ---------- TrueType hinting ----------
|
|
41
|
+
|
|
42
|
+
def self.validate_truetype_hinting(font)
|
|
43
|
+
issues = []
|
|
44
|
+
has_fpgm = font.has_table?("fpgm")
|
|
45
|
+
has_prep = font.has_table?("prep")
|
|
46
|
+
has_cvt = font.has_table?("cvt")
|
|
47
|
+
has_glyf_data = table_has_data?(font, "glyf")
|
|
48
|
+
|
|
49
|
+
if has_fpgm && has_glyf_data
|
|
50
|
+
issues.concat(validate_maxp_hint_capacity(font))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
issues << no_program_warning("prep") if !has_prep && has_fpgm
|
|
54
|
+
issues << no_program_warning("cvt") if !has_cvt && has_fpgm
|
|
55
|
+
|
|
56
|
+
unless font.has_table?("gasp")
|
|
57
|
+
issues << issue(severity: :info,
|
|
58
|
+
message: "TrueType font has no 'gasp' table — " \
|
|
59
|
+
"grid-fitting behavior per ppem is " \
|
|
60
|
+
"implementation-defined",
|
|
61
|
+
location: "tables.gasp")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
issues
|
|
65
|
+
end
|
|
66
|
+
private_class_method :validate_truetype_hinting
|
|
67
|
+
|
|
68
|
+
def self.validate_maxp_hint_capacity(font)
|
|
69
|
+
issues = []
|
|
70
|
+
maxp = font.table("maxp")
|
|
71
|
+
return issues unless maxp.version_1_0?
|
|
72
|
+
|
|
73
|
+
stack = maxp.max_stack_elements.to_i
|
|
74
|
+
zones = maxp.max_zones.to_i
|
|
75
|
+
if has_fpgm_instructions?(font) && stack.zero?
|
|
76
|
+
issues << issue(severity: :warning,
|
|
77
|
+
message: "maxp.maxStackElements is 0 but the font " \
|
|
78
|
+
"has hinting instructions — rendering " \
|
|
79
|
+
"may fail",
|
|
80
|
+
location: "maxp.max_stack_elements")
|
|
81
|
+
end
|
|
82
|
+
unless zones.between?(1, 2)
|
|
83
|
+
issues << issue(severity: :warning,
|
|
84
|
+
message: "maxp.maxZones is #{zones} but must be 1 or 2 " \
|
|
85
|
+
"per the OpenType spec",
|
|
86
|
+
location: "maxp.max_zones")
|
|
87
|
+
end
|
|
88
|
+
issues
|
|
89
|
+
end
|
|
90
|
+
private_class_method :validate_maxp_hint_capacity
|
|
91
|
+
|
|
92
|
+
def self.has_fpgm_instructions?(font)
|
|
93
|
+
table_has_data?(font, "fpgm")
|
|
94
|
+
end
|
|
95
|
+
private_class_method :has_fpgm_instructions?
|
|
96
|
+
|
|
97
|
+
def self.no_program_warning(table)
|
|
98
|
+
issue(severity: :info,
|
|
99
|
+
message: "Font has 'fpgm' but no '#{table}' table — " \
|
|
100
|
+
"#{table == 'cvt' ? 'control values' : 'pre-program'} " \
|
|
101
|
+
"are recommended for consistent hinting",
|
|
102
|
+
location: "tables.#{table}")
|
|
103
|
+
end
|
|
104
|
+
private_class_method :no_program_warning
|
|
105
|
+
|
|
106
|
+
# ---------- CFF hinting ----------
|
|
107
|
+
|
|
108
|
+
def self.validate_cff_hinting(font)
|
|
109
|
+
issues = []
|
|
110
|
+
tag = font.has_table?("CFF2") ? "CFF2" : "CFF "
|
|
111
|
+
cff = font.table(tag)
|
|
112
|
+
return issues unless cff
|
|
113
|
+
|
|
114
|
+
priv = begin
|
|
115
|
+
cff.private_dict(0)
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
return issues unless priv
|
|
120
|
+
|
|
121
|
+
has_blues = priv_values?(priv, :blue_values) || priv_values?(priv, :other_blues)
|
|
122
|
+
has_stems = priv_value?(priv, :std_hw) || priv_value?(priv, :std_vw)
|
|
123
|
+
|
|
124
|
+
if !has_blues && !has_stems
|
|
125
|
+
issues << issue(severity: :info,
|
|
126
|
+
message: "CFF Private DICT has no BlueValues or " \
|
|
127
|
+
"StdHW/StdVW — alignment zones and stem " \
|
|
128
|
+
"widths are the primary hinting mechanism " \
|
|
129
|
+
"for CFF outlines",
|
|
130
|
+
location: "cff.private_dict")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
issues
|
|
134
|
+
end
|
|
135
|
+
private_class_method :validate_cff_hinting
|
|
136
|
+
|
|
137
|
+
def self.priv_values?(priv, key)
|
|
138
|
+
val = priv[key] || priv[key.to_s]
|
|
139
|
+
val.is_a?(Array) ? val.any? : false
|
|
140
|
+
rescue StandardError
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
private_class_method :priv_values?
|
|
144
|
+
|
|
145
|
+
def self.priv_value?(priv, key)
|
|
146
|
+
val = priv[key] || priv[key.to_s]
|
|
147
|
+
val && !val.zero?
|
|
148
|
+
rescue StandardError
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
private_class_method :priv_value?
|
|
152
|
+
|
|
153
|
+
# ---------- helpers ----------
|
|
154
|
+
|
|
155
|
+
def self.table_has_data?(font, tag)
|
|
156
|
+
raw = font.table_data[tag]
|
|
157
|
+
raw && !raw.empty?
|
|
158
|
+
end
|
|
159
|
+
private_class_method :table_has_data?
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Foundational OpenType spec conformance checks. Covers the
|
|
7
|
+
# most commonly-violated "MUST" and "SHOULD" rules from the OT
|
|
8
|
+
# spec that other check domains don't already cover.
|
|
9
|
+
#
|
|
10
|
+
# This check is intentionally a growing foundation — new OT spec
|
|
11
|
+
# rules are added incrementally as real-world fonts surface them.
|
|
12
|
+
# Full OT conformance is a long-running effort that tracks the
|
|
13
|
+
# spec's evolution (axis 14 per TODO #03).
|
|
14
|
+
#
|
|
15
|
+
# Current checks:
|
|
16
|
+
#
|
|
17
|
+
# - Required tables for each sfnt flavor (TTF vs CFF vs variable)
|
|
18
|
+
# - head.indexToLocFormat consistency with loca table size
|
|
19
|
+
# - name table must have at least family (1), subfamily (2),
|
|
20
|
+
# full (4), PostScript (6) name IDs
|
|
21
|
+
# - OS/2 fsSelection must not use reserved bits
|
|
22
|
+
# - hhea.numberOfHMetrics must be ≤ maxp.numGlyphs
|
|
23
|
+
# - post table version must be valid
|
|
24
|
+
# - cmap must have at least one Unicode subtable (already
|
|
25
|
+
# covered by CmapCheck — skipped here to avoid duplicate issues)
|
|
26
|
+
#
|
|
27
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/otff
|
|
28
|
+
class OpenTypeConformanceCheck < Check
|
|
29
|
+
REQUIRED_NAME_IDS = {
|
|
30
|
+
1 => "Family",
|
|
31
|
+
2 => "Subfamily",
|
|
32
|
+
4 => "Full",
|
|
33
|
+
6 => "PostScript",
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# @param font [SfntFont]
|
|
37
|
+
# @return [Array<Models::ValidationReport::Issue>]
|
|
38
|
+
def self.call(font)
|
|
39
|
+
issues = []
|
|
40
|
+
issues.concat(validate_required_tables(font))
|
|
41
|
+
issues.concat(validate_loca_format(font))
|
|
42
|
+
issues.concat(validate_name_coverage(font))
|
|
43
|
+
issues.concat(validate_os2_fsselection(font))
|
|
44
|
+
issues.concat(validate_hhea_metrics(font))
|
|
45
|
+
issues
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.code
|
|
49
|
+
:opentype_conformance
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ---------- required tables ----------
|
|
53
|
+
|
|
54
|
+
def self.validate_required_tables(font)
|
|
55
|
+
required = required_tables_for_flavor(font)
|
|
56
|
+
missing = required.reject { |tag| font.has_table?(tag) }
|
|
57
|
+
missing.map do |tag|
|
|
58
|
+
issue(severity: :error,
|
|
59
|
+
message: "Required table '#{tag}' is missing for this " \
|
|
60
|
+
"sfnt flavor",
|
|
61
|
+
location: "tables.#{tag}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
private_class_method :validate_required_tables
|
|
65
|
+
|
|
66
|
+
def self.required_tables_for_flavor(font)
|
|
67
|
+
base = %w[head hhea maxp hmtx name post cmap OS/2]
|
|
68
|
+
if font.has_table?("glyf")
|
|
69
|
+
base + %w[glyf loca]
|
|
70
|
+
elsif font.has_table?("CFF2")
|
|
71
|
+
base + ["CFF2"]
|
|
72
|
+
elsif font.has_table?("CFF ")
|
|
73
|
+
base + ["CFF "]
|
|
74
|
+
else
|
|
75
|
+
base
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_class_method :required_tables_for_flavor
|
|
79
|
+
|
|
80
|
+
# ---------- loca format ----------
|
|
81
|
+
|
|
82
|
+
def self.validate_loca_format(font)
|
|
83
|
+
return [] unless font.has_table?("head") && font.has_table?("loca")
|
|
84
|
+
|
|
85
|
+
head = font.table("head")
|
|
86
|
+
loca = font.table("loca")
|
|
87
|
+
return [] unless head && loca
|
|
88
|
+
|
|
89
|
+
format = head.index_to_loc_format
|
|
90
|
+
maxp = font.table("maxp")
|
|
91
|
+
num_glyphs = maxp&.num_glyphs.to_i
|
|
92
|
+
return [] if num_glyphs.zero?
|
|
93
|
+
|
|
94
|
+
expected_size = format.zero? ? (num_glyphs + 1) * 2 : (num_glyphs + 1) * 4
|
|
95
|
+
actual_size = loca.to_binary_s.bytesize
|
|
96
|
+
return [] if actual_size == expected_size
|
|
97
|
+
|
|
98
|
+
[issue(severity: :warning,
|
|
99
|
+
message: "loca table size (#{actual_size} bytes) doesn't match " \
|
|
100
|
+
"head.indexToLocFormat=#{format} + numGlyphs=#{num_glyphs} " \
|
|
101
|
+
"(expected #{expected_size} bytes)",
|
|
102
|
+
location: "head.index_to_loc_format")]
|
|
103
|
+
end
|
|
104
|
+
private_class_method :validate_loca_format
|
|
105
|
+
|
|
106
|
+
# ---------- name table coverage ----------
|
|
107
|
+
|
|
108
|
+
def self.validate_name_coverage(font)
|
|
109
|
+
return [] unless font.has_table?("name")
|
|
110
|
+
|
|
111
|
+
name = font.table("name")
|
|
112
|
+
REQUIRED_NAME_IDS.each_with_object([]) do |(id, label), issues|
|
|
113
|
+
val = name.english_name(id).to_s
|
|
114
|
+
next unless val.empty?
|
|
115
|
+
|
|
116
|
+
issues << issue(severity: :error,
|
|
117
|
+
message: "Required name ID #{id} (#{label}) is " \
|
|
118
|
+
"missing from the name table",
|
|
119
|
+
location: "name.nameID.#{id}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_class_method :validate_name_coverage
|
|
123
|
+
|
|
124
|
+
# ---------- OS/2 fsSelection reserved bits ----------
|
|
125
|
+
|
|
126
|
+
def self.validate_os2_fsselection(font)
|
|
127
|
+
return [] unless font.has_table?("OS/2")
|
|
128
|
+
|
|
129
|
+
os2 = font.table("OS/2")
|
|
130
|
+
fs = os2.fs_selection.to_i
|
|
131
|
+
reserved_mask = 0xFC00 # bits 10-15 are reserved
|
|
132
|
+
return [] if (fs & reserved_mask).zero?
|
|
133
|
+
|
|
134
|
+
[issue(severity: :warning,
|
|
135
|
+
message: "OS/2 fsSelection uses reserved bits " \
|
|
136
|
+
"(value 0x#{fs.to_s(16)}, bits 10-15 must be 0)",
|
|
137
|
+
location: "os2.fs_selection")]
|
|
138
|
+
end
|
|
139
|
+
private_class_method :validate_os2_fsselection
|
|
140
|
+
|
|
141
|
+
# ---------- hhea.numberOfHMetrics ----------
|
|
142
|
+
|
|
143
|
+
def self.validate_hhea_metrics(font)
|
|
144
|
+
return [] unless font.has_table?("hhea") && font.has_table?("maxp")
|
|
145
|
+
|
|
146
|
+
num_metrics = font.table("hhea").number_of_h_metrics.to_i
|
|
147
|
+
num_glyphs = font.table("maxp").num_glyphs.to_i
|
|
148
|
+
return [] if num_metrics.positive? && num_metrics <= num_glyphs
|
|
149
|
+
|
|
150
|
+
[issue(severity: :error,
|
|
151
|
+
message: "hhea.numberOfHMetrics (#{num_metrics}) must be " \
|
|
152
|
+
"between 1 and maxp.numGlyphs (#{num_glyphs})",
|
|
153
|
+
location: "hhea.number_of_h_metrics")]
|
|
154
|
+
end
|
|
155
|
+
private_class_method :validate_hhea_metrics
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates variable-font readiness: fvar axis invariants, named
|
|
7
|
+
# instances within ranges, companion-table presence (gvar for
|
|
8
|
+
# TrueType outlines, HVAR for horizontal metrics, STAT for OS
|
|
9
|
+
# style matching). Ports the most commonly-hit fontbakery GF-VF
|
|
10
|
+
# check subset.
|
|
11
|
+
#
|
|
12
|
+
# Only runs when the font HAS an fvar table — static fonts are
|
|
13
|
+
# skipped (returns empty issues array).
|
|
14
|
+
#
|
|
15
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/fvar
|
|
16
|
+
class VariableFontCheck < Check
|
|
17
|
+
# @param font [SfntFont]
|
|
18
|
+
# @return [Array<Models::ValidationReport::Issue>]
|
|
19
|
+
def self.call(font)
|
|
20
|
+
return [] unless font.has_table?("fvar")
|
|
21
|
+
|
|
22
|
+
issues = []
|
|
23
|
+
issues.concat(validate_axes(font))
|
|
24
|
+
issues.concat(validate_instances(font))
|
|
25
|
+
issues.concat(validate_companion_tables(font))
|
|
26
|
+
issues
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.code
|
|
30
|
+
:variable_font
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ---------- fvar axes ----------
|
|
34
|
+
|
|
35
|
+
def self.validate_axes(font)
|
|
36
|
+
fvar = font.table("fvar")
|
|
37
|
+
axes = fvar.axes
|
|
38
|
+
issues = []
|
|
39
|
+
|
|
40
|
+
issues.concat(check_axis_uniqueness(axes))
|
|
41
|
+
axes.each_with_index { |axis, idx| issues.concat(check_one_axis(axis, idx)) }
|
|
42
|
+
issues
|
|
43
|
+
end
|
|
44
|
+
private_class_method :validate_axes
|
|
45
|
+
|
|
46
|
+
def self.check_axis_uniqueness(axes)
|
|
47
|
+
tags = axes.map(&:axis_tag)
|
|
48
|
+
duplicates = tags.tally.filter_map { |t, c| t if c > 1 }
|
|
49
|
+
duplicates.map do |tag|
|
|
50
|
+
issue(severity: :error,
|
|
51
|
+
message: "Duplicate fvar axis tag '#{tag}' — axis tags " \
|
|
52
|
+
"must be unique within a font",
|
|
53
|
+
location: "fvar.axes.#{tag}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_class_method :check_axis_uniqueness
|
|
57
|
+
|
|
58
|
+
def self.check_one_axis(axis, idx)
|
|
59
|
+
issues = []
|
|
60
|
+
min_v = axis.min_value.to_f
|
|
61
|
+
default_v = axis.default_value.to_f
|
|
62
|
+
max_v = axis.max_value.to_f
|
|
63
|
+
|
|
64
|
+
unless min_v <= default_v && default_v <= max_v
|
|
65
|
+
issues << issue(severity: :error,
|
|
66
|
+
message: "fvar axis '#{axis.axis_tag}' (#{idx}) has " \
|
|
67
|
+
"invalid range: min=#{min_v} " \
|
|
68
|
+
"default=#{default_v} max=#{max_v} " \
|
|
69
|
+
"(must be min ≤ default ≤ max)",
|
|
70
|
+
location: "fvar.axes.#{idx}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if axis.axis_name_id.to_i.zero?
|
|
74
|
+
issues << issue(severity: :warning,
|
|
75
|
+
message: "fvar axis '#{axis.axis_tag}' (#{idx}) has " \
|
|
76
|
+
"no name ID — users cannot identify the axis",
|
|
77
|
+
location: "fvar.axes.#{idx}.axis_name_id")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
issues
|
|
81
|
+
end
|
|
82
|
+
private_class_method :check_one_axis
|
|
83
|
+
|
|
84
|
+
# ---------- fvar named instances ----------
|
|
85
|
+
|
|
86
|
+
def self.validate_instances(font)
|
|
87
|
+
fvar = font.table("fvar")
|
|
88
|
+
instances = fvar.instances || []
|
|
89
|
+
axes = fvar.axes
|
|
90
|
+
|
|
91
|
+
instances.each_with_index.with_object([]) do |(inst, idx), issues|
|
|
92
|
+
issues.concat(check_instance_ranges(inst, idx, axes))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
private_class_method :validate_instances
|
|
96
|
+
|
|
97
|
+
def self.check_instance_ranges(inst, idx, axes)
|
|
98
|
+
issues = []
|
|
99
|
+
coords = inst[:coordinates] || inst["coordinates"] || []
|
|
100
|
+
coords.each_with_index do |coord, axis_idx|
|
|
101
|
+
next unless axes[axis_idx]
|
|
102
|
+
|
|
103
|
+
axis = axes[axis_idx]
|
|
104
|
+
min_v = axis.min_value.to_f
|
|
105
|
+
max_v = axis.max_value.to_f
|
|
106
|
+
next if coord.to_f.between?(min_v, max_v)
|
|
107
|
+
|
|
108
|
+
issues << issue(severity: :warning,
|
|
109
|
+
message: "fvar instance #{idx} has coordinate " \
|
|
110
|
+
"#{coord} for axis '#{axis.axis_tag}' " \
|
|
111
|
+
"outside its range [#{min_v}, #{max_v}]",
|
|
112
|
+
location: "fvar.instances.#{idx}.#{axis.axis_tag}")
|
|
113
|
+
end
|
|
114
|
+
issues
|
|
115
|
+
end
|
|
116
|
+
private_class_method :check_instance_ranges
|
|
117
|
+
|
|
118
|
+
# ---------- companion tables ----------
|
|
119
|
+
|
|
120
|
+
def self.validate_companion_tables(font)
|
|
121
|
+
issues = []
|
|
122
|
+
is_truetype = font.has_table?("glyf")
|
|
123
|
+
if is_truetype && !font.has_table?("gvar")
|
|
124
|
+
issues << issue(severity: :warning,
|
|
125
|
+
message: "Variable TrueType font has no 'gvar' table — " \
|
|
126
|
+
"glyph outlines won't vary across the design space",
|
|
127
|
+
location: "tables.gvar")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless font.has_table?("HVAR")
|
|
131
|
+
issues << issue(severity: :warning,
|
|
132
|
+
message: "Variable font has no 'HVAR' table — " \
|
|
133
|
+
"advance widths won't vary; renderers must " \
|
|
134
|
+
"fall back to per-glyph hmtx lookups",
|
|
135
|
+
location: "tables.HVAR")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unless font.has_table?("STAT")
|
|
139
|
+
issues << issue(severity: :info,
|
|
140
|
+
message: "Variable font has no 'STAT' table — " \
|
|
141
|
+
"OS style-matching won't work; Windows and " \
|
|
142
|
+
"modern font pickers rely on STAT",
|
|
143
|
+
location: "tables.STAT")
|
|
144
|
+
end
|
|
145
|
+
issues
|
|
146
|
+
end
|
|
147
|
+
private_class_method :validate_companion_tables
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates WOFF2 (Web Open Font Format 2) wrapper structure per
|
|
7
|
+
# the W3C WOFF2 spec. Only runs when the font IS a Woff2Font —
|
|
8
|
+
# plain TTF/OTF inputs are skipped.
|
|
9
|
+
#
|
|
10
|
+
# Checks:
|
|
11
|
+
#
|
|
12
|
+
# - Signature must be 0x774F4632 ('wOF2')
|
|
13
|
+
# - Flavor must be a valid SFNT version
|
|
14
|
+
# - reserved field must be 0
|
|
15
|
+
# - major_version should be 1
|
|
16
|
+
# - total_compressed_size must be positive
|
|
17
|
+
# - If metadata block is present, lengths must be consistent
|
|
18
|
+
# - num_tables must match the table directory count
|
|
19
|
+
#
|
|
20
|
+
# @see https://www.w3.org/TR/WOFF2/
|
|
21
|
+
class Woff2ValidationCheck < Check
|
|
22
|
+
WOFF2_SIGNATURE = 0x774F4632
|
|
23
|
+
VALID_FLAVORS = [
|
|
24
|
+
0x00010000, # TrueType
|
|
25
|
+
0x74727565, # 'true' (Apple TrueType)
|
|
26
|
+
0x4F54544F, # 'OTTO' (OpenType CFF)
|
|
27
|
+
0x74746366, # 'ttcf' (collection)
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# @param font [Woff2Font, SfntFont]
|
|
31
|
+
# @return [Array<Models::ValidationReport::Issue>]
|
|
32
|
+
def self.call(font)
|
|
33
|
+
return [] unless woff2?(font)
|
|
34
|
+
|
|
35
|
+
header = font.header
|
|
36
|
+
issues = []
|
|
37
|
+
issues.concat(validate_signature(header))
|
|
38
|
+
issues.concat(validate_flavor(header))
|
|
39
|
+
issues.concat(validate_reserved(header))
|
|
40
|
+
issues.concat(validate_version(header))
|
|
41
|
+
issues.concat(validate_sizes(header))
|
|
42
|
+
issues.concat(validate_metadata(header))
|
|
43
|
+
issues
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.code
|
|
47
|
+
:woff2_validation
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.woff2?(font)
|
|
51
|
+
font.is_a?(Woff2Font)
|
|
52
|
+
end
|
|
53
|
+
private_class_method :woff2?
|
|
54
|
+
|
|
55
|
+
def self.validate_signature(header)
|
|
56
|
+
return [] if header.signature == WOFF2_SIGNATURE
|
|
57
|
+
|
|
58
|
+
[issue(severity: :error,
|
|
59
|
+
message: "WOFF2 signature is 0x#{header.signature.to_s(16)} " \
|
|
60
|
+
"but must be 0x#{WOFF2_SIGNATURE.to_s(16)} ('wOF2')",
|
|
61
|
+
location: "woff2_header.signature")]
|
|
62
|
+
end
|
|
63
|
+
private_class_method :validate_signature
|
|
64
|
+
|
|
65
|
+
def self.validate_flavor(header)
|
|
66
|
+
return [] if VALID_FLAVORS.include?(header.flavor)
|
|
67
|
+
|
|
68
|
+
[issue(severity: :error,
|
|
69
|
+
message: "WOFF2 flavor is 0x#{header.flavor.to_s(16)} " \
|
|
70
|
+
"but must be a valid SFNT version " \
|
|
71
|
+
"(0x00010000, 0x74727565, 0x4F54544F, or 0x74746366)",
|
|
72
|
+
location: "woff2_header.flavor")]
|
|
73
|
+
end
|
|
74
|
+
private_class_method :validate_flavor
|
|
75
|
+
|
|
76
|
+
def self.validate_reserved(header)
|
|
77
|
+
return [] if header.reserved.zero?
|
|
78
|
+
|
|
79
|
+
[issue(severity: :warning,
|
|
80
|
+
message: "WOFF2 reserved field is #{header.reserved} " \
|
|
81
|
+
"but must be 0 per the spec",
|
|
82
|
+
location: "woff2_header.reserved")]
|
|
83
|
+
end
|
|
84
|
+
private_class_method :validate_reserved
|
|
85
|
+
|
|
86
|
+
def self.validate_version(header)
|
|
87
|
+
issues = []
|
|
88
|
+
if header.major_version != 1
|
|
89
|
+
issues << issue(severity: :warning,
|
|
90
|
+
message: "WOFF2 major version is #{header.major_version} " \
|
|
91
|
+
"but the spec recommends 1",
|
|
92
|
+
location: "woff2_header.major_version")
|
|
93
|
+
end
|
|
94
|
+
issues
|
|
95
|
+
end
|
|
96
|
+
private_class_method :validate_version
|
|
97
|
+
|
|
98
|
+
def self.validate_sizes(header)
|
|
99
|
+
issues = []
|
|
100
|
+
if header.total_compressed_size.to_i <= 0
|
|
101
|
+
issues << issue(severity: :error,
|
|
102
|
+
message: "WOFF2 total_compressed_size is " \
|
|
103
|
+
"#{header.total_compressed_size} but must be positive",
|
|
104
|
+
location: "woff2_header.total_compressed_size")
|
|
105
|
+
end
|
|
106
|
+
if header.total_sfnt_size.to_i <= 0
|
|
107
|
+
issues << issue(severity: :error,
|
|
108
|
+
message: "WOFF2 total_sfnt_size is " \
|
|
109
|
+
"#{header.total_sfnt_size} but must be positive",
|
|
110
|
+
location: "woff2_header.total_sfnt_size")
|
|
111
|
+
end
|
|
112
|
+
issues
|
|
113
|
+
end
|
|
114
|
+
private_class_method :validate_sizes
|
|
115
|
+
|
|
116
|
+
def self.validate_metadata(header)
|
|
117
|
+
return [] unless header.meta_offset.to_i.positive?
|
|
118
|
+
|
|
119
|
+
issues = []
|
|
120
|
+
if header.meta_length.to_i <= 0
|
|
121
|
+
issues << issue(severity: :error,
|
|
122
|
+
message: "WOFF2 metadata offset is set but " \
|
|
123
|
+
"meta_length is #{header.meta_length} " \
|
|
124
|
+
"(must be positive)",
|
|
125
|
+
location: "woff2_header.meta_length")
|
|
126
|
+
end
|
|
127
|
+
if header.meta_orig_length.to_i <= 0
|
|
128
|
+
issues << issue(severity: :error,
|
|
129
|
+
message: "WOFF2 metadata offset is set but " \
|
|
130
|
+
"meta_orig_length is #{header.meta_orig_length} " \
|
|
131
|
+
"(must be positive)",
|
|
132
|
+
location: "woff2_header.meta_orig_length")
|
|
133
|
+
end
|
|
134
|
+
issues
|
|
135
|
+
end
|
|
136
|
+
private_class_method :validate_metadata
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -17,6 +17,14 @@ module Fontisan
|
|
|
17
17
|
"fontisan/audit/checks/ots_compatibility_check"
|
|
18
18
|
autoload :CollectionIntegrityCheck,
|
|
19
19
|
"fontisan/audit/checks/collection_integrity_check"
|
|
20
|
+
autoload :VariableFontCheck, "fontisan/audit/checks/variable_font_check"
|
|
21
|
+
autoload :HintingCheck, "fontisan/audit/checks/hinting_check"
|
|
22
|
+
autoload :Woff2ValidationCheck,
|
|
23
|
+
"fontisan/audit/checks/woff2_validation_check"
|
|
24
|
+
autoload :FormatRoundTripCheck,
|
|
25
|
+
"fontisan/audit/checks/format_round_trip_check"
|
|
26
|
+
autoload :OpenTypeConformanceCheck,
|
|
27
|
+
"fontisan/audit/checks/opentype_conformance_check"
|
|
20
28
|
end
|
|
21
29
|
end
|
|
22
30
|
end
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.36
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -299,9 +299,14 @@ files:
|
|
|
299
299
|
- lib/fontisan/audit/checks.rb
|
|
300
300
|
- lib/fontisan/audit/checks/cmap_check.rb
|
|
301
301
|
- lib/fontisan/audit/checks/collection_integrity_check.rb
|
|
302
|
+
- lib/fontisan/audit/checks/format_round_trip_check.rb
|
|
302
303
|
- lib/fontisan/audit/checks/glyph_name_check.rb
|
|
304
|
+
- lib/fontisan/audit/checks/hinting_check.rb
|
|
305
|
+
- lib/fontisan/audit/checks/opentype_conformance_check.rb
|
|
303
306
|
- lib/fontisan/audit/checks/ots_compatibility_check.rb
|
|
304
307
|
- lib/fontisan/audit/checks/table_directory_check.rb
|
|
308
|
+
- lib/fontisan/audit/checks/variable_font_check.rb
|
|
309
|
+
- lib/fontisan/audit/checks/woff2_validation_check.rb
|
|
305
310
|
- lib/fontisan/base_collection.rb
|
|
306
311
|
- lib/fontisan/binary.rb
|
|
307
312
|
- lib/fontisan/binary/base_record.rb
|