fontisan 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Utils
|
|
5
|
+
# Simple thread pool implementation
|
|
6
|
+
#
|
|
7
|
+
# Manages a fixed number of worker threads for parallel job execution.
|
|
8
|
+
# Jobs are queued and processed by available workers.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# pool = ThreadPool.new(4)
|
|
12
|
+
# future = pool.schedule { expensive_computation }
|
|
13
|
+
# result = future.value
|
|
14
|
+
# pool.shutdown
|
|
15
|
+
class ThreadPool
|
|
16
|
+
# Initialize thread pool
|
|
17
|
+
#
|
|
18
|
+
# @param size [Integer] Number of worker threads
|
|
19
|
+
def initialize(size)
|
|
20
|
+
@size = size
|
|
21
|
+
@queue = Queue.new
|
|
22
|
+
@threads = []
|
|
23
|
+
@shutdown = false
|
|
24
|
+
|
|
25
|
+
# Start worker threads
|
|
26
|
+
@size.times do
|
|
27
|
+
@threads << Thread.new { worker_loop }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Schedule a job for execution
|
|
32
|
+
#
|
|
33
|
+
# @yield Job to execute
|
|
34
|
+
# @return [Future] Future object to retrieve result
|
|
35
|
+
def schedule(&block)
|
|
36
|
+
future = Future.new
|
|
37
|
+
@queue << { block: block, future: future }
|
|
38
|
+
future
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Shutdown thread pool
|
|
42
|
+
#
|
|
43
|
+
# Waits for all queued jobs to complete and stops workers.
|
|
44
|
+
def shutdown
|
|
45
|
+
@shutdown = true
|
|
46
|
+
|
|
47
|
+
# Signal all threads to stop
|
|
48
|
+
@size.times { @queue << :stop }
|
|
49
|
+
|
|
50
|
+
# Wait for all threads to finish
|
|
51
|
+
@threads.each(&:join)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Worker thread main loop
|
|
57
|
+
def worker_loop
|
|
58
|
+
loop do
|
|
59
|
+
job = @queue.pop
|
|
60
|
+
break if job == :stop
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
result = job[:block].call
|
|
64
|
+
job[:future].set_value(result)
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
job[:future].set_error(e)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Future object for async result retrieval
|
|
73
|
+
#
|
|
74
|
+
# Represents a computation that will complete in the future.
|
|
75
|
+
# Provides blocking access to the eventual result or error.
|
|
76
|
+
#
|
|
77
|
+
# @example Basic usage
|
|
78
|
+
# future = Future.new
|
|
79
|
+
# Thread.new { future.set_value(42) }
|
|
80
|
+
# result = future.value # Blocks until value is set
|
|
81
|
+
class Future
|
|
82
|
+
def initialize
|
|
83
|
+
@mutex = Mutex.new
|
|
84
|
+
@condition = ConditionVariable.new
|
|
85
|
+
@completed = false
|
|
86
|
+
@value = nil
|
|
87
|
+
@error = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Set the computed value
|
|
91
|
+
#
|
|
92
|
+
# @param value [Object] Result value
|
|
93
|
+
def set_value(value)
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
@value = value
|
|
96
|
+
@completed = true
|
|
97
|
+
@condition.signal
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Set an error
|
|
102
|
+
#
|
|
103
|
+
# @param error [Exception] Error that occurred
|
|
104
|
+
def set_error(error)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
@error = error
|
|
107
|
+
@completed = true
|
|
108
|
+
@condition.signal
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the value, blocking until available
|
|
113
|
+
#
|
|
114
|
+
# @return [Object] Computed value
|
|
115
|
+
# @raise [Exception] If computation failed
|
|
116
|
+
def value
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
@condition.wait(@mutex) until @completed
|
|
119
|
+
|
|
120
|
+
raise @error if @error
|
|
121
|
+
|
|
122
|
+
@value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if computation is complete
|
|
127
|
+
#
|
|
128
|
+
# @return [Boolean] True if complete
|
|
129
|
+
def completed?
|
|
130
|
+
@mutex.synchronize { @completed }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../utilities/checksum_calculator"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validation
|
|
7
|
+
# ChecksumValidator validates font file and table checksums
|
|
8
|
+
#
|
|
9
|
+
# This validator checks that the head table checksum adjustment is correct
|
|
10
|
+
# and validates individual table checksums to ensure file integrity.
|
|
11
|
+
#
|
|
12
|
+
# Single Responsibility: Checksum validation and file integrity
|
|
13
|
+
#
|
|
14
|
+
# @example Validating checksums
|
|
15
|
+
# validator = ChecksumValidator.new(rules)
|
|
16
|
+
# issues = validator.validate(font, font_path)
|
|
17
|
+
class ChecksumValidator
|
|
18
|
+
# Initialize checksum validator
|
|
19
|
+
#
|
|
20
|
+
# @param rules [Hash] Validation rules configuration
|
|
21
|
+
def initialize(rules)
|
|
22
|
+
@rules = rules
|
|
23
|
+
@checksum_config = rules["checksum_validation"] || {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Validate font checksums
|
|
27
|
+
#
|
|
28
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
29
|
+
# @param font_path [String] Path to the font file
|
|
30
|
+
# @return [Array<Hash>] Array of validation issues
|
|
31
|
+
def validate(font, font_path)
|
|
32
|
+
issues = []
|
|
33
|
+
|
|
34
|
+
# Check head table checksum adjustment if enabled
|
|
35
|
+
if should_check?("check_head_checksum_adjustment")
|
|
36
|
+
issues.concat(check_head_checksum_adjustment(font, font_path))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check individual table checksums if enabled
|
|
40
|
+
if should_check?("check_table_checksums")
|
|
41
|
+
issues.concat(check_table_checksums(font))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
issues
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Check if a validation should be performed
|
|
50
|
+
#
|
|
51
|
+
# @param check_name [String] The check name
|
|
52
|
+
# @return [Boolean] true if check should be performed
|
|
53
|
+
def should_check?(check_name)
|
|
54
|
+
@rules.dig("validation_levels", "standard", check_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check head table checksum adjustment
|
|
58
|
+
#
|
|
59
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
60
|
+
# @param font_path [String] Path to the font file
|
|
61
|
+
# @return [Array<Hash>] Array of checksum issues
|
|
62
|
+
def check_head_checksum_adjustment(font, font_path)
|
|
63
|
+
issues = []
|
|
64
|
+
|
|
65
|
+
head_entry = font.head_table
|
|
66
|
+
return issues unless head_entry
|
|
67
|
+
|
|
68
|
+
# Calculate the checksum of the entire font file
|
|
69
|
+
begin
|
|
70
|
+
file_checksum = Utilities::ChecksumCalculator.calculate_file_checksum(font_path)
|
|
71
|
+
magic = @checksum_config["magic"] || Constants::CHECKSUM_ADJUSTMENT_MAGIC
|
|
72
|
+
|
|
73
|
+
# Read the actual checksum adjustment from head table
|
|
74
|
+
head_data = font.table_data[Constants::HEAD_TAG]
|
|
75
|
+
return issues unless head_data && head_data.bytesize >= 12
|
|
76
|
+
|
|
77
|
+
actual_adjustment = head_data.byteslice(8, 4).unpack1("N")
|
|
78
|
+
|
|
79
|
+
# The actual adjustment should be 0 when we calculate, since we zero it out
|
|
80
|
+
# So we need to check if the file checksum with zeroed adjustment equals magic
|
|
81
|
+
if file_checksum != magic
|
|
82
|
+
# Calculate what the adjustment should be
|
|
83
|
+
temp_checksum = (file_checksum - actual_adjustment) & 0xFFFFFFFF
|
|
84
|
+
correct_adjustment = (magic - temp_checksum) & 0xFFFFFFFF
|
|
85
|
+
|
|
86
|
+
if actual_adjustment != correct_adjustment
|
|
87
|
+
issues << {
|
|
88
|
+
severity: "error",
|
|
89
|
+
category: "checksum",
|
|
90
|
+
message: "Invalid head table checksum adjustment (expected: 0x#{correct_adjustment.to_s(16)}, got: 0x#{actual_adjustment.to_s(16)})",
|
|
91
|
+
location: "head table",
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
issues << {
|
|
97
|
+
severity: "error",
|
|
98
|
+
category: "checksum",
|
|
99
|
+
message: "Failed to validate head checksum adjustment: #{e.message}",
|
|
100
|
+
location: "head table",
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
issues
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check individual table checksums
|
|
108
|
+
#
|
|
109
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
110
|
+
# @return [Array<Hash>] Array of table checksum issues
|
|
111
|
+
def check_table_checksums(font)
|
|
112
|
+
issues = []
|
|
113
|
+
|
|
114
|
+
skip_tables = @checksum_config["skip_tables"] || []
|
|
115
|
+
|
|
116
|
+
font.tables.each do |table_entry|
|
|
117
|
+
tag = table_entry.tag
|
|
118
|
+
|
|
119
|
+
# Skip tables that are exempt from checksum validation
|
|
120
|
+
next if skip_tables.include?(tag)
|
|
121
|
+
|
|
122
|
+
# Get table data
|
|
123
|
+
table_data = font.table_data[tag]
|
|
124
|
+
next unless table_data
|
|
125
|
+
|
|
126
|
+
# Calculate checksum for the table
|
|
127
|
+
calculated_checksum = calculate_table_checksum(table_data)
|
|
128
|
+
declared_checksum = table_entry.checksum
|
|
129
|
+
|
|
130
|
+
# Special handling for head table (checksum adjustment field should be 0)
|
|
131
|
+
if tag == Constants::HEAD_TAG
|
|
132
|
+
# Zero out checksum adjustment field for calculation
|
|
133
|
+
modified_data = table_data.dup
|
|
134
|
+
modified_data[8, 4] = "\x00\x00\x00\x00"
|
|
135
|
+
calculated_checksum = calculate_table_checksum(modified_data)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if calculated_checksum != declared_checksum
|
|
139
|
+
issues << {
|
|
140
|
+
severity: "warning",
|
|
141
|
+
category: "checksum",
|
|
142
|
+
message: "Table '#{tag}' checksum mismatch (expected: 0x#{declared_checksum.to_s(16)}, got: 0x#{calculated_checksum.to_s(16)})",
|
|
143
|
+
location: "#{tag} table",
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
issues
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Calculate checksum for table data
|
|
152
|
+
#
|
|
153
|
+
# @param data [String] The table data
|
|
154
|
+
# @return [Integer] The calculated checksum
|
|
155
|
+
def calculate_table_checksum(data)
|
|
156
|
+
sum = 0
|
|
157
|
+
# Pad to 4-byte boundary
|
|
158
|
+
padded_data = data + ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
159
|
+
|
|
160
|
+
# Sum all 32-bit values
|
|
161
|
+
(0...padded_data.bytesize).step(4) do |i|
|
|
162
|
+
value = padded_data.byteslice(i, 4).unpack1("N")
|
|
163
|
+
sum = (sum + value) & 0xFFFFFFFF
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sum
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Validation
|
|
5
|
+
# ConsistencyValidator validates cross-table consistency
|
|
6
|
+
#
|
|
7
|
+
# This validator ensures that references between tables are valid,
|
|
8
|
+
# such as cmap glyph references, hmtx entry counts, and variable
|
|
9
|
+
# font table consistency.
|
|
10
|
+
#
|
|
11
|
+
# Single Responsibility: Cross-table data consistency validation
|
|
12
|
+
#
|
|
13
|
+
# @example Validating consistency
|
|
14
|
+
# validator = ConsistencyValidator.new(rules)
|
|
15
|
+
# issues = validator.validate(font)
|
|
16
|
+
class ConsistencyValidator
|
|
17
|
+
# Initialize consistency validator
|
|
18
|
+
#
|
|
19
|
+
# @param rules [Hash] Validation rules configuration
|
|
20
|
+
def initialize(rules)
|
|
21
|
+
@rules = rules
|
|
22
|
+
@consistency_config = rules["consistency_checks"] || {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Validate font consistency
|
|
26
|
+
#
|
|
27
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
28
|
+
# @return [Array<Hash>] Array of validation issues
|
|
29
|
+
def validate(font)
|
|
30
|
+
issues = []
|
|
31
|
+
|
|
32
|
+
# Check hmtx consistency if enabled
|
|
33
|
+
issues.concat(check_hmtx_consistency(font)) if should_check?("check_hmtx_consistency")
|
|
34
|
+
|
|
35
|
+
# Check name table consistency if enabled
|
|
36
|
+
issues.concat(check_name_consistency(font)) if should_check?("check_name_consistency")
|
|
37
|
+
|
|
38
|
+
# Check variable font consistency if enabled
|
|
39
|
+
issues.concat(check_variable_consistency(font)) if should_check?("check_variable_consistency")
|
|
40
|
+
|
|
41
|
+
issues
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Check if a validation should be performed
|
|
47
|
+
#
|
|
48
|
+
# @param check_name [String] The check name
|
|
49
|
+
# @return [Boolean] true if check should be performed
|
|
50
|
+
def should_check?(check_name)
|
|
51
|
+
@rules.dig("validation_levels", "standard", check_name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check hmtx entry count matches glyph count
|
|
55
|
+
#
|
|
56
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
57
|
+
# @return [Array<Hash>] Array of hmtx consistency issues
|
|
58
|
+
def check_hmtx_consistency(font)
|
|
59
|
+
issues = []
|
|
60
|
+
|
|
61
|
+
hmtx = font.table(Constants::HMTX_TAG)
|
|
62
|
+
maxp = font.table(Constants::MAXP_TAG)
|
|
63
|
+
hhea = font.table(Constants::HHEA_TAG)
|
|
64
|
+
|
|
65
|
+
return issues unless hmtx && maxp && hhea
|
|
66
|
+
|
|
67
|
+
glyph_count = maxp.num_glyphs
|
|
68
|
+
num_of_long_hor_metrics = hhea.number_of_h_metrics
|
|
69
|
+
|
|
70
|
+
# Verify the structure makes sense
|
|
71
|
+
if num_of_long_hor_metrics > glyph_count
|
|
72
|
+
issues << {
|
|
73
|
+
severity: "error",
|
|
74
|
+
category: "consistency",
|
|
75
|
+
message: "hhea number_of_h_metrics (#{num_of_long_hor_metrics}) exceeds glyph count (#{glyph_count})",
|
|
76
|
+
location: "hhea/hmtx tables",
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if num_of_long_hor_metrics < 1
|
|
81
|
+
issues << {
|
|
82
|
+
severity: "error",
|
|
83
|
+
category: "consistency",
|
|
84
|
+
message: "hhea number_of_h_metrics is #{num_of_long_hor_metrics}, must be at least 1",
|
|
85
|
+
location: "hhea table",
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
issues
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check name table for consistency issues
|
|
93
|
+
#
|
|
94
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
95
|
+
# @return [Array<Hash>] Array of name table issues
|
|
96
|
+
def check_name_consistency(font)
|
|
97
|
+
issues = []
|
|
98
|
+
|
|
99
|
+
name = font.table(Constants::NAME_TAG)
|
|
100
|
+
return issues unless name
|
|
101
|
+
|
|
102
|
+
# Check that required name IDs are present
|
|
103
|
+
required_name_ids = [
|
|
104
|
+
Tables::Name::FAMILY, # 1
|
|
105
|
+
Tables::Name::SUBFAMILY, # 2
|
|
106
|
+
Tables::Name::FULL_NAME, # 4
|
|
107
|
+
Tables::Name::VERSION, # 5
|
|
108
|
+
Tables::Name::POSTSCRIPT_NAME, # 6
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
required_name_ids.each do |name_id|
|
|
112
|
+
has_entry = name.name_records.any? do |record|
|
|
113
|
+
record.name_id == name_id
|
|
114
|
+
end
|
|
115
|
+
unless has_entry
|
|
116
|
+
issues << {
|
|
117
|
+
severity: "warning",
|
|
118
|
+
category: "consistency",
|
|
119
|
+
message: "Missing recommended name ID #{name_id}",
|
|
120
|
+
location: "name table",
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check for duplicate entries (same platform/encoding/language/nameID)
|
|
126
|
+
seen = {}
|
|
127
|
+
name.name_records.each do |record|
|
|
128
|
+
key = [record.platform_id, record.encoding_id, record.language_id,
|
|
129
|
+
record.name_id]
|
|
130
|
+
if seen[key]
|
|
131
|
+
issues << {
|
|
132
|
+
severity: "warning",
|
|
133
|
+
category: "consistency",
|
|
134
|
+
message: "Duplicate name record: platform=#{record.platform_id}, encoding=#{record.encoding_id}, language=#{record.language_id}, nameID=#{record.name_id}",
|
|
135
|
+
location: "name table",
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
seen[key] = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
issues
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check variable font table consistency
|
|
145
|
+
#
|
|
146
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
147
|
+
# @return [Array<Hash>] Array of variable font issues
|
|
148
|
+
def check_variable_consistency(font)
|
|
149
|
+
issues = []
|
|
150
|
+
|
|
151
|
+
# Only check if this is a variable font
|
|
152
|
+
return issues unless font.has_table?(Constants::FVAR_TAG)
|
|
153
|
+
|
|
154
|
+
fvar = font.table(Constants::FVAR_TAG)
|
|
155
|
+
return issues unless fvar
|
|
156
|
+
|
|
157
|
+
axis_count = fvar.axes.length
|
|
158
|
+
|
|
159
|
+
# For TrueType variable fonts, check gvar consistency
|
|
160
|
+
if font.has_table?(Constants::GVAR_TAG)
|
|
161
|
+
gvar = font.table(Constants::GVAR_TAG)
|
|
162
|
+
gvar_axis_count = gvar.axis_count
|
|
163
|
+
if gvar_axis_count != axis_count
|
|
164
|
+
issues << {
|
|
165
|
+
severity: "error",
|
|
166
|
+
category: "consistency",
|
|
167
|
+
message: "fvar axis count (#{axis_count}) doesn't match gvar axis count (#{gvar_axis_count})",
|
|
168
|
+
location: "fvar/gvar tables",
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check that recommended variation tables are present
|
|
174
|
+
unless font.has_table?(Constants::GVAR_TAG) || font.has_table?(Constants::CFF2_TAG)
|
|
175
|
+
issues << {
|
|
176
|
+
severity: "error",
|
|
177
|
+
category: "consistency",
|
|
178
|
+
message: "Variable font missing gvar (TrueType) or CFF2 (CFF) table",
|
|
179
|
+
location: "variable font",
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check for recommended metrics variation tables
|
|
184
|
+
unless font.has_table?(Constants::HVAR_TAG)
|
|
185
|
+
issues << {
|
|
186
|
+
severity: "info",
|
|
187
|
+
category: "consistency",
|
|
188
|
+
message: "Variable font missing HVAR table (recommended for better rendering)",
|
|
189
|
+
location: nil,
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
issues
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Validation
|
|
5
|
+
# StructureValidator validates the structural integrity of fonts
|
|
6
|
+
#
|
|
7
|
+
# This validator checks the SFNT structure, table offsets, table ordering,
|
|
8
|
+
# and other structural properties that ensure the font file is well-formed.
|
|
9
|
+
#
|
|
10
|
+
# Single Responsibility: Font structure and SFNT format validation
|
|
11
|
+
#
|
|
12
|
+
# @example Validating structure
|
|
13
|
+
# validator = StructureValidator.new(rules)
|
|
14
|
+
# issues = validator.validate(font)
|
|
15
|
+
class StructureValidator
|
|
16
|
+
# Initialize structure validator
|
|
17
|
+
#
|
|
18
|
+
# @param rules [Hash] Validation rules configuration
|
|
19
|
+
def initialize(rules)
|
|
20
|
+
@rules = rules
|
|
21
|
+
@structure_config = rules["structure_validation"] || {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Validate font structure
|
|
25
|
+
#
|
|
26
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
27
|
+
# @return [Array<Hash>] Array of validation issues
|
|
28
|
+
def validate(font)
|
|
29
|
+
issues = []
|
|
30
|
+
|
|
31
|
+
# Check glyph count consistency
|
|
32
|
+
issues.concat(check_glyph_consistency(font))
|
|
33
|
+
|
|
34
|
+
# Check table offsets
|
|
35
|
+
issues.concat(check_table_offsets(font)) if @rules.dig(
|
|
36
|
+
"validation_levels", "standard", "check_table_offsets"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Check table ordering (optional optimization check)
|
|
40
|
+
issues.concat(check_table_ordering(font)) if @rules.dig(
|
|
41
|
+
"validation_levels", "standard", "check_table_ordering"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
issues
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Check glyph count consistency across tables
|
|
50
|
+
#
|
|
51
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
52
|
+
# @return [Array<Hash>] Array of consistency issues
|
|
53
|
+
def check_glyph_consistency(font)
|
|
54
|
+
issues = []
|
|
55
|
+
|
|
56
|
+
# Get glyph count from maxp table
|
|
57
|
+
maxp = font.table(Constants::MAXP_TAG)
|
|
58
|
+
return issues unless maxp
|
|
59
|
+
|
|
60
|
+
expected_count = maxp.num_glyphs
|
|
61
|
+
|
|
62
|
+
# For TrueType fonts, check glyf table glyph count
|
|
63
|
+
if font.has_table?(Constants::GLYF_TAG)
|
|
64
|
+
glyf = font.table(Constants::GLYF_TAG)
|
|
65
|
+
actual_count = glyf.glyphs.length if glyf.respond_to?(:glyphs)
|
|
66
|
+
|
|
67
|
+
if actual_count && actual_count != expected_count
|
|
68
|
+
issues << {
|
|
69
|
+
severity: "error",
|
|
70
|
+
category: "structure",
|
|
71
|
+
message: "Glyph count mismatch: maxp=#{expected_count}, glyf=#{actual_count}",
|
|
72
|
+
location: "glyf table",
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check glyph count bounds with safe defaults
|
|
78
|
+
min_glyph_count = @structure_config["min_glyph_count"] || 1
|
|
79
|
+
max_glyph_count = @structure_config["max_glyph_count"] || 65536
|
|
80
|
+
|
|
81
|
+
if expected_count < min_glyph_count
|
|
82
|
+
issues << {
|
|
83
|
+
severity: "error",
|
|
84
|
+
category: "structure",
|
|
85
|
+
message: "Glyph count (#{expected_count}) below minimum (#{min_glyph_count})",
|
|
86
|
+
location: "maxp table",
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if expected_count > max_glyph_count
|
|
91
|
+
issues << {
|
|
92
|
+
severity: "error",
|
|
93
|
+
category: "structure",
|
|
94
|
+
message: "Glyph count (#{expected_count}) exceeds maximum (#{max_glyph_count})",
|
|
95
|
+
location: "maxp table",
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
issues
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check that table offsets are valid
|
|
103
|
+
#
|
|
104
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
105
|
+
# @return [Array<Hash>] Array of offset issues
|
|
106
|
+
def check_table_offsets(font)
|
|
107
|
+
issues = []
|
|
108
|
+
|
|
109
|
+
min_offset = @structure_config["min_table_offset"] || 12
|
|
110
|
+
max_size = @structure_config["max_table_size"] || 104857600
|
|
111
|
+
|
|
112
|
+
font.tables.each do |table_entry|
|
|
113
|
+
tag = table_entry.tag
|
|
114
|
+
offset = table_entry.offset
|
|
115
|
+
length = table_entry.table_length
|
|
116
|
+
|
|
117
|
+
# Check minimum offset
|
|
118
|
+
if offset < min_offset
|
|
119
|
+
issues << {
|
|
120
|
+
severity: "error",
|
|
121
|
+
category: "structure",
|
|
122
|
+
message: "Table '#{tag}' has invalid offset: #{offset} (minimum: #{min_offset})",
|
|
123
|
+
location: "#{tag} table directory",
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check for reasonable table size
|
|
128
|
+
if length > max_size
|
|
129
|
+
issues << {
|
|
130
|
+
severity: "warning",
|
|
131
|
+
category: "structure",
|
|
132
|
+
message: "Table '#{tag}' has unusually large size: #{length} bytes",
|
|
133
|
+
location: "#{tag} table",
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check alignment (tables should be 4-byte aligned)
|
|
138
|
+
alignment = @structure_config["table_alignment"] || 4
|
|
139
|
+
if offset % alignment != 0
|
|
140
|
+
issues << {
|
|
141
|
+
severity: "warning",
|
|
142
|
+
category: "structure",
|
|
143
|
+
message: "Table '#{tag}' is not #{alignment}-byte aligned (offset: #{offset})",
|
|
144
|
+
location: "#{tag} table directory",
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
issues
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Check table ordering (optimization check, not critical)
|
|
153
|
+
#
|
|
154
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
155
|
+
# @return [Array<Hash>] Array of ordering issues
|
|
156
|
+
def check_table_ordering(font)
|
|
157
|
+
issues = []
|
|
158
|
+
|
|
159
|
+
# Recommended table order for optimal loading
|
|
160
|
+
recommended_order = [
|
|
161
|
+
Constants::HEAD_TAG,
|
|
162
|
+
Constants::HHEA_TAG,
|
|
163
|
+
Constants::MAXP_TAG,
|
|
164
|
+
Constants::OS2_TAG,
|
|
165
|
+
Constants::NAME_TAG,
|
|
166
|
+
Constants::CMAP_TAG,
|
|
167
|
+
Constants::POST_TAG,
|
|
168
|
+
Constants::GLYF_TAG,
|
|
169
|
+
Constants::LOCA_TAG,
|
|
170
|
+
Constants::HMTX_TAG,
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# Get actual table order
|
|
174
|
+
actual_order = font.table_names
|
|
175
|
+
|
|
176
|
+
# Check if critical tables are in recommended order
|
|
177
|
+
critical_tables = recommended_order.take(7) # head through post
|
|
178
|
+
actual_critical = actual_order.select do |tag|
|
|
179
|
+
critical_tables.include?(tag)
|
|
180
|
+
end
|
|
181
|
+
expected_critical = critical_tables.select do |tag|
|
|
182
|
+
actual_order.include?(tag)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if actual_critical != expected_critical
|
|
186
|
+
issues << {
|
|
187
|
+
severity: "info",
|
|
188
|
+
category: "structure",
|
|
189
|
+
message: "Tables not in optimal order for performance",
|
|
190
|
+
location: nil,
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
issues
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|