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
@@ -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
- require_relative "../validation/validator"
266
+ # Use new validation framework with production profile
267
+ report = Fontisan.validate(@output_path, profile: :production)
267
268
 
268
- # Load font for validation
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
- return if result.valid
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
 
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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