fontisan 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +168 -32
- data/README.adoc +673 -1091
- data/lib/fontisan/cli.rb +94 -13
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +13 -12
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Parsers
|
|
8
|
+
# Parser for Apple dfont (Data Fork Font) resource fork format.
|
|
9
|
+
#
|
|
10
|
+
# dfont files store resource fork data in the data fork, containing
|
|
11
|
+
# TrueType or OpenType SFNT data embedded in a resource fork structure.
|
|
12
|
+
#
|
|
13
|
+
# @example Extract SFNT from dfont
|
|
14
|
+
# File.open("font.dfont", "rb") do |io|
|
|
15
|
+
# sfnt_data = DfontParser.extract_sfnt(io)
|
|
16
|
+
# # sfnt_data is raw SFNT binary
|
|
17
|
+
# end
|
|
18
|
+
class DfontParser
|
|
19
|
+
# Resource fork header structure (16 bytes)
|
|
20
|
+
class ResourceHeader < BinData::Record
|
|
21
|
+
endian :big
|
|
22
|
+
uint32 :resource_data_offset # Offset to resource data section
|
|
23
|
+
uint32 :resource_map_offset # Offset to resource map
|
|
24
|
+
uint32 :resource_data_length # Length of resource data
|
|
25
|
+
uint32 :resource_map_length # Length of resource map
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resource type entry in type list
|
|
29
|
+
class ResourceType < BinData::Record
|
|
30
|
+
endian :big
|
|
31
|
+
string :type_code, length: 4 # Resource type (e.g., 'sfnt')
|
|
32
|
+
uint16 :resource_count_minus_1 # Number of resources - 1
|
|
33
|
+
uint16 :reference_list_offset # Offset to reference list
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Resource reference in reference list
|
|
37
|
+
class ResourceReference < BinData::Record
|
|
38
|
+
endian :big
|
|
39
|
+
uint16 :resource_id # Resource ID
|
|
40
|
+
int16 :name_offset # Offset to name in name list (-1 if none)
|
|
41
|
+
uint8 :attributes # Resource attributes
|
|
42
|
+
bit24 :data_offset # Offset to data (relative to resource data section)
|
|
43
|
+
uint32 :reserved # Reserved (handle to resource in memory)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extract SFNT data from dfont file
|
|
47
|
+
#
|
|
48
|
+
# @param io [IO] Open file handle
|
|
49
|
+
# @param index [Integer] Font index for multi-font suitcases (default: 0)
|
|
50
|
+
# @return [String] Raw SFNT binary data
|
|
51
|
+
# @raise [InvalidFontError] if not valid dfont or index out of range
|
|
52
|
+
def self.extract_sfnt(io, index: 0)
|
|
53
|
+
header = parse_header(io)
|
|
54
|
+
sfnt_resources = find_sfnt_resources(io, header)
|
|
55
|
+
|
|
56
|
+
if sfnt_resources.empty?
|
|
57
|
+
raise InvalidFontError, "No sfnt resources found in dfont file"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if index >= sfnt_resources.length
|
|
61
|
+
raise InvalidFontError,
|
|
62
|
+
"Font index #{index} out of range (dfont has #{sfnt_resources.length} fonts)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
extract_resource_data(io, header, sfnt_resources[index])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Count number of sfnt resources (fonts) in dfont
|
|
69
|
+
#
|
|
70
|
+
# @param io [IO] Open file handle
|
|
71
|
+
# @return [Integer] Number of fonts
|
|
72
|
+
def self.sfnt_count(io)
|
|
73
|
+
header = parse_header(io)
|
|
74
|
+
sfnt_resources = find_sfnt_resources(io, header)
|
|
75
|
+
sfnt_resources.length
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if file is valid dfont resource fork
|
|
79
|
+
#
|
|
80
|
+
# @param io [IO] Open file handle
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def self.dfont?(io)
|
|
83
|
+
io.rewind
|
|
84
|
+
header_bytes = io.read(16)
|
|
85
|
+
io.rewind
|
|
86
|
+
|
|
87
|
+
return false if header_bytes.nil? || header_bytes.length < 16
|
|
88
|
+
|
|
89
|
+
# Basic sanity check on resource fork structure
|
|
90
|
+
data_offset = header_bytes[0..3].unpack1("N")
|
|
91
|
+
map_offset = header_bytes[4..7].unpack1("N")
|
|
92
|
+
|
|
93
|
+
data_offset.positive? && map_offset > data_offset
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse resource fork header
|
|
97
|
+
#
|
|
98
|
+
# @param io [IO] Open file handle
|
|
99
|
+
# @return [ResourceHeader] Parsed header
|
|
100
|
+
# @raise [InvalidFontError] if header invalid
|
|
101
|
+
# @api private
|
|
102
|
+
def self.parse_header(io)
|
|
103
|
+
io.rewind
|
|
104
|
+
ResourceHeader.read(io)
|
|
105
|
+
rescue BinData::ValidityError => e
|
|
106
|
+
raise InvalidFontError, "Invalid dfont resource header: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Find all sfnt resources in resource map
|
|
110
|
+
#
|
|
111
|
+
# @param io [IO] Open file handle
|
|
112
|
+
# @param header [ResourceHeader] Parsed header
|
|
113
|
+
# @return [Array<Hash>] Array of resource info hashes with :id and :offset
|
|
114
|
+
# @api private
|
|
115
|
+
def self.find_sfnt_resources(io, header)
|
|
116
|
+
# Seek to resource map
|
|
117
|
+
io.seek(header.resource_map_offset)
|
|
118
|
+
|
|
119
|
+
# Skip resource map header (22 bytes reserved + 4 bytes attributes + 2 bytes type list offset + 2 bytes name list offset)
|
|
120
|
+
# The actual layout is:
|
|
121
|
+
# - Bytes 0-15: Copy of resource header (16 bytes)
|
|
122
|
+
# - Bytes 16-19: Reserved for handle to next resource map (4 bytes)
|
|
123
|
+
# - Bytes 20-21: Reserved for file reference number (2 bytes)
|
|
124
|
+
# - Bytes 22-23: Resource file attributes (2 bytes)
|
|
125
|
+
# - Bytes 24-25: Offset to type list (2 bytes)
|
|
126
|
+
# - Bytes 26-27: Offset to name list (2 bytes)
|
|
127
|
+
io.seek(header.resource_map_offset + 24)
|
|
128
|
+
|
|
129
|
+
# Read type list offset (relative to start of resource map)
|
|
130
|
+
type_list_offset = io.read(2).unpack1("n")
|
|
131
|
+
|
|
132
|
+
# Seek to type list
|
|
133
|
+
io.seek(header.resource_map_offset + type_list_offset)
|
|
134
|
+
|
|
135
|
+
# Read number of types minus 1
|
|
136
|
+
type_count_minus_1 = io.read(2).unpack1("n")
|
|
137
|
+
type_count = type_count_minus_1 + 1
|
|
138
|
+
|
|
139
|
+
# Find 'sfnt' type in type list
|
|
140
|
+
sfnt_type = nil
|
|
141
|
+
type_count.times do
|
|
142
|
+
type_entry = ResourceType.read(io)
|
|
143
|
+
|
|
144
|
+
if type_entry.type_code == "sfnt"
|
|
145
|
+
sfnt_type = type_entry
|
|
146
|
+
break
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return [] unless sfnt_type
|
|
151
|
+
|
|
152
|
+
# Read reference list for sfnt resources
|
|
153
|
+
reference_list_offset = header.resource_map_offset + type_list_offset + sfnt_type.reference_list_offset
|
|
154
|
+
io.seek(reference_list_offset)
|
|
155
|
+
|
|
156
|
+
resource_count = sfnt_type.resource_count_minus_1 + 1
|
|
157
|
+
resources = []
|
|
158
|
+
|
|
159
|
+
resource_count.times do
|
|
160
|
+
ref = ResourceReference.read(io)
|
|
161
|
+
resources << { id: ref.resource_id, offset: ref.data_offset }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
resources
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Extract resource data at specific offset
|
|
168
|
+
#
|
|
169
|
+
# @param io [IO] Open file handle
|
|
170
|
+
# @param header [ResourceHeader] Parsed header
|
|
171
|
+
# @param resource_info [Hash] Resource info with :offset
|
|
172
|
+
# @return [String] Raw SFNT binary data
|
|
173
|
+
# @api private
|
|
174
|
+
def self.extract_resource_data(io, header, resource_info)
|
|
175
|
+
# Calculate absolute offset to resource data
|
|
176
|
+
# The offset in the reference is relative to the start of the resource data section
|
|
177
|
+
data_offset = header.resource_data_offset + resource_info[:offset]
|
|
178
|
+
|
|
179
|
+
io.seek(data_offset)
|
|
180
|
+
|
|
181
|
+
# Read data length (first 4 bytes of resource data)
|
|
182
|
+
data_length = io.read(4).unpack1("N")
|
|
183
|
+
|
|
184
|
+
# Read the actual data
|
|
185
|
+
io.read(data_length)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private_class_method :parse_header, :find_sfnt_resources,
|
|
189
|
+
:extract_resource_data
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -263,16 +263,12 @@ module Fontisan
|
|
|
263
263
|
def validate_output
|
|
264
264
|
return unless File.exist?(@output_path)
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
# Use new validation framework with production profile
|
|
267
|
+
report = Fontisan.validate(@output_path, profile: :production)
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
font = FontLoader.load(@output_path, mode: :full)
|
|
270
|
-
validator = Validation::Validator.new
|
|
271
|
-
result = validator.validate(font, @output_path)
|
|
269
|
+
return if report.valid?
|
|
272
270
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
error_messages = result.errors.map(&:message).join(", ")
|
|
271
|
+
error_messages = report.errors.map(&:message).join(", ")
|
|
276
272
|
raise Error, "Output validation failed: #{error_messages}"
|
|
277
273
|
end
|
|
278
274
|
|
data/lib/fontisan/tables/cmap.rb
CHANGED
|
@@ -36,8 +36,6 @@ module Fontisan
|
|
|
36
36
|
@unicode_mappings ||= parse_mappings
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
39
|
# Parse all encoding records and extract Unicode mappings
|
|
42
40
|
def parse_mappings
|
|
43
41
|
mappings = {}
|
|
@@ -279,6 +277,88 @@ module Fontisan
|
|
|
279
277
|
mappings[code] = glyph_index if glyph_index != 0
|
|
280
278
|
end
|
|
281
279
|
end
|
|
280
|
+
|
|
281
|
+
public
|
|
282
|
+
|
|
283
|
+
# Validation helper: Check if version is valid
|
|
284
|
+
#
|
|
285
|
+
# cmap version should be 0
|
|
286
|
+
#
|
|
287
|
+
# @return [Boolean] True if version is 0
|
|
288
|
+
def valid_version?
|
|
289
|
+
version == 0
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Validation helper: Check if at least one subtable exists
|
|
293
|
+
#
|
|
294
|
+
# @return [Boolean] True if num_tables > 0
|
|
295
|
+
def has_subtables?
|
|
296
|
+
num_tables && num_tables > 0
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Validation helper: Check if Unicode mapping exists
|
|
300
|
+
#
|
|
301
|
+
# @return [Boolean] True if Unicode mappings are present
|
|
302
|
+
def has_unicode_mapping?
|
|
303
|
+
!unicode_mappings.nil? && !unicode_mappings.empty?
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Validation helper: Check if BMP coverage exists
|
|
307
|
+
#
|
|
308
|
+
# Checks if the Basic Multilingual Plane (U+0000-U+FFFF) has mappings
|
|
309
|
+
#
|
|
310
|
+
# @return [Boolean] True if BMP characters are mapped
|
|
311
|
+
def has_bmp_coverage?
|
|
312
|
+
mappings = unicode_mappings
|
|
313
|
+
return false if mappings.nil? || mappings.empty?
|
|
314
|
+
|
|
315
|
+
# Check if any BMP characters (0x0000-0xFFFF) are mapped
|
|
316
|
+
mappings.keys.any? { |code| code.between?(0x0000, 0xFFFF) }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Validation helper: Check if required characters are mapped
|
|
320
|
+
#
|
|
321
|
+
# Checks for essential characters like space (U+0020)
|
|
322
|
+
#
|
|
323
|
+
# @param required_chars [Array<Integer>] Character codes that must be present
|
|
324
|
+
# @return [Boolean] True if all required characters are mapped
|
|
325
|
+
def has_required_characters?(*required_chars)
|
|
326
|
+
mappings = unicode_mappings
|
|
327
|
+
return false if mappings.nil?
|
|
328
|
+
|
|
329
|
+
required_chars.all? { |code| mappings.key?(code) }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Validation helper: Check if format 4 subtable exists
|
|
333
|
+
#
|
|
334
|
+
# Format 4 is the minimum requirement for Unicode BMP support
|
|
335
|
+
#
|
|
336
|
+
# @return [Boolean] True if format 4 subtable is found
|
|
337
|
+
def has_format_4_subtable?
|
|
338
|
+
data = to_binary_s
|
|
339
|
+
records = read_encoding_records(data)
|
|
340
|
+
|
|
341
|
+
records.any? do |record|
|
|
342
|
+
subtable_data = extract_subtable_data(record, data)
|
|
343
|
+
next false unless subtable_data && subtable_data.length >= 2
|
|
344
|
+
|
|
345
|
+
format = subtable_data[0, 2].unpack1("n")
|
|
346
|
+
format == 4
|
|
347
|
+
end
|
|
348
|
+
rescue StandardError
|
|
349
|
+
false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Validation helper: Check if glyph indices are within bounds
|
|
353
|
+
#
|
|
354
|
+
# @param max_glyph_id [Integer] Maximum valid glyph ID from maxp table
|
|
355
|
+
# @return [Boolean] True if all mapped glyph IDs are valid
|
|
356
|
+
def valid_glyph_indices?(max_glyph_id)
|
|
357
|
+
mappings = unicode_mappings
|
|
358
|
+
return true if mappings.nil? || mappings.empty?
|
|
359
|
+
|
|
360
|
+
mappings.values.all? { |glyph_id| glyph_id >= 0 && glyph_id < max_glyph_id }
|
|
361
|
+
end
|
|
282
362
|
end
|
|
283
363
|
end
|
|
284
364
|
end
|
data/lib/fontisan/tables/glyf.rb
CHANGED
|
@@ -158,6 +158,124 @@ module Fontisan
|
|
|
158
158
|
@glyphs_cache ||= {}
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
# Validation helper: Check if all non-special glyphs have contours
|
|
162
|
+
#
|
|
163
|
+
# The .notdef glyph (ID 0) can be empty, but other glyphs should have geometry
|
|
164
|
+
#
|
|
165
|
+
# @param loca [Loca] Loca table for glyph access
|
|
166
|
+
# @param head [Head] Head table for context
|
|
167
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
168
|
+
# @return [Boolean] True if all non-special glyphs have contours
|
|
169
|
+
def no_empty_glyphs_except_special?(loca, head, num_glyphs)
|
|
170
|
+
# Check glyphs 1 through num_glyphs-1 (.notdef at 0 can be empty)
|
|
171
|
+
(1...num_glyphs).all? do |glyph_id|
|
|
172
|
+
size = loca.size_of(glyph_id)
|
|
173
|
+
# Empty glyphs (like space) are allowed, but check if they should be empty
|
|
174
|
+
# This is a basic check - we just ensure non-control glyphs have data
|
|
175
|
+
size.nil? || size.positive?
|
|
176
|
+
end
|
|
177
|
+
rescue StandardError
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Validation helper: Check if any glyphs are clipped (exceed bounds)
|
|
182
|
+
#
|
|
183
|
+
# Validates that glyph coordinates don't exceed head table's bounding box
|
|
184
|
+
#
|
|
185
|
+
# @param loca [Loca] Loca table for glyph access
|
|
186
|
+
# @param head [Head] Head table for bounds reference
|
|
187
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
188
|
+
# @return [Boolean] True if no glyphs exceed the font's bounding box
|
|
189
|
+
def no_clipped_glyphs?(loca, head, num_glyphs)
|
|
190
|
+
font_x_min = head.x_min
|
|
191
|
+
font_y_min = head.y_min
|
|
192
|
+
font_x_max = head.x_max
|
|
193
|
+
font_y_max = head.y_max
|
|
194
|
+
|
|
195
|
+
(0...num_glyphs).all? do |glyph_id|
|
|
196
|
+
glyph = glyph_for(glyph_id, loca, head)
|
|
197
|
+
next true if glyph.nil? # Empty glyphs are OK
|
|
198
|
+
|
|
199
|
+
# Check if glyph bounds are within font bounds
|
|
200
|
+
glyph.x_min >= font_x_min &&
|
|
201
|
+
glyph.y_min >= font_y_min &&
|
|
202
|
+
glyph.x_max <= font_x_max &&
|
|
203
|
+
glyph.y_max <= font_y_max
|
|
204
|
+
end
|
|
205
|
+
rescue StandardError
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Validation helper: Check if TrueType instructions are sound
|
|
210
|
+
#
|
|
211
|
+
# Validates that glyph instructions (if present) are parseable
|
|
212
|
+
# This is a basic check that ensures instructions exist and have valid length
|
|
213
|
+
#
|
|
214
|
+
# @param loca [Loca] Loca table for glyph access
|
|
215
|
+
# @param head [Head] Head table for context
|
|
216
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
217
|
+
# @return [Boolean] True if all instructions are valid or absent
|
|
218
|
+
def instructions_sound?(loca, head, num_glyphs)
|
|
219
|
+
(0...num_glyphs).all? do |glyph_id|
|
|
220
|
+
glyph = glyph_for(glyph_id, loca, head)
|
|
221
|
+
next true if glyph.nil? # Empty glyphs are OK
|
|
222
|
+
|
|
223
|
+
# Simple glyphs have instructions
|
|
224
|
+
if glyph.respond_to?(:instruction_length)
|
|
225
|
+
inst_len = glyph.instruction_length
|
|
226
|
+
# If instructions present, length should be reasonable
|
|
227
|
+
inst_len.nil? || inst_len >= 0
|
|
228
|
+
else
|
|
229
|
+
# Compound glyphs may have instructions too
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
rescue StandardError
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validation helper: Check if glyph has valid number of contours
|
|
238
|
+
#
|
|
239
|
+
# @param glyph_id [Integer] Glyph ID to check
|
|
240
|
+
# @param loca [Loca] Loca table for glyph access
|
|
241
|
+
# @param head [Head] Head table for context
|
|
242
|
+
# @return [Boolean] True if contour count is valid
|
|
243
|
+
def valid_contour_count?(glyph_id, loca, head)
|
|
244
|
+
glyph = glyph_for(glyph_id, loca, head)
|
|
245
|
+
return true if glyph.nil? # Empty glyphs are OK
|
|
246
|
+
|
|
247
|
+
# Simple glyphs: contours should be >= 0
|
|
248
|
+
# Compound glyphs: numberOfContours = -1
|
|
249
|
+
if glyph.respond_to?(:num_contours)
|
|
250
|
+
glyph.num_contours >= -1
|
|
251
|
+
else
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
rescue StandardError
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Validation helper: Check if all glyphs are accessible
|
|
259
|
+
#
|
|
260
|
+
# Attempts to access each glyph to ensure no corruption
|
|
261
|
+
#
|
|
262
|
+
# @param loca [Loca] Loca table for glyph access
|
|
263
|
+
# @param head [Head] Head table for context
|
|
264
|
+
# @param num_glyphs [Integer] Total number of glyphs
|
|
265
|
+
# @return [Boolean] True if all glyphs can be accessed
|
|
266
|
+
def all_glyphs_accessible?(loca, head, num_glyphs)
|
|
267
|
+
(0...num_glyphs).all? do |glyph_id|
|
|
268
|
+
begin
|
|
269
|
+
glyph_for(glyph_id, loca, head)
|
|
270
|
+
true
|
|
271
|
+
rescue Fontisan::CorruptedTableError
|
|
272
|
+
false
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
rescue StandardError
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
|
|
161
279
|
private
|
|
162
280
|
|
|
163
281
|
# Validate context and glyph ID
|
data/lib/fontisan/tables/head.rb
CHANGED
|
@@ -82,6 +82,66 @@ module Fontisan
|
|
|
82
82
|
magic_number == MAGIC_NUMBER
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# Validation helper: Check if magic number is valid
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] True if magic number equals 0x5F0F3CF5
|
|
88
|
+
def valid_magic?
|
|
89
|
+
magic_number == MAGIC_NUMBER
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validation helper: Check if version is valid
|
|
93
|
+
#
|
|
94
|
+
# OpenType spec requires version to be 1.0
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean] True if version is 1.0
|
|
97
|
+
def valid_version?
|
|
98
|
+
version_raw == 0x00010000 # Version 1.0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validation helper: Check if units per em is valid
|
|
102
|
+
#
|
|
103
|
+
# Units per em should be a power of 2 between 16 and 16384
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] True if units_per_em is valid
|
|
106
|
+
def valid_units_per_em?
|
|
107
|
+
return false if units_per_em.nil? || units_per_em.zero?
|
|
108
|
+
|
|
109
|
+
# Must be between 16 and 16384
|
|
110
|
+
return false unless units_per_em.between?(16, 16384)
|
|
111
|
+
|
|
112
|
+
# Should be a power of 2 (recommended but not required)
|
|
113
|
+
# Common values: 1000, 1024, 2048
|
|
114
|
+
# We'll allow any value in range for flexibility
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Validation helper: Check if bounding box is valid
|
|
119
|
+
#
|
|
120
|
+
# The bounding box should have xMin < xMax and yMin < yMax
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] True if bounding box coordinates are valid
|
|
123
|
+
def valid_bounding_box?
|
|
124
|
+
x_min < x_max && y_min < y_max
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validation helper: Check if index_to_loc_format is valid
|
|
128
|
+
#
|
|
129
|
+
# Must be 0 (short) or 1 (long)
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] True if format is 0 or 1
|
|
132
|
+
def valid_index_to_loc_format?
|
|
133
|
+
index_to_loc_format == 0 || index_to_loc_format == 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Validation helper: Check if glyph_data_format is valid
|
|
137
|
+
#
|
|
138
|
+
# Must be 0 for current format
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean] True if format is 0
|
|
141
|
+
def valid_glyph_data_format?
|
|
142
|
+
glyph_data_format == 0
|
|
143
|
+
end
|
|
144
|
+
|
|
85
145
|
# Validate magic number and raise error if invalid
|
|
86
146
|
#
|
|
87
147
|
# @raise [Fontisan::CorruptedTableError] If magic number is invalid
|
data/lib/fontisan/tables/hhea.rb
CHANGED
|
@@ -97,6 +97,80 @@ module Fontisan
|
|
|
97
97
|
true
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# Validation helper: Check if version is valid
|
|
101
|
+
#
|
|
102
|
+
# OpenType spec requires version to be 1.0
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean] True if version is 1.0
|
|
105
|
+
def valid_version?
|
|
106
|
+
version_raw == 0x00010000
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validation helper: Check if metric data format is valid
|
|
110
|
+
#
|
|
111
|
+
# Must be 0 for current format
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean] True if format is 0
|
|
114
|
+
def valid_metric_data_format?
|
|
115
|
+
metric_data_format == 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Validation helper: Check if number of h metrics is valid
|
|
119
|
+
#
|
|
120
|
+
# Must be at least 1
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] True if number_of_h_metrics >= 1
|
|
123
|
+
def valid_number_of_h_metrics?
|
|
124
|
+
number_of_h_metrics && number_of_h_metrics >= 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validation helper: Check if ascent/descent values are reasonable
|
|
128
|
+
#
|
|
129
|
+
# Ascent should be positive, descent should be negative
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] True if ascent/descent have correct signs
|
|
132
|
+
def valid_ascent_descent?
|
|
133
|
+
ascent > 0 && descent < 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Validation helper: Check if line gap is non-negative
|
|
137
|
+
#
|
|
138
|
+
# Line gap should be >= 0
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean] True if line_gap >= 0
|
|
141
|
+
def valid_line_gap?
|
|
142
|
+
line_gap >= 0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Validation helper: Check if advance width max is positive
|
|
146
|
+
#
|
|
147
|
+
# Maximum advance width should be > 0
|
|
148
|
+
#
|
|
149
|
+
# @return [Boolean] True if advance_width_max > 0
|
|
150
|
+
def valid_advance_width_max?
|
|
151
|
+
advance_width_max && advance_width_max > 0
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Validation helper: Check if caret slope is valid
|
|
155
|
+
#
|
|
156
|
+
# For vertical text: rise=1, run=0
|
|
157
|
+
# For horizontal italic: both should be non-zero
|
|
158
|
+
#
|
|
159
|
+
# @return [Boolean] True if caret slope values are sensible
|
|
160
|
+
def valid_caret_slope?
|
|
161
|
+
# At least one should be non-zero
|
|
162
|
+
caret_slope_rise != 0 || caret_slope_run != 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Validation helper: Check if extent is reasonable
|
|
166
|
+
#
|
|
167
|
+
# x_max_extent should be positive
|
|
168
|
+
#
|
|
169
|
+
# @return [Boolean] True if x_max_extent > 0
|
|
170
|
+
def valid_x_max_extent?
|
|
171
|
+
x_max_extent > 0
|
|
172
|
+
end
|
|
173
|
+
|
|
100
174
|
# Validate the table and raise error if invalid
|
|
101
175
|
#
|
|
102
176
|
# @raise [Fontisan::CorruptedTableError] If table is invalid
|
data/lib/fontisan/tables/maxp.rb
CHANGED
|
@@ -157,6 +157,66 @@ module Fontisan
|
|
|
157
157
|
true
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# Validation helper: Check if version is valid (0.5 or 1.0)
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean] True if version is 0.5 or 1.0
|
|
163
|
+
def valid_version?
|
|
164
|
+
version_0_5? || version_1_0?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validation helper: Check if number of glyphs is valid
|
|
168
|
+
#
|
|
169
|
+
# Must be at least 1 (.notdef glyph must exist)
|
|
170
|
+
#
|
|
171
|
+
# @return [Boolean] True if num_glyphs >= 1
|
|
172
|
+
def valid_num_glyphs?
|
|
173
|
+
num_glyphs && num_glyphs >= 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Validation helper: Check if maxZones is valid (version 1.0 only)
|
|
177
|
+
#
|
|
178
|
+
# For TrueType fonts, maxZones must be 1 or 2
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] True if maxZones is valid or not applicable
|
|
181
|
+
def valid_max_zones?
|
|
182
|
+
return true if version_0_5? # Not applicable for CFF
|
|
183
|
+
|
|
184
|
+
max_zones && max_zones.between?(1, 2)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Validation helper: Check if all TrueType metrics are present
|
|
188
|
+
#
|
|
189
|
+
# For version 1.0, all max* fields should be present
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean] True if all required fields are present
|
|
192
|
+
def has_truetype_metrics?
|
|
193
|
+
version_1_0? &&
|
|
194
|
+
!max_points.nil? &&
|
|
195
|
+
!max_contours.nil? &&
|
|
196
|
+
!max_composite_points.nil? &&
|
|
197
|
+
!max_composite_contours.nil?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Validation helper: Check if metrics are reasonable
|
|
201
|
+
#
|
|
202
|
+
# Checks that values don't exceed reasonable limits
|
|
203
|
+
#
|
|
204
|
+
# @return [Boolean] True if metrics are within reasonable bounds
|
|
205
|
+
def reasonable_metrics?
|
|
206
|
+
# num_glyphs should not exceed 65535
|
|
207
|
+
return false if num_glyphs > 65535
|
|
208
|
+
|
|
209
|
+
if version_1_0?
|
|
210
|
+
# Check reasonable limits for TrueType metrics
|
|
211
|
+
# These are generous limits to allow for complex fonts
|
|
212
|
+
return false if max_points && max_points > 50000
|
|
213
|
+
return false if max_contours && max_contours > 10000
|
|
214
|
+
return false if max_stack_elements && max_stack_elements > 1000
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
|
|
160
220
|
# Validate the table and raise error if invalid
|
|
161
221
|
#
|
|
162
222
|
# @raise [Fontisan::CorruptedTableError] If table is invalid
|