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
data/lib/fontisan/tables/name.rb
CHANGED
|
@@ -199,6 +199,82 @@ module Fontisan
|
|
|
199
199
|
false
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
# Validation helper: Check if version is valid (0 or 1)
|
|
203
|
+
#
|
|
204
|
+
# @return [Boolean] True if version is 0 or 1
|
|
205
|
+
def valid_version?
|
|
206
|
+
format == 0 || format == 1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Validation helper: Check if encoding combinations are valid
|
|
210
|
+
#
|
|
211
|
+
# According to OpenType spec, certain platform/encoding combinations are valid:
|
|
212
|
+
# - Platform 0 (Unicode): encoding 0-6
|
|
213
|
+
# - Platform 1 (Mac): encoding 0-32
|
|
214
|
+
# - Platform 3 (Windows): encoding 0-10
|
|
215
|
+
#
|
|
216
|
+
# @return [Boolean] True if all encoding heuristics are valid
|
|
217
|
+
def valid_encoding_heuristics?
|
|
218
|
+
name_records.all? do |rec|
|
|
219
|
+
case rec.platform_id
|
|
220
|
+
when PLATFORM_UNICODE
|
|
221
|
+
rec.encoding_id.between?(0, 6)
|
|
222
|
+
when PLATFORM_MACINTOSH
|
|
223
|
+
rec.encoding_id.between?(0, 32)
|
|
224
|
+
when PLATFORM_WINDOWS
|
|
225
|
+
rec.encoding_id.between?(0, 10)
|
|
226
|
+
else
|
|
227
|
+
# Unknown platform - consider invalid
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Validation helper: Check if required platform combinations exist
|
|
234
|
+
#
|
|
235
|
+
# @param combos [Array<Array<Integer>>] Array of [platform_id, encoding_id, language_id] arrays
|
|
236
|
+
# @return [Boolean] True if all required combinations are present
|
|
237
|
+
#
|
|
238
|
+
# @example Check for Windows English name
|
|
239
|
+
# name.has_valid_platform_combos?([3, 1, 0x0409])
|
|
240
|
+
def has_valid_platform_combos?(*combos)
|
|
241
|
+
combos.all? do |platform_id, encoding_id, language_id|
|
|
242
|
+
name_records.any? do |rec|
|
|
243
|
+
rec.platform_id == platform_id &&
|
|
244
|
+
rec.encoding_id == encoding_id &&
|
|
245
|
+
rec.language_id == language_id
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Validation helper: Check if family name is present and non-empty
|
|
251
|
+
#
|
|
252
|
+
# @return [Boolean] True if family name exists and is not empty
|
|
253
|
+
def family_name_present?
|
|
254
|
+
name = english_name(FAMILY)
|
|
255
|
+
!name.nil? && !name.empty?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Validation helper: Check if PostScript name is present and non-empty
|
|
259
|
+
#
|
|
260
|
+
# @return [Boolean] True if PostScript name exists and is not empty
|
|
261
|
+
def postscript_name_present?
|
|
262
|
+
name = english_name(POSTSCRIPT_NAME)
|
|
263
|
+
!name.nil? && !name.empty?
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Validation helper: Check if PostScript name is valid
|
|
267
|
+
#
|
|
268
|
+
# PostScript names must contain only ASCII alphanumerics and hyphens
|
|
269
|
+
#
|
|
270
|
+
# @return [Boolean] True if PostScript name matches the required pattern
|
|
271
|
+
def postscript_name_valid?
|
|
272
|
+
name = english_name(POSTSCRIPT_NAME)
|
|
273
|
+
return false if name.nil? || name.empty?
|
|
274
|
+
|
|
275
|
+
name.match?(/^[A-Za-z0-9-]+$/)
|
|
276
|
+
end
|
|
277
|
+
|
|
202
278
|
private
|
|
203
279
|
|
|
204
280
|
# Find a name record matching the criteria
|
data/lib/fontisan/tables/os2.rb
CHANGED
|
@@ -170,6 +170,119 @@ module Fontisan
|
|
|
170
170
|
|
|
171
171
|
us_upper_optical_point_size / 20.0
|
|
172
172
|
end
|
|
173
|
+
|
|
174
|
+
# Validation helper: Check if version is valid
|
|
175
|
+
#
|
|
176
|
+
# Valid versions are 0 through 5
|
|
177
|
+
#
|
|
178
|
+
# @return [Boolean] True if version is 0-5
|
|
179
|
+
def valid_version?
|
|
180
|
+
version && version.between?(0, 5)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Validation helper: Check if weight class is valid
|
|
184
|
+
#
|
|
185
|
+
# Valid values are 1-1000, common values are multiples of 100
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] True if weight class is valid
|
|
188
|
+
def valid_weight_class?
|
|
189
|
+
us_weight_class && us_weight_class.between?(1, 1000)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Validation helper: Check if width class is valid
|
|
193
|
+
#
|
|
194
|
+
# Valid values are 1-9
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean] True if width class is 1-9
|
|
197
|
+
def valid_width_class?
|
|
198
|
+
us_width_class && us_width_class.between?(1, 9)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Validation helper: Check if vendor ID is present
|
|
202
|
+
#
|
|
203
|
+
# Vendor ID should be a 4-character code
|
|
204
|
+
#
|
|
205
|
+
# @return [Boolean] True if vendor ID exists and is non-empty
|
|
206
|
+
def has_vendor_id?
|
|
207
|
+
!vendor_id.empty?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Validation helper: Check if typo metrics are reasonable
|
|
211
|
+
#
|
|
212
|
+
# Ascent should be positive, descender negative, line gap non-negative
|
|
213
|
+
#
|
|
214
|
+
# @return [Boolean] True if typo metrics have correct signs
|
|
215
|
+
def valid_typo_metrics?
|
|
216
|
+
s_typo_ascender > 0 && s_typo_descender < 0 && s_typo_line_gap >= 0
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Validation helper: Check if Win metrics are valid
|
|
220
|
+
#
|
|
221
|
+
# Both should be positive (unsigned in spec)
|
|
222
|
+
#
|
|
223
|
+
# @return [Boolean] True if Win ascent and descent are positive
|
|
224
|
+
def valid_win_metrics?
|
|
225
|
+
us_win_ascent > 0 && us_win_descent > 0
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Validation helper: Check if Unicode ranges are set
|
|
229
|
+
#
|
|
230
|
+
# At least one Unicode range bit should be set
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] True if any Unicode range bits are set
|
|
233
|
+
def has_unicode_ranges?
|
|
234
|
+
(ul_unicode_range1 | ul_unicode_range2 | ul_unicode_range3 | ul_unicode_range4) != 0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validation helper: Check if PANOSE data is present
|
|
238
|
+
#
|
|
239
|
+
# All PANOSE values should not be zero
|
|
240
|
+
#
|
|
241
|
+
# @return [Boolean] True if PANOSE seems to be set
|
|
242
|
+
def has_panose?
|
|
243
|
+
panose && panose.any? { |val| val != 0 }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Validation helper: Check if embedding permissions are set
|
|
247
|
+
#
|
|
248
|
+
# fs_type indicates embedding and subsetting permissions
|
|
249
|
+
#
|
|
250
|
+
# @return [Boolean] True if embedding permissions are defined
|
|
251
|
+
def has_embedding_permissions?
|
|
252
|
+
!fs_type.nil?
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Validation helper: Check if selection flags are valid
|
|
256
|
+
#
|
|
257
|
+
# Checks for valid combinations of selection flags
|
|
258
|
+
#
|
|
259
|
+
# @return [Boolean] True if fs_selection has valid flags
|
|
260
|
+
def valid_selection_flags?
|
|
261
|
+
return false if fs_selection.nil?
|
|
262
|
+
|
|
263
|
+
# Bits 0-9 are defined, others should be zero
|
|
264
|
+
(fs_selection & 0xFC00).zero?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Validation helper: Check if x_height and cap_height are present (v2+)
|
|
268
|
+
#
|
|
269
|
+
# For version 2+, these should be set
|
|
270
|
+
#
|
|
271
|
+
# @return [Boolean] True if metrics are present (or not required)
|
|
272
|
+
def has_x_height_cap_height?
|
|
273
|
+
return true if version < 2 # Not required for v0-1
|
|
274
|
+
|
|
275
|
+
!sx_height.nil? && !s_cap_height.nil? && sx_height > 0 && s_cap_height > 0
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Validation helper: Check if first/last char indices are reasonable
|
|
279
|
+
#
|
|
280
|
+
# first should be <= last
|
|
281
|
+
#
|
|
282
|
+
# @return [Boolean] True if character range is valid
|
|
283
|
+
def valid_char_range?
|
|
284
|
+
us_first_char_index <= us_last_char_index
|
|
285
|
+
end
|
|
173
286
|
end
|
|
174
287
|
end
|
|
175
288
|
end
|
data/lib/fontisan/tables/post.rb
CHANGED
|
@@ -143,6 +143,63 @@ module Fontisan
|
|
|
143
143
|
end
|
|
144
144
|
end
|
|
145
145
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
146
|
+
|
|
147
|
+
public
|
|
148
|
+
|
|
149
|
+
# Validation helper: Check if version is valid
|
|
150
|
+
#
|
|
151
|
+
# Common versions: 1.0, 2.0, 2.5, 3.0, 4.0
|
|
152
|
+
#
|
|
153
|
+
# @return [Boolean] True if version is recognized
|
|
154
|
+
def valid_version?
|
|
155
|
+
[1.0, 2.0, 2.5, 3.0, 4.0].include?(version)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Validation helper: Check if italic angle is reasonable
|
|
159
|
+
#
|
|
160
|
+
# Italic angle should be between -60 and 60 degrees
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean] True if italic angle is within reasonable bounds
|
|
163
|
+
def valid_italic_angle?
|
|
164
|
+
italic_angle.abs <= 60.0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validation helper: Check if underline values are present
|
|
168
|
+
#
|
|
169
|
+
# Both position and thickness should be non-zero for valid underline
|
|
170
|
+
#
|
|
171
|
+
# @return [Boolean] True if underline metrics exist
|
|
172
|
+
def has_underline_metrics?
|
|
173
|
+
underline_position != 0 && underline_thickness != 0
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Validation helper: Check if fixed pitch flag is consistent
|
|
177
|
+
#
|
|
178
|
+
# @return [Boolean] True if is_fixed_pitch is 0 or 1
|
|
179
|
+
def valid_fixed_pitch_flag?
|
|
180
|
+
is_fixed_pitch == 0 || is_fixed_pitch == 1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Validation helper: Check if glyph names are available
|
|
184
|
+
#
|
|
185
|
+
# For versions 1.0 and 2.0, glyph names should be accessible
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] True if glyph names can be retrieved
|
|
188
|
+
def has_glyph_names?
|
|
189
|
+
names = glyph_names
|
|
190
|
+
!names.nil? && !names.empty?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Validation helper: Check if version 2.0 data is complete
|
|
194
|
+
#
|
|
195
|
+
# For version 2.0, we should have glyph count and name data
|
|
196
|
+
#
|
|
197
|
+
# @return [Boolean] True if version 2.0 data is present and complete
|
|
198
|
+
def complete_version_2_data?
|
|
199
|
+
return true unless version == 2.0
|
|
200
|
+
|
|
201
|
+
!num_glyphs_v2.nil? && num_glyphs_v2 > 0 && !remaining_data.empty?
|
|
202
|
+
end
|
|
146
203
|
end
|
|
147
204
|
end
|
|
148
205
|
end
|
|
@@ -69,12 +69,6 @@ module Fontisan
|
|
|
69
69
|
# Whether lazy loading is enabled
|
|
70
70
|
attr_accessor :lazy_load_enabled
|
|
71
71
|
|
|
72
|
-
# Page cache for lazy loading (maps page_start_offset => page_data)
|
|
73
|
-
attr_accessor :page_cache
|
|
74
|
-
|
|
75
|
-
# Page size for lazy loading alignment (typical filesystem page size)
|
|
76
|
-
PAGE_SIZE = 4096
|
|
77
|
-
|
|
78
72
|
# Read TrueType Font from a file
|
|
79
73
|
#
|
|
80
74
|
# @param path [String] Path to the TTF file
|
|
@@ -101,8 +95,9 @@ module Fontisan
|
|
|
101
95
|
font.lazy_load_enabled = lazy
|
|
102
96
|
|
|
103
97
|
if lazy
|
|
104
|
-
#
|
|
105
|
-
|
|
98
|
+
# Reuse existing IO handle by duplicating it to prevent double file open
|
|
99
|
+
# The dup ensures the handle stays open after this block closes
|
|
100
|
+
font.io_source = io.dup
|
|
106
101
|
font.setup_finalizer
|
|
107
102
|
else
|
|
108
103
|
# Read tables upfront
|
|
@@ -141,7 +136,6 @@ module Fontisan
|
|
|
141
136
|
@loading_mode = LoadingModes::FULL
|
|
142
137
|
@lazy_load_enabled = false
|
|
143
138
|
@io_source = nil
|
|
144
|
-
@page_cache = {}
|
|
145
139
|
end
|
|
146
140
|
|
|
147
141
|
# Read table data for all tables
|
|
@@ -450,8 +444,8 @@ module Fontisan
|
|
|
450
444
|
|
|
451
445
|
# Load a single table's data on demand
|
|
452
446
|
#
|
|
453
|
-
# Uses
|
|
454
|
-
# performance is
|
|
447
|
+
# Uses direct seek-and-read for minimal overhead. This ensures lazy loading
|
|
448
|
+
# performance is comparable to eager loading when accessing all tables.
|
|
455
449
|
#
|
|
456
450
|
# @param tag [String] The table tag to load
|
|
457
451
|
# @return [void]
|
|
@@ -461,42 +455,10 @@ module Fontisan
|
|
|
461
455
|
entry = find_table_entry(tag)
|
|
462
456
|
return nil unless entry
|
|
463
457
|
|
|
464
|
-
#
|
|
465
|
-
|
|
466
|
-
table_end = entry.offset + entry.table_length
|
|
467
|
-
|
|
468
|
-
# Calculate page boundaries
|
|
469
|
-
page_start = (table_start / PAGE_SIZE) * PAGE_SIZE
|
|
470
|
-
page_end = ((table_end + PAGE_SIZE - 1) / PAGE_SIZE) * PAGE_SIZE
|
|
471
|
-
|
|
472
|
-
# Read all required pages (or use cached pages)
|
|
473
|
-
table_data_parts = []
|
|
474
|
-
current_page = page_start
|
|
475
|
-
|
|
476
|
-
while current_page < page_end
|
|
477
|
-
page_data = @page_cache[current_page]
|
|
478
|
-
|
|
479
|
-
unless page_data
|
|
480
|
-
# Read page from disk and cache it
|
|
481
|
-
@io_source.seek(current_page)
|
|
482
|
-
page_data = @io_source.read(PAGE_SIZE) || ""
|
|
483
|
-
@page_cache[current_page] = page_data
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Calculate which part of this page we need
|
|
487
|
-
chunk_start = [table_start - current_page, 0].max
|
|
488
|
-
chunk_end = [table_end - current_page, PAGE_SIZE].min
|
|
489
|
-
|
|
490
|
-
if chunk_end > chunk_start
|
|
491
|
-
table_data_parts << page_data[chunk_start...chunk_end]
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
current_page += PAGE_SIZE
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
# Combine parts and store
|
|
458
|
+
# Direct seek and read - same as eager loading but on-demand
|
|
459
|
+
@io_source.seek(entry.offset)
|
|
498
460
|
tag_key = tag.dup.force_encoding("UTF-8")
|
|
499
|
-
@table_data[tag_key] =
|
|
461
|
+
@table_data[tag_key] = @io_source.read(entry.table_length)
|
|
500
462
|
end
|
|
501
463
|
|
|
502
464
|
# Parse a table from raw data (Fontisan extension)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validation
|
|
7
|
+
# CollectionValidator validates font compatibility for collection formats
|
|
8
|
+
#
|
|
9
|
+
# Main responsibility: Enforce format-specific compatibility rules for
|
|
10
|
+
# TTC, OTC, and dfont collections according to OpenType spec and Apple standards.
|
|
11
|
+
#
|
|
12
|
+
# Rules:
|
|
13
|
+
# - TTC: TrueType fonts ONLY (per OpenType spec)
|
|
14
|
+
# - OTC: CFF fonts required, mixed TTF+OTF allowed (Fontisan extension)
|
|
15
|
+
# - dfont: Any SFNT fonts (TTF, OTF, or mixed)
|
|
16
|
+
# - All: Web fonts (WOFF/WOFF2) are NEVER allowed in collections
|
|
17
|
+
#
|
|
18
|
+
# @example Validate TTC compatibility
|
|
19
|
+
# validator = CollectionValidator.new
|
|
20
|
+
# validator.validate!([font1, font2], :ttc)
|
|
21
|
+
#
|
|
22
|
+
# @example Check compatibility without raising
|
|
23
|
+
# validator = CollectionValidator.new
|
|
24
|
+
# result = validator.compatible?([font1, font2], :otc)
|
|
25
|
+
class CollectionValidator
|
|
26
|
+
# Validate fonts are compatible with collection format
|
|
27
|
+
#
|
|
28
|
+
# @param fonts [Array<TrueTypeFont, OpenTypeFont>] Fonts to validate
|
|
29
|
+
# @param format [Symbol] Collection format (:ttc, :otc, or :dfont)
|
|
30
|
+
# @return [Boolean] true if valid
|
|
31
|
+
# @raise [Error] if validation fails
|
|
32
|
+
def validate!(fonts, format)
|
|
33
|
+
validate_not_empty!(fonts)
|
|
34
|
+
validate_format!(format)
|
|
35
|
+
|
|
36
|
+
case format
|
|
37
|
+
when :ttc
|
|
38
|
+
validate_ttc!(fonts)
|
|
39
|
+
when :otc
|
|
40
|
+
validate_otc!(fonts)
|
|
41
|
+
when :dfont
|
|
42
|
+
validate_dfont!(fonts)
|
|
43
|
+
else
|
|
44
|
+
raise Error, "Unknown collection format: #{format}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if fonts are compatible with format (without raising)
|
|
51
|
+
#
|
|
52
|
+
# @param fonts [Array] Fonts to check
|
|
53
|
+
# @param format [Symbol] Collection format
|
|
54
|
+
# @return [Boolean] true if compatible
|
|
55
|
+
def compatible?(fonts, format)
|
|
56
|
+
validate!(fonts, format)
|
|
57
|
+
true
|
|
58
|
+
rescue Error
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get compatibility issues for fonts and format
|
|
63
|
+
#
|
|
64
|
+
# @param fonts [Array] Fonts to check
|
|
65
|
+
# @param format [Symbol] Collection format
|
|
66
|
+
# @return [Array<String>] Array of issue descriptions (empty if compatible)
|
|
67
|
+
def compatibility_issues(fonts, format)
|
|
68
|
+
issues = []
|
|
69
|
+
|
|
70
|
+
return ["Font array cannot be empty"] if fonts.nil? || fonts.empty?
|
|
71
|
+
return ["Invalid format: #{format}"] unless %i[ttc otc dfont].include?(format)
|
|
72
|
+
|
|
73
|
+
case format
|
|
74
|
+
when :ttc
|
|
75
|
+
issues.concat(ttc_issues(fonts))
|
|
76
|
+
when :otc
|
|
77
|
+
issues.concat(otc_issues(fonts))
|
|
78
|
+
when :dfont
|
|
79
|
+
issues.concat(dfont_issues(fonts))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
issues
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Validate fonts array is not empty
|
|
88
|
+
#
|
|
89
|
+
# @param fonts [Array] Fonts
|
|
90
|
+
# @raise [ArgumentError] if empty or nil
|
|
91
|
+
def validate_not_empty!(fonts)
|
|
92
|
+
if fonts.nil? || fonts.empty?
|
|
93
|
+
raise ArgumentError, "Font array cannot be empty"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validate format is supported
|
|
98
|
+
#
|
|
99
|
+
# @param format [Symbol] Format
|
|
100
|
+
# @raise [ArgumentError] if invalid
|
|
101
|
+
def validate_format!(format)
|
|
102
|
+
unless %i[ttc otc dfont].include?(format)
|
|
103
|
+
raise ArgumentError, "Invalid format: #{format}. Must be :ttc, :otc, or :dfont"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validate TTC compatibility
|
|
108
|
+
#
|
|
109
|
+
# Per OpenType spec: TTC = TrueType outlines ONLY
|
|
110
|
+
# "CFF rasterizer does not currently support TTC files"
|
|
111
|
+
#
|
|
112
|
+
# @param fonts [Array] Fonts
|
|
113
|
+
# @raise [Error] if incompatible
|
|
114
|
+
def validate_ttc!(fonts)
|
|
115
|
+
fonts.each_with_index do |font, index|
|
|
116
|
+
# Check for web fonts
|
|
117
|
+
if web_font?(font)
|
|
118
|
+
raise Error,
|
|
119
|
+
"Font #{index} is a web font (WOFF/WOFF2). " \
|
|
120
|
+
"Web fonts cannot be packed into collections."
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check for TrueType outline format
|
|
124
|
+
unless truetype_font?(font)
|
|
125
|
+
raise Error,
|
|
126
|
+
"Font #{index} is not TrueType. " \
|
|
127
|
+
"TTC requires TrueType fonts only (per OpenType spec)."
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Validate OTC compatibility
|
|
133
|
+
#
|
|
134
|
+
# Per OpenType 1.8: OTC for CFF collections
|
|
135
|
+
# Fontisan extension: Also allows mixed TTF+OTF for flexibility
|
|
136
|
+
#
|
|
137
|
+
# @param fonts [Array] Fonts
|
|
138
|
+
# @raise [Error] if incompatible
|
|
139
|
+
def validate_otc!(fonts)
|
|
140
|
+
has_cff = false
|
|
141
|
+
|
|
142
|
+
fonts.each_with_index do |font, index|
|
|
143
|
+
# Check for web fonts
|
|
144
|
+
if web_font?(font)
|
|
145
|
+
raise Error,
|
|
146
|
+
"Font #{index} is a web font (WOFF/WOFF2). " \
|
|
147
|
+
"Web fonts cannot be packed into collections."
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Track if any font has CFF
|
|
151
|
+
has_cff = true if cff_font?(font)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# OTC should have at least one CFF font
|
|
155
|
+
unless has_cff
|
|
156
|
+
raise Error,
|
|
157
|
+
"OTC requires at least one CFF/OpenType font. " \
|
|
158
|
+
"All fonts are TrueType - use TTC instead."
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Validate dfont compatibility
|
|
163
|
+
#
|
|
164
|
+
# Apple dfont suitcase: Any SFNT fonts OK (TTF, OTF, or mixed)
|
|
165
|
+
# dfont stores complete Mac resources (FOND, NFNT, sfnt)
|
|
166
|
+
#
|
|
167
|
+
# @param fonts [Array] Fonts
|
|
168
|
+
# @raise [Error] if incompatible
|
|
169
|
+
def validate_dfont!(fonts)
|
|
170
|
+
fonts.each_with_index do |font, index|
|
|
171
|
+
# Only check for web fonts - dfont accepts any SFNT
|
|
172
|
+
if web_font?(font)
|
|
173
|
+
raise Error,
|
|
174
|
+
"Font #{index} is a web font (WOFF/WOFF2). " \
|
|
175
|
+
"Web fonts cannot be packed into dfont."
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Get TTC compatibility issues
|
|
181
|
+
#
|
|
182
|
+
# @param fonts [Array] Fonts
|
|
183
|
+
# @return [Array<String>] Issues
|
|
184
|
+
def ttc_issues(fonts)
|
|
185
|
+
issues = []
|
|
186
|
+
|
|
187
|
+
fonts.each_with_index do |font, index|
|
|
188
|
+
if web_font?(font)
|
|
189
|
+
issues << "Font #{index} is WOFF/WOFF2 (not allowed in collections)"
|
|
190
|
+
elsif !truetype_font?(font)
|
|
191
|
+
issues << "Font #{index} is not TrueType (TTC requires TrueType only)"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
issues
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get OTC compatibility issues
|
|
199
|
+
#
|
|
200
|
+
# @param fonts [Array] Fonts
|
|
201
|
+
# @return [Array<String>] Issues
|
|
202
|
+
def otc_issues(fonts)
|
|
203
|
+
issues = []
|
|
204
|
+
has_cff = false
|
|
205
|
+
|
|
206
|
+
fonts.each_with_index do |font, index|
|
|
207
|
+
if web_font?(font)
|
|
208
|
+
issues << "Font #{index} is WOFF/WOFF2 (not allowed in collections)"
|
|
209
|
+
end
|
|
210
|
+
has_cff = true if cff_font?(font)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
unless has_cff
|
|
214
|
+
issues << "OTC requires at least one CFF font (all fonts are TrueType)"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
issues
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get dfont compatibility issues
|
|
221
|
+
#
|
|
222
|
+
# @param fonts [Array] Fonts
|
|
223
|
+
# @return [Array<String>] Issues
|
|
224
|
+
def dfont_issues(fonts)
|
|
225
|
+
issues = []
|
|
226
|
+
|
|
227
|
+
fonts.each_with_index do |font, index|
|
|
228
|
+
if web_font?(font)
|
|
229
|
+
issues << "Font #{index} is WOFF/WOFF2 (not allowed in dfont)"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
issues
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Check if font is a web font
|
|
237
|
+
#
|
|
238
|
+
# @param font [Object] Font object
|
|
239
|
+
# @return [Boolean] true if WOFF or WOFF2
|
|
240
|
+
def web_font?(font)
|
|
241
|
+
font.class.name.include?("Woff")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Check if font is TrueType
|
|
245
|
+
#
|
|
246
|
+
# @param font [Object] Font object
|
|
247
|
+
# @return [Boolean] true if TrueType
|
|
248
|
+
def truetype_font?(font)
|
|
249
|
+
return false unless font.respond_to?(:has_table?)
|
|
250
|
+
|
|
251
|
+
font.has_table?("glyf")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Check if font is CFF/OpenType
|
|
255
|
+
#
|
|
256
|
+
# @param font [Object] Font object
|
|
257
|
+
# @return [Boolean] true if CFF
|
|
258
|
+
def cff_font?(font)
|
|
259
|
+
return false unless font.respond_to?(:has_table?)
|
|
260
|
+
|
|
261
|
+
font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "validator"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validators
|
|
7
|
+
# BasicValidator provides minimal validation for fast font indexing
|
|
8
|
+
#
|
|
9
|
+
# This validator implements only essential checks needed for font discovery
|
|
10
|
+
# and indexing systems (e.g., Fontist). It is optimized for speed with a
|
|
11
|
+
# target performance of < 50ms per font.
|
|
12
|
+
#
|
|
13
|
+
# The validator checks only critical font identification and structural
|
|
14
|
+
# integrity, making it suitable for:
|
|
15
|
+
# - Font discovery and indexing
|
|
16
|
+
# - Quick font database updates
|
|
17
|
+
# - Large-scale font scanning
|
|
18
|
+
#
|
|
19
|
+
# @example Using BasicValidator
|
|
20
|
+
# validator = BasicValidator.new
|
|
21
|
+
# report = validator.validate(font)
|
|
22
|
+
# puts "Font is valid for indexing" if report.valid?
|
|
23
|
+
#
|
|
24
|
+
# @example Target performance
|
|
25
|
+
# # Should complete in < 50ms
|
|
26
|
+
# start_time = Time.now
|
|
27
|
+
# report = BasicValidator.new.validate(font)
|
|
28
|
+
# elapsed = Time.now - start_time
|
|
29
|
+
# puts "Validated in #{elapsed * 1000}ms"
|
|
30
|
+
class BasicValidator < Validator
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Define essential validation checks
|
|
34
|
+
#
|
|
35
|
+
# This validator implements 8 checks covering:
|
|
36
|
+
# - Required tables presence
|
|
37
|
+
# - Name table identification
|
|
38
|
+
# - Head table integrity
|
|
39
|
+
# - Maxp table glyph count
|
|
40
|
+
#
|
|
41
|
+
# All checks use helpers from Week 1 table implementations.
|
|
42
|
+
def define_checks
|
|
43
|
+
# Check 1: Required tables must be present
|
|
44
|
+
check_structure :required_tables, severity: :error do |font|
|
|
45
|
+
%w[name head maxp hhea].all? { |tag| font.table(tag) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check 2: Name table version must be valid (0 or 1)
|
|
49
|
+
check_table :name_version, "name", severity: :error do |table|
|
|
50
|
+
table.valid_version?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check 3: Family name must be present and non-empty
|
|
54
|
+
check_table :family_name, "name", severity: :error do |table|
|
|
55
|
+
table.family_name_present?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check 4: PostScript name must be valid (alphanumeric + hyphens)
|
|
59
|
+
check_table :postscript_name, "name", severity: :error do |table|
|
|
60
|
+
table.postscript_name_valid?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check 5: Head table magic number must be correct
|
|
64
|
+
check_table :head_magic, "head", severity: :error do |table|
|
|
65
|
+
table.valid_magic?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check 6: Units per em must be valid (16-16384)
|
|
69
|
+
check_table :units_per_em, "head", severity: :error do |table|
|
|
70
|
+
table.valid_units_per_em?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check 7: Number of glyphs must be at least 1 (.notdef)
|
|
74
|
+
check_table :num_glyphs, "maxp", severity: :error do |table|
|
|
75
|
+
table.valid_num_glyphs?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check 8: Maxp metrics should be reasonable (not absurd values)
|
|
79
|
+
check_table :reasonable_metrics, "maxp", severity: :warning do |table|
|
|
80
|
+
table.reasonable_metrics?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|