fontisan 0.2.7 → 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 +106 -319
- data/Gemfile +1 -1
- data/README.adoc +81 -14
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- data/lib/fontisan/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- 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/font_loader.rb +7 -6
- 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 +4 -2
|
@@ -109,7 +109,7 @@ module Fontisan
|
|
|
109
109
|
woff2_binary = assemble_woff2(header, entries, compressed_data)
|
|
110
110
|
|
|
111
111
|
# Prepare result
|
|
112
|
-
|
|
112
|
+
{ woff2_binary: woff2_binary }
|
|
113
113
|
|
|
114
114
|
# Optional validation
|
|
115
115
|
# Temporarily disabled - will be reimplemented with new DSL framework
|
|
@@ -117,8 +117,6 @@ module Fontisan
|
|
|
117
117
|
# validation_report = validate_encoding(woff2_binary, options)
|
|
118
118
|
# result[:validation_report] = validation_report
|
|
119
119
|
# end
|
|
120
|
-
|
|
121
|
-
result
|
|
122
120
|
end
|
|
123
121
|
|
|
124
122
|
# Get list of supported conversions
|
|
@@ -185,14 +183,16 @@ module Fontisan
|
|
|
185
183
|
end
|
|
186
184
|
|
|
187
185
|
# Read table directory
|
|
188
|
-
woff2.table_entries = Woff2Font.read_table_directory_from_io(io,
|
|
186
|
+
woff2.table_entries = Woff2Font.read_table_directory_from_io(io,
|
|
187
|
+
woff2.header)
|
|
189
188
|
|
|
190
189
|
# Decompress tables
|
|
191
190
|
woff2.decompressed_tables = Woff2Font.decompress_tables(io, woff2.header,
|
|
192
|
-
|
|
191
|
+
woff2.table_entries)
|
|
193
192
|
|
|
194
193
|
# Apply transformations
|
|
195
|
-
Woff2Font.apply_transformations!(woff2.table_entries,
|
|
194
|
+
Woff2Font.apply_transformations!(woff2.table_entries,
|
|
195
|
+
woff2.decompressed_tables)
|
|
196
196
|
|
|
197
197
|
woff2
|
|
198
198
|
end
|
|
@@ -369,9 +369,11 @@ module Fontisan
|
|
|
369
369
|
# Sort tables by tag for consistent output (same order as directory)
|
|
370
370
|
sorted_tables = compressed_tables.sort_by { |tag, _| tag }
|
|
371
371
|
|
|
372
|
-
sorted_tables
|
|
372
|
+
# rubocop:disable Style/HashEachMethods - sorted_tables is an Array, not a Hash
|
|
373
|
+
sorted_tables.each do |_, table_info|
|
|
373
374
|
io.write(table_info[:compressed_data])
|
|
374
375
|
end
|
|
376
|
+
# rubocop:enable Style/HashEachMethods
|
|
375
377
|
end
|
|
376
378
|
|
|
377
379
|
# Write metadata to output
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -72,7 +72,8 @@ module Fontisan
|
|
|
72
72
|
when "wOF2"
|
|
73
73
|
Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
74
74
|
when Constants::DFONT_RESOURCE_HEADER
|
|
75
|
-
extract_and_load_dfont(io, path, font_index, resolved_mode,
|
|
75
|
+
extract_and_load_dfont(io, path, font_index, resolved_mode,
|
|
76
|
+
resolved_lazy)
|
|
76
77
|
else
|
|
77
78
|
raise InvalidFontError,
|
|
78
79
|
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
|
|
@@ -185,7 +186,7 @@ module Fontisan
|
|
|
185
186
|
num_fonts = io.read(4).unpack1("N")
|
|
186
187
|
|
|
187
188
|
# Read all font offsets
|
|
188
|
-
font_offsets = num_fonts
|
|
189
|
+
font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
|
|
189
190
|
|
|
190
191
|
# Scan all fonts to determine collection type (not just first)
|
|
191
192
|
truetype_count = 0
|
|
@@ -212,7 +213,7 @@ module Fontisan
|
|
|
212
213
|
# Determine collection type based on what fonts are inside
|
|
213
214
|
# If ANY font is OpenType, use OpenTypeCollection (more general format)
|
|
214
215
|
# Only use TrueTypeCollection if ALL fonts are TrueType
|
|
215
|
-
if opentype_count
|
|
216
|
+
if opentype_count.positive?
|
|
216
217
|
OpenTypeCollection.from_file(path)
|
|
217
218
|
else
|
|
218
219
|
# All fonts are TrueType
|
|
@@ -282,7 +283,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
282
283
|
end
|
|
283
284
|
|
|
284
285
|
# Read all font offsets
|
|
285
|
-
font_offsets = num_fonts
|
|
286
|
+
font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
|
|
286
287
|
|
|
287
288
|
# Scan all fonts to determine collection type (not just first)
|
|
288
289
|
truetype_count = 0
|
|
@@ -308,7 +309,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
308
309
|
|
|
309
310
|
# If ANY font is OpenType, use OpenTypeCollection (more general format)
|
|
310
311
|
# Only use TrueTypeCollection if ALL fonts are TrueType
|
|
311
|
-
if opentype_count
|
|
312
|
+
if opentype_count.positive?
|
|
312
313
|
# OpenType Collection
|
|
313
314
|
otc = OpenTypeCollection.from_file(path)
|
|
314
315
|
File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
|
|
@@ -328,7 +329,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
328
329
|
# @param lazy [Boolean] Lazy loading flag
|
|
329
330
|
# @return [TrueTypeFont, OpenTypeFont] Loaded font
|
|
330
331
|
# @api private
|
|
331
|
-
def self.extract_and_load_dfont(io,
|
|
332
|
+
def self.extract_and_load_dfont(io, _path, font_index, mode, lazy)
|
|
332
333
|
require_relative "parsers/dfont_parser"
|
|
333
334
|
|
|
334
335
|
# Extract SFNT data from resource fork
|
|
@@ -410,15 +410,16 @@ module Fontisan
|
|
|
410
410
|
|
|
411
411
|
info.fonts.each_with_index do |font_info, index|
|
|
412
412
|
# Show font index with offset
|
|
413
|
-
if font_info.collection_offset
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
413
|
+
lines << if font_info.collection_offset
|
|
414
|
+
"Font #{index} (offset: #{font_info.collection_offset}):"
|
|
415
|
+
else
|
|
416
|
+
"Font #{index}:"
|
|
417
|
+
end
|
|
418
418
|
lines << ""
|
|
419
419
|
|
|
420
420
|
# Format each font using same structure as brief mode
|
|
421
|
-
font_type_display = format_font_type_display(font_info.font_format,
|
|
421
|
+
font_type_display = format_font_type_display(font_info.font_format,
|
|
422
|
+
font_info.is_variable)
|
|
422
423
|
add_line(lines, "Font type", font_type_display)
|
|
423
424
|
add_line(lines, "Family", font_info.family_name)
|
|
424
425
|
add_line(lines, "Subfamily", font_info.subfamily_name)
|
|
@@ -426,7 +427,8 @@ module Fontisan
|
|
|
426
427
|
add_line(lines, "PostScript name", font_info.postscript_name)
|
|
427
428
|
add_line(lines, "Version", font_info.version)
|
|
428
429
|
add_line(lines, "Vendor ID", font_info.vendor_id)
|
|
429
|
-
add_line(lines, "Font revision",
|
|
430
|
+
add_line(lines, "Font revision",
|
|
431
|
+
format_float(font_info.font_revision))
|
|
430
432
|
add_line(lines, "Units per em", font_info.units_per_em)
|
|
431
433
|
|
|
432
434
|
# Blank line between fonts (except after last)
|
|
@@ -467,15 +469,16 @@ module Fontisan
|
|
|
467
469
|
# Each font's brief info
|
|
468
470
|
info.fonts.each_with_index do |font_info, index|
|
|
469
471
|
# Show font index with offset
|
|
470
|
-
if font_info.collection_offset
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
472
|
+
lines << if font_info.collection_offset
|
|
473
|
+
"Font #{index} (offset: #{font_info.collection_offset}):"
|
|
474
|
+
else
|
|
475
|
+
"Font #{index}:"
|
|
476
|
+
end
|
|
475
477
|
lines << ""
|
|
476
478
|
|
|
477
479
|
# Format each font using same structure as individual fonts
|
|
478
|
-
font_type_display = format_font_type_display(font_info.font_format,
|
|
480
|
+
font_type_display = format_font_type_display(font_info.font_format,
|
|
481
|
+
font_info.is_variable)
|
|
479
482
|
add_line(lines, "Font type", font_type_display)
|
|
480
483
|
add_line(lines, "Family", font_info.family_name)
|
|
481
484
|
add_line(lines, "Subfamily", font_info.subfamily_name)
|
|
@@ -483,7 +486,8 @@ module Fontisan
|
|
|
483
486
|
add_line(lines, "PostScript name", font_info.postscript_name)
|
|
484
487
|
add_line(lines, "Version", font_info.version)
|
|
485
488
|
add_line(lines, "Vendor ID", font_info.vendor_id)
|
|
486
|
-
add_line(lines, "Font revision",
|
|
489
|
+
add_line(lines, "Font revision",
|
|
490
|
+
format_float(font_info.font_revision))
|
|
487
491
|
add_line(lines, "Units per em", font_info.units_per_em)
|
|
488
492
|
|
|
489
493
|
# Blank line between fonts (except after last)
|
|
@@ -244,7 +244,7 @@ module Fontisan
|
|
|
244
244
|
# CVT values typically contain standard widths at the beginning
|
|
245
245
|
if cvt && !cvt.empty?
|
|
246
246
|
# First CVT value often represents standard horizontal stem
|
|
247
|
-
hints[:std_hw] = cvt[0].abs if cvt.length
|
|
247
|
+
hints[:std_hw] = cvt[0].abs if cvt.length.positive?
|
|
248
248
|
# Second CVT value often represents standard vertical stem
|
|
249
249
|
hints[:std_vw] = cvt[1].abs if cvt.length > 1
|
|
250
250
|
end
|
|
@@ -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