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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +168 -32
  3. data/README.adoc +673 -1091
  4. data/lib/fontisan/cli.rb +94 -13
  5. data/lib/fontisan/collection/dfont_builder.rb +315 -0
  6. data/lib/fontisan/commands/convert_command.rb +118 -7
  7. data/lib/fontisan/commands/pack_command.rb +129 -22
  8. data/lib/fontisan/commands/validate_command.rb +107 -151
  9. data/lib/fontisan/config/conversion_matrix.yml +175 -1
  10. data/lib/fontisan/constants.rb +8 -0
  11. data/lib/fontisan/converters/collection_converter.rb +438 -0
  12. data/lib/fontisan/converters/woff2_encoder.rb +7 -29
  13. data/lib/fontisan/dfont_collection.rb +185 -0
  14. data/lib/fontisan/font_loader.rb +91 -6
  15. data/lib/fontisan/models/validation_report.rb +227 -0
  16. data/lib/fontisan/parsers/dfont_parser.rb +192 -0
  17. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  18. data/lib/fontisan/tables/cmap.rb +82 -2
  19. data/lib/fontisan/tables/glyf.rb +118 -0
  20. data/lib/fontisan/tables/head.rb +60 -0
  21. data/lib/fontisan/tables/hhea.rb +74 -0
  22. data/lib/fontisan/tables/maxp.rb +60 -0
  23. data/lib/fontisan/tables/name.rb +76 -0
  24. data/lib/fontisan/tables/os2.rb +113 -0
  25. data/lib/fontisan/tables/post.rb +57 -0
  26. data/lib/fontisan/true_type_font.rb +8 -46
  27. data/lib/fontisan/validation/collection_validator.rb +265 -0
  28. data/lib/fontisan/validators/basic_validator.rb +85 -0
  29. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  30. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  31. data/lib/fontisan/validators/profile_loader.rb +139 -0
  32. data/lib/fontisan/validators/validator.rb +484 -0
  33. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  34. data/lib/fontisan/version.rb +1 -1
  35. data/lib/fontisan.rb +78 -6
  36. metadata +13 -12
  37. data/lib/fontisan/config/validation_rules.yml +0 -149
  38. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  39. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  40. data/lib/fontisan/validation/structure_validator.rb +0 -198
  41. data/lib/fontisan/validation/table_validator.rb +0 -158
  42. data/lib/fontisan/validation/validator.rb +0 -152
  43. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
  44. data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
  45. data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
  46. data/lib/fontisan/validation/woff2_validator.rb +0 -248
@@ -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
@@ -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
@@ -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
- # Keep file handle open for lazy loading
105
- font.io_source = File.open(path, "rb")
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 page-aligned reads and caches pages to ensure lazy loading
454
- # performance is not slower than eager loading.
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
- # Use page-aligned reading with caching
465
- table_start = entry.offset
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] = table_data_parts.join
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