fontisan 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +103 -0
- data/.rubocop_todo.yml +107 -318
- data/Gemfile +1 -1
- data/README.adoc +127 -17
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- data/lib/fontisan/base_collection.rb +5 -33
- data/lib/fontisan/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- data/lib/fontisan/collection/shared_logic.rb +54 -0
- data/lib/fontisan/commands/convert_command.rb +2 -4
- data/lib/fontisan/commands/info_command.rb +3 -3
- data/lib/fontisan/commands/pack_command.rb +2 -1
- data/lib/fontisan/commands/validate_command.rb +157 -6
- data/lib/fontisan/converters/collection_converter.rb +22 -13
- data/lib/fontisan/converters/svg_generator.rb +2 -1
- data/lib/fontisan/converters/woff2_encoder.rb +6 -6
- data/lib/fontisan/converters/woff_writer.rb +3 -1
- data/lib/fontisan/dfont_collection.rb +84 -0
- data/lib/fontisan/font_loader.rb +9 -9
- data/lib/fontisan/formatters/text_formatter.rb +18 -14
- data/lib/fontisan/hints/hint_converter.rb +1 -1
- data/lib/fontisan/hints/hint_validator.rb +13 -10
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
- data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
- data/lib/fontisan/models/collection_validation_report.rb +104 -0
- data/lib/fontisan/models/font_report.rb +24 -0
- data/lib/fontisan/models/validation_report.rb +7 -2
- data/lib/fontisan/open_type_font.rb +2 -3
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
- data/lib/fontisan/subset/glyph_mapping.rb +2 -0
- data/lib/fontisan/subset/table_subsetter.rb +2 -2
- data/lib/fontisan/tables/cblc.rb +8 -4
- data/lib/fontisan/tables/cff/index.rb +2 -0
- data/lib/fontisan/tables/cff.rb +6 -3
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/cmap.rb +5 -5
- data/lib/fontisan/tables/glyf.rb +8 -10
- data/lib/fontisan/tables/head.rb +3 -3
- data/lib/fontisan/tables/hhea.rb +4 -4
- data/lib/fontisan/tables/maxp.rb +2 -2
- data/lib/fontisan/tables/name.rb +1 -1
- data/lib/fontisan/tables/os2.rb +8 -8
- data/lib/fontisan/tables/post.rb +2 -2
- data/lib/fontisan/tables/sbix.rb +5 -4
- data/lib/fontisan/true_type_font.rb +2 -3
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
- data/lib/fontisan/validation/collection_validator.rb +4 -2
- data/lib/fontisan/validators/basic_validator.rb +11 -21
- data/lib/fontisan/validators/font_book_validator.rb +29 -50
- data/lib/fontisan/validators/opentype_validator.rb +24 -28
- data/lib/fontisan/validators/validator.rb +87 -66
- data/lib/fontisan/validators/web_font_validator.rb +16 -21
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
- data/lib/fontisan/woff2/table_transformer.rb +4 -2
- data/lib/fontisan/woff2_font.rb +4 -2
- data/lib/fontisan/woff_font.rb +2 -2
- data/lib/fontisan.rb +2 -2
- data/scripts/compare_stack_aware.rb +1 -1
- data/scripts/measure_optimization.rb +1 -2
- metadata +5 -2
|
@@ -40,7 +40,10 @@ module Fontisan
|
|
|
40
40
|
# @param instructions [String] Binary instruction bytes
|
|
41
41
|
# @return [Hash] Validation result with :valid, :errors, :warnings keys
|
|
42
42
|
def validate_truetype_instructions(instructions)
|
|
43
|
-
|
|
43
|
+
if instructions.nil? || instructions.empty?
|
|
44
|
+
return { valid: true, errors: [],
|
|
45
|
+
warnings: [] }
|
|
46
|
+
end
|
|
44
47
|
|
|
45
48
|
errors = []
|
|
46
49
|
warnings = []
|
|
@@ -114,7 +117,6 @@ module Fontisan
|
|
|
114
117
|
if stack_depth != 0
|
|
115
118
|
warnings << "Stack not neutral: #{stack_depth} value(s) remaining"
|
|
116
119
|
end
|
|
117
|
-
|
|
118
120
|
rescue StandardError => e
|
|
119
121
|
errors << "Exception during validation: #{e.message}"
|
|
120
122
|
end
|
|
@@ -162,14 +164,14 @@ module Fontisan
|
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
# Validate stem widths
|
|
165
|
-
[
|
|
167
|
+
%i[std_hw std_vw].each do |key|
|
|
166
168
|
if hints[key] && hints[key] <= 0
|
|
167
169
|
errors << "#{key} must be positive: #{hints[key]}"
|
|
168
170
|
end
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
# Validate stem snaps
|
|
172
|
-
[
|
|
174
|
+
%i[stem_snap_h stem_snap_v].each do |key|
|
|
173
175
|
if hints[key]
|
|
174
176
|
if hints[key].length > MAX_STEM_SNAP
|
|
175
177
|
errors << "#{key} exceeds maximum (#{MAX_STEM_SNAP}): #{hints[key].length}"
|
|
@@ -191,10 +193,8 @@ module Fontisan
|
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
# Validate language_group
|
|
194
|
-
if hints[:language_group]
|
|
195
|
-
|
|
196
|
-
errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
|
|
197
|
-
end
|
|
196
|
+
if hints[:language_group] && ![0, 1].include?(hints[:language_group])
|
|
197
|
+
errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
{
|
|
@@ -212,7 +212,10 @@ module Fontisan
|
|
|
212
212
|
# @param instructions [String] Binary instruction bytes
|
|
213
213
|
# @return [Hash] Result with :neutral, :stack_depth, :errors keys
|
|
214
214
|
def validate_stack_neutrality(instructions)
|
|
215
|
-
|
|
215
|
+
if instructions.nil? || instructions.empty?
|
|
216
|
+
return { neutral: true, stack_depth: 0,
|
|
217
|
+
errors: [] }
|
|
218
|
+
end
|
|
216
219
|
|
|
217
220
|
errors = []
|
|
218
221
|
stack_depth = 0
|
|
@@ -257,7 +260,7 @@ module Fontisan
|
|
|
257
260
|
end
|
|
258
261
|
|
|
259
262
|
{
|
|
260
|
-
neutral: stack_depth
|
|
263
|
+
neutral: stack_depth.zero?,
|
|
261
264
|
stack_depth: stack_depth,
|
|
262
265
|
errors: errors,
|
|
263
266
|
}
|
|
@@ -29,7 +29,7 @@ module Fontisan
|
|
|
29
29
|
RCVT = 0x45 # Read CVT
|
|
30
30
|
WCVTP = 0x44 # Write CVT (in Pixels)
|
|
31
31
|
WCVTF = 0x70 # Write CVT (in FUnits)
|
|
32
|
-
MDAP = [0x2E, 0x2F].freeze
|
|
32
|
+
MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
|
|
33
33
|
SCVTCI = 0x1D # Set Control Value Table Cut In
|
|
34
34
|
SSWCI = 0x1E # Set Single Width Cut In
|
|
35
35
|
SSW = 0x1F # Set Single Width
|
|
@@ -106,7 +106,7 @@ module Fontisan
|
|
|
106
106
|
# Pattern: value cvt_index WCVTP (stack top to bottom)
|
|
107
107
|
if stack.length >= 2
|
|
108
108
|
value = stack.pop
|
|
109
|
-
|
|
109
|
+
stack.pop
|
|
110
110
|
# Track CVT modifications (useful for understanding setup)
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -168,7 +168,7 @@ module Fontisan
|
|
|
168
168
|
|
|
169
169
|
{
|
|
170
170
|
fpgm_size: size,
|
|
171
|
-
has_functions: size
|
|
171
|
+
has_functions: size.positive?,
|
|
172
172
|
complexity: complexity,
|
|
173
173
|
}
|
|
174
174
|
rescue StandardError
|
|
@@ -208,13 +208,19 @@ module Fontisan
|
|
|
208
208
|
xheight_min = (450 * scale_factor).to_i
|
|
209
209
|
xheight_max = (600 * scale_factor).to_i
|
|
210
210
|
capheight_min = (650 * scale_factor).to_i
|
|
211
|
-
capheight_max = (1500 * scale_factor).to_i
|
|
211
|
+
capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
|
|
212
212
|
|
|
213
213
|
# Group CVT values by typical alignment zones
|
|
214
|
-
descender_values = cvt.select
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
descender_values = cvt.select do |v|
|
|
215
|
+
v < descender_max && v > descender_min
|
|
216
|
+
end
|
|
217
|
+
baseline_values = cvt.select do |v|
|
|
218
|
+
v >= -baseline_range && v <= baseline_range
|
|
219
|
+
end
|
|
220
|
+
cvt.select { |v| v >= xheight_min && v <= xheight_max }
|
|
221
|
+
capheight_values = cvt.select do |v|
|
|
222
|
+
v >= capheight_min && v <= capheight_max
|
|
223
|
+
end
|
|
218
224
|
|
|
219
225
|
# Build blue_values (baseline and top zones)
|
|
220
226
|
blue_values = []
|
|
@@ -254,6 +260,7 @@ module Fontisan
|
|
|
254
260
|
def estimate_complexity(bytes)
|
|
255
261
|
return :simple if bytes.length < 50
|
|
256
262
|
return :moderate if bytes.length < 200
|
|
263
|
+
|
|
257
264
|
:complex
|
|
258
265
|
end
|
|
259
266
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "font_report"
|
|
4
|
+
require_relative "validation_report"
|
|
5
|
+
require "lutaml/model"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Models
|
|
9
|
+
# CollectionValidationReport aggregates validation results for all fonts
|
|
10
|
+
# in a TTC/OTC/dfont collection.
|
|
11
|
+
#
|
|
12
|
+
# Provides collection-level summary statistics and per-font validation
|
|
13
|
+
# details with clear formatting.
|
|
14
|
+
class CollectionValidationReport < Lutaml::Model::Serializable
|
|
15
|
+
attribute :collection_path, :string
|
|
16
|
+
attribute :collection_type, :string
|
|
17
|
+
attribute :num_fonts, :integer
|
|
18
|
+
attribute :font_reports, FontReport, collection: true,
|
|
19
|
+
initialize_empty: true
|
|
20
|
+
attribute :valid, :boolean, default: -> { true }
|
|
21
|
+
|
|
22
|
+
key_value do
|
|
23
|
+
map "collection_path", to: :collection_path
|
|
24
|
+
map "collection_type", to: :collection_type
|
|
25
|
+
map "num_fonts", to: :num_fonts
|
|
26
|
+
map "font_reports", to: :font_reports
|
|
27
|
+
map "valid", to: :valid
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Add a font report to the collection
|
|
31
|
+
#
|
|
32
|
+
# @param font_report [FontReport] The font report to add
|
|
33
|
+
# @return [void]
|
|
34
|
+
def add_font_report(font_report)
|
|
35
|
+
font_reports << font_report
|
|
36
|
+
# Mark that we're no longer using the default value
|
|
37
|
+
value_set_for(:font_reports)
|
|
38
|
+
# Update overall validity
|
|
39
|
+
self.valid = valid && font_report.report.valid?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get overall validation status for the collection
|
|
43
|
+
#
|
|
44
|
+
# @return [String] "valid", "invalid", or "valid_with_warnings"
|
|
45
|
+
def overall_status
|
|
46
|
+
return "invalid" unless font_reports.all? { |fr| fr.report.valid? }
|
|
47
|
+
return "valid_with_warnings" if font_reports.any? do |fr|
|
|
48
|
+
fr.report.has_warnings?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
"valid"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Generate text summary with collection header and per-font sections
|
|
55
|
+
#
|
|
56
|
+
# @return [String] Formatted validation report
|
|
57
|
+
def text_summary
|
|
58
|
+
lines = []
|
|
59
|
+
lines << "Collection: #{collection_path}"
|
|
60
|
+
lines << "Type: #{collection_type}"
|
|
61
|
+
lines << "Fonts: #{num_fonts}"
|
|
62
|
+
lines << ""
|
|
63
|
+
lines << "Summary:"
|
|
64
|
+
lines << " Total Errors: #{total_errors}"
|
|
65
|
+
lines << " Total Warnings: #{total_warnings}"
|
|
66
|
+
lines << " Total Info: #{total_info}"
|
|
67
|
+
|
|
68
|
+
if font_reports.any?
|
|
69
|
+
lines << ""
|
|
70
|
+
font_reports.each do |font_report|
|
|
71
|
+
lines << "=== Font #{font_report.font_index}: #{font_report.font_name} ==="
|
|
72
|
+
# Indent each line of the font's report
|
|
73
|
+
font_lines = font_report.report.text_summary.split("\n")
|
|
74
|
+
lines.concat(font_lines)
|
|
75
|
+
lines << "" unless font_report == font_reports.last
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Calculate total errors across all fonts
|
|
83
|
+
#
|
|
84
|
+
# @return [Integer] Total error count
|
|
85
|
+
def total_errors
|
|
86
|
+
font_reports.sum { |fr| fr.report.summary.errors }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Calculate total warnings across all fonts
|
|
90
|
+
#
|
|
91
|
+
# @return [Integer] Total warning count
|
|
92
|
+
def total_warnings
|
|
93
|
+
font_reports.sum { |fr| fr.report.summary.warnings }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Calculate total info messages across all fonts
|
|
97
|
+
#
|
|
98
|
+
# @return [Integer] Total info count
|
|
99
|
+
def total_info
|
|
100
|
+
font_reports.sum { |fr| fr.report.summary.info }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "validation_report"
|
|
4
|
+
require "lutaml/model"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# FontReport wraps a single font's validation report with collection context
|
|
9
|
+
#
|
|
10
|
+
# Used within CollectionValidationReport to associate validation results
|
|
11
|
+
# with a specific font index and name in the collection.
|
|
12
|
+
class FontReport < Lutaml::Model::Serializable
|
|
13
|
+
attribute :font_index, :integer
|
|
14
|
+
attribute :font_name, :string
|
|
15
|
+
attribute :report, ValidationReport
|
|
16
|
+
|
|
17
|
+
key_value do
|
|
18
|
+
map "font_index", to: :font_index
|
|
19
|
+
map "font_name", to: :font_name
|
|
20
|
+
map "report", to: :report
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -101,7 +101,9 @@ module Fontisan
|
|
|
101
101
|
attribute :status, :string
|
|
102
102
|
attribute :use_case, :string
|
|
103
103
|
attribute :checks_performed, :string, collection: true, default: -> { [] }
|
|
104
|
-
attribute :check_results, CheckResult, collection: true, default: -> {
|
|
104
|
+
attribute :check_results, CheckResult, collection: true, default: -> {
|
|
105
|
+
[]
|
|
106
|
+
}
|
|
105
107
|
|
|
106
108
|
yaml do
|
|
107
109
|
map "font_path", to: :font_path
|
|
@@ -340,7 +342,9 @@ module Fontisan
|
|
|
340
342
|
# @param field_name [String, Symbol] Field name
|
|
341
343
|
# @return [Array<CheckResult>] Array of check results for the field
|
|
342
344
|
def field_issues(table_tag, field_name)
|
|
343
|
-
check_results.select
|
|
345
|
+
check_results.select do |cr|
|
|
346
|
+
cr.table == table_tag.to_s && cr.field == field_name.to_s
|
|
347
|
+
end
|
|
344
348
|
end
|
|
345
349
|
|
|
346
350
|
# Check filtering methods
|
|
@@ -374,6 +378,7 @@ module Fontisan
|
|
|
374
378
|
# @return [Float] Failure rate (0.0 to 1.0)
|
|
375
379
|
def failure_rate
|
|
376
380
|
return 0.0 if check_results.empty?
|
|
381
|
+
|
|
377
382
|
failed_checks.length.to_f / check_results.length
|
|
378
383
|
end
|
|
379
384
|
|
|
@@ -581,10 +581,9 @@ module Fontisan
|
|
|
581
581
|
# @param path [String] Path to the OTF file
|
|
582
582
|
# @return [void]
|
|
583
583
|
def update_checksum_adjustment_in_file(path)
|
|
584
|
-
# Use tempfile-based checksum calculation for Windows compatibility
|
|
585
|
-
# This keeps the tempfile alive until we're done with the checksum
|
|
586
584
|
File.open(path, "r+b") do |io|
|
|
587
|
-
checksum
|
|
585
|
+
# Calculate checksum directly from IO to avoid Windows Tempfile issues
|
|
586
|
+
checksum = Utilities::ChecksumCalculator.calculate_checksum_from_io(io)
|
|
588
587
|
|
|
589
588
|
# Calculate adjustment
|
|
590
589
|
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
@@ -151,7 +151,7 @@ module Fontisan
|
|
|
151
151
|
# @param charstring [String] CharString to search
|
|
152
152
|
# @param pattern [Pattern] pattern to find
|
|
153
153
|
# @return [Array<Integer>] array of start positions
|
|
154
|
-
def find_pattern_positions(charstring, pattern,
|
|
154
|
+
def find_pattern_positions(charstring, pattern, _glyph_id = nil)
|
|
155
155
|
positions = []
|
|
156
156
|
offset = 0
|
|
157
157
|
|
|
@@ -32,7 +32,9 @@ module Fontisan
|
|
|
32
32
|
selected = []
|
|
33
33
|
# Sort by savings (descending), then by length (descending), then by min glyph ID,
|
|
34
34
|
# then by byte values for complete determinism across platforms
|
|
35
|
-
remaining = @patterns.sort_by
|
|
35
|
+
remaining = @patterns.sort_by do |p|
|
|
36
|
+
[-p.savings, -p.length, p.glyphs.min, p.bytes.bytes]
|
|
37
|
+
end
|
|
36
38
|
|
|
37
39
|
remaining.each do |pattern|
|
|
38
40
|
break if selected.length >= @max_subrs
|
|
@@ -53,7 +55,9 @@ module Fontisan
|
|
|
53
55
|
def optimize_ordering(subroutines)
|
|
54
56
|
# Higher frequency = lower ID (shorter encoding)
|
|
55
57
|
# Use same comprehensive sort keys as optimize_selection for consistency
|
|
56
|
-
subroutines.sort_by
|
|
58
|
+
subroutines.sort_by do |subr|
|
|
59
|
+
[-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes]
|
|
60
|
+
end
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
# Check if nesting would be beneficial
|
|
@@ -152,7 +152,7 @@ module Fontisan
|
|
|
152
152
|
# Build new hmtx data
|
|
153
153
|
data = String.new(encoding: Encoding::BINARY)
|
|
154
154
|
|
|
155
|
-
mapping.each do |old_id
|
|
155
|
+
mapping.old_ids.each do |old_id|
|
|
156
156
|
metric = table.metric_for(old_id)
|
|
157
157
|
next unless metric
|
|
158
158
|
|
|
@@ -319,7 +319,7 @@ module Fontisan
|
|
|
319
319
|
current_offset = 0
|
|
320
320
|
|
|
321
321
|
# Process glyphs in mapping order
|
|
322
|
-
mapping.each do |old_id
|
|
322
|
+
mapping.old_ids.each do |old_id|
|
|
323
323
|
@loca_offsets << current_offset
|
|
324
324
|
|
|
325
325
|
# Get offset and size from original loca
|
data/lib/fontisan/tables/cblc.rb
CHANGED
|
@@ -86,9 +86,12 @@ module Fontisan
|
|
|
86
86
|
size = new
|
|
87
87
|
|
|
88
88
|
io = StringIO.new(data)
|
|
89
|
-
size.instance_variable_set(:@index_subtable_array_offset,
|
|
90
|
-
|
|
91
|
-
size.instance_variable_set(:@
|
|
89
|
+
size.instance_variable_set(:@index_subtable_array_offset,
|
|
90
|
+
io.read(4).unpack1("N"))
|
|
91
|
+
size.instance_variable_set(:@index_tables_size,
|
|
92
|
+
io.read(4).unpack1("N"))
|
|
93
|
+
size.instance_variable_set(:@number_of_index_subtables,
|
|
94
|
+
io.read(4).unpack1("N"))
|
|
92
95
|
size.instance_variable_set(:@color_ref, io.read(4).unpack1("N"))
|
|
93
96
|
|
|
94
97
|
# Parse hori and vert metrics (12 bytes each)
|
|
@@ -98,7 +101,8 @@ module Fontisan
|
|
|
98
101
|
size.instance_variable_set(:@vert, SbitLineMetrics.read(vert_data))
|
|
99
102
|
|
|
100
103
|
# Parse remaining fields
|
|
101
|
-
size.instance_variable_set(:@start_glyph_index,
|
|
104
|
+
size.instance_variable_set(:@start_glyph_index,
|
|
105
|
+
io.read(2).unpack1("n"))
|
|
102
106
|
size.instance_variable_set(:@end_glyph_index, io.read(2).unpack1("n"))
|
|
103
107
|
size.instance_variable_set(:@ppem_x, io.read(1).unpack1("C"))
|
|
104
108
|
size.instance_variable_set(:@ppem_y, io.read(1).unpack1("C"))
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -299,7 +299,8 @@ module Fontisan
|
|
|
299
299
|
io.seek(absolute_offset)
|
|
300
300
|
Index.new(io, start_offset: absolute_offset)
|
|
301
301
|
rescue StandardError => e
|
|
302
|
-
raise CorruptedTableError,
|
|
302
|
+
raise CorruptedTableError,
|
|
303
|
+
"Failed to parse Local Subr INDEX: #{e.message}"
|
|
303
304
|
end
|
|
304
305
|
|
|
305
306
|
# Get the CharStrings INDEX for a specific font
|
|
@@ -320,7 +321,8 @@ module Fontisan
|
|
|
320
321
|
io.seek(charstrings_offset)
|
|
321
322
|
CharstringsIndex.new(io, start_offset: charstrings_offset)
|
|
322
323
|
rescue StandardError => e
|
|
323
|
-
raise CorruptedTableError,
|
|
324
|
+
raise CorruptedTableError,
|
|
325
|
+
"Failed to parse CharStrings INDEX: #{e.message}"
|
|
324
326
|
end
|
|
325
327
|
|
|
326
328
|
# Get a CharString for a specific glyph
|
|
@@ -355,7 +357,8 @@ module Fontisan
|
|
|
355
357
|
local_subr_index,
|
|
356
358
|
)
|
|
357
359
|
rescue StandardError => e
|
|
358
|
-
raise CorruptedTableError,
|
|
360
|
+
raise CorruptedTableError,
|
|
361
|
+
"Failed to get CharString for glyph #{glyph_index}: #{e.message}"
|
|
359
362
|
end
|
|
360
363
|
|
|
361
364
|
# Get the number of glyphs in a font
|
|
@@ -51,7 +51,7 @@ module Fontisan
|
|
|
51
51
|
# Check if this is blend data
|
|
52
52
|
# Format: base1 delta1_1 ... delta1_N base2 delta2_1 ... delta2_N ...
|
|
53
53
|
# The array must be divisible by (num_axes + 1)
|
|
54
|
-
return nil unless value.size % (num_axes + 1)
|
|
54
|
+
return nil unless (value.size % (num_axes + 1)).zero?
|
|
55
55
|
|
|
56
56
|
num_values = value.size / (num_axes + 1)
|
|
57
57
|
blends = []
|
data/lib/fontisan/tables/cff2.rb
CHANGED
data/lib/fontisan/tables/cmap.rb
CHANGED
|
@@ -278,22 +278,20 @@ module Fontisan
|
|
|
278
278
|
end
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
-
public
|
|
282
|
-
|
|
283
281
|
# Validation helper: Check if version is valid
|
|
284
282
|
#
|
|
285
283
|
# cmap version should be 0
|
|
286
284
|
#
|
|
287
285
|
# @return [Boolean] True if version is 0
|
|
288
286
|
def valid_version?
|
|
289
|
-
version
|
|
287
|
+
version.zero?
|
|
290
288
|
end
|
|
291
289
|
|
|
292
290
|
# Validation helper: Check if at least one subtable exists
|
|
293
291
|
#
|
|
294
292
|
# @return [Boolean] True if num_tables > 0
|
|
295
293
|
def has_subtables?
|
|
296
|
-
num_tables
|
|
294
|
+
num_tables&.positive?
|
|
297
295
|
end
|
|
298
296
|
|
|
299
297
|
# Validation helper: Check if Unicode mapping exists
|
|
@@ -357,7 +355,9 @@ module Fontisan
|
|
|
357
355
|
mappings = unicode_mappings
|
|
358
356
|
return true if mappings.nil? || mappings.empty?
|
|
359
357
|
|
|
360
|
-
mappings.values.all?
|
|
358
|
+
mappings.values.all? do |glyph_id|
|
|
359
|
+
glyph_id >= 0 && glyph_id < max_glyph_id
|
|
360
|
+
end
|
|
361
361
|
end
|
|
362
362
|
end
|
|
363
363
|
end
|
data/lib/fontisan/tables/glyf.rb
CHANGED
|
@@ -166,7 +166,7 @@ module Fontisan
|
|
|
166
166
|
# @param head [Head] Head table for context
|
|
167
167
|
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
168
168
|
# @return [Boolean] True if all non-special glyphs have contours
|
|
169
|
-
def no_empty_glyphs_except_special?(loca,
|
|
169
|
+
def no_empty_glyphs_except_special?(loca, _head, num_glyphs)
|
|
170
170
|
# Check glyphs 1 through num_glyphs-1 (.notdef at 0 can be empty)
|
|
171
171
|
(1...num_glyphs).all? do |glyph_id|
|
|
172
172
|
size = loca.size_of(glyph_id)
|
|
@@ -194,7 +194,7 @@ module Fontisan
|
|
|
194
194
|
|
|
195
195
|
(0...num_glyphs).all? do |glyph_id|
|
|
196
196
|
glyph = glyph_for(glyph_id, loca, head)
|
|
197
|
-
next true if glyph.nil?
|
|
197
|
+
next true if glyph.nil? # Empty glyphs are OK
|
|
198
198
|
|
|
199
199
|
# Check if glyph bounds are within font bounds
|
|
200
200
|
glyph.x_min >= font_x_min &&
|
|
@@ -218,7 +218,7 @@ module Fontisan
|
|
|
218
218
|
def instructions_sound?(loca, head, num_glyphs)
|
|
219
219
|
(0...num_glyphs).all? do |glyph_id|
|
|
220
220
|
glyph = glyph_for(glyph_id, loca, head)
|
|
221
|
-
next true if glyph.nil?
|
|
221
|
+
next true if glyph.nil? # Empty glyphs are OK
|
|
222
222
|
|
|
223
223
|
# Simple glyphs have instructions
|
|
224
224
|
if glyph.respond_to?(:instruction_length)
|
|
@@ -242,7 +242,7 @@ module Fontisan
|
|
|
242
242
|
# @return [Boolean] True if contour count is valid
|
|
243
243
|
def valid_contour_count?(glyph_id, loca, head)
|
|
244
244
|
glyph = glyph_for(glyph_id, loca, head)
|
|
245
|
-
return true if glyph.nil?
|
|
245
|
+
return true if glyph.nil? # Empty glyphs are OK
|
|
246
246
|
|
|
247
247
|
# Simple glyphs: contours should be >= 0
|
|
248
248
|
# Compound glyphs: numberOfContours = -1
|
|
@@ -265,12 +265,10 @@ module Fontisan
|
|
|
265
265
|
# @return [Boolean] True if all glyphs can be accessed
|
|
266
266
|
def all_glyphs_accessible?(loca, head, num_glyphs)
|
|
267
267
|
(0...num_glyphs).all? do |glyph_id|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
false
|
|
273
|
-
end
|
|
268
|
+
glyph_for(glyph_id, loca, head)
|
|
269
|
+
true
|
|
270
|
+
rescue Fontisan::CorruptedTableError
|
|
271
|
+
false
|
|
274
272
|
end
|
|
275
273
|
rescue StandardError
|
|
276
274
|
false
|
data/lib/fontisan/tables/head.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Fontisan
|
|
|
95
95
|
#
|
|
96
96
|
# @return [Boolean] True if version is 1.0
|
|
97
97
|
def valid_version?
|
|
98
|
-
version_raw == 0x00010000
|
|
98
|
+
version_raw == 0x00010000 # Version 1.0
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Validation helper: Check if units per em is valid
|
|
@@ -130,7 +130,7 @@ module Fontisan
|
|
|
130
130
|
#
|
|
131
131
|
# @return [Boolean] True if format is 0 or 1
|
|
132
132
|
def valid_index_to_loc_format?
|
|
133
|
-
|
|
133
|
+
[0, 1].include?(index_to_loc_format)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Validation helper: Check if glyph_data_format is valid
|
|
@@ -139,7 +139,7 @@ module Fontisan
|
|
|
139
139
|
#
|
|
140
140
|
# @return [Boolean] True if format is 0
|
|
141
141
|
def valid_glyph_data_format?
|
|
142
|
-
glyph_data_format
|
|
142
|
+
glyph_data_format.zero?
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
# Validate magic number and raise error if invalid
|
data/lib/fontisan/tables/hhea.rb
CHANGED
|
@@ -112,7 +112,7 @@ module Fontisan
|
|
|
112
112
|
#
|
|
113
113
|
# @return [Boolean] True if format is 0
|
|
114
114
|
def valid_metric_data_format?
|
|
115
|
-
metric_data_format
|
|
115
|
+
metric_data_format.zero?
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
# Validation helper: Check if number of h metrics is valid
|
|
@@ -130,7 +130,7 @@ module Fontisan
|
|
|
130
130
|
#
|
|
131
131
|
# @return [Boolean] True if ascent/descent have correct signs
|
|
132
132
|
def valid_ascent_descent?
|
|
133
|
-
ascent
|
|
133
|
+
ascent.positive? && descent.negative?
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Validation helper: Check if line gap is non-negative
|
|
@@ -148,7 +148,7 @@ module Fontisan
|
|
|
148
148
|
#
|
|
149
149
|
# @return [Boolean] True if advance_width_max > 0
|
|
150
150
|
def valid_advance_width_max?
|
|
151
|
-
advance_width_max
|
|
151
|
+
advance_width_max&.positive?
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Validation helper: Check if caret slope is valid
|
|
@@ -168,7 +168,7 @@ module Fontisan
|
|
|
168
168
|
#
|
|
169
169
|
# @return [Boolean] True if x_max_extent > 0
|
|
170
170
|
def valid_x_max_extent?
|
|
171
|
-
x_max_extent
|
|
171
|
+
x_max_extent.positive?
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# Validate the table and raise error if invalid
|
data/lib/fontisan/tables/maxp.rb
CHANGED
|
@@ -179,9 +179,9 @@ module Fontisan
|
|
|
179
179
|
#
|
|
180
180
|
# @return [Boolean] True if maxZones is valid or not applicable
|
|
181
181
|
def valid_max_zones?
|
|
182
|
-
return true if version_0_5?
|
|
182
|
+
return true if version_0_5? # Not applicable for CFF
|
|
183
183
|
|
|
184
|
-
max_zones
|
|
184
|
+
max_zones&.between?(1, 2)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
# Validation helper: Check if all TrueType metrics are present
|
data/lib/fontisan/tables/name.rb
CHANGED