fontisan 0.2.3 → 0.2.5

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +221 -49
  3. data/README.adoc +519 -5
  4. data/Rakefile +20 -7
  5. data/lib/fontisan/cli.rb +67 -6
  6. data/lib/fontisan/commands/base_command.rb +2 -19
  7. data/lib/fontisan/commands/convert_command.rb +16 -13
  8. data/lib/fontisan/commands/info_command.rb +88 -0
  9. data/lib/fontisan/commands/validate_command.rb +107 -151
  10. data/lib/fontisan/config/conversion_matrix.yml +58 -20
  11. data/lib/fontisan/converters/outline_converter.rb +6 -3
  12. data/lib/fontisan/converters/svg_generator.rb +45 -0
  13. data/lib/fontisan/converters/woff2_encoder.rb +84 -13
  14. data/lib/fontisan/models/bitmap_glyph.rb +123 -0
  15. data/lib/fontisan/models/bitmap_strike.rb +94 -0
  16. data/lib/fontisan/models/color_glyph.rb +57 -0
  17. data/lib/fontisan/models/color_layer.rb +53 -0
  18. data/lib/fontisan/models/color_palette.rb +60 -0
  19. data/lib/fontisan/models/font_info.rb +26 -0
  20. data/lib/fontisan/models/svg_glyph.rb +89 -0
  21. data/lib/fontisan/models/validation_report.rb +227 -0
  22. data/lib/fontisan/open_type_font.rb +6 -0
  23. data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
  24. data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
  25. data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
  26. data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
  27. data/lib/fontisan/pipeline/output_writer.rb +2 -2
  28. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  29. data/lib/fontisan/tables/cbdt.rb +169 -0
  30. data/lib/fontisan/tables/cblc.rb +290 -0
  31. data/lib/fontisan/tables/cff.rb +6 -12
  32. data/lib/fontisan/tables/cmap.rb +82 -2
  33. data/lib/fontisan/tables/colr.rb +291 -0
  34. data/lib/fontisan/tables/cpal.rb +281 -0
  35. data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
  36. data/lib/fontisan/tables/glyf.rb +118 -0
  37. data/lib/fontisan/tables/head.rb +60 -0
  38. data/lib/fontisan/tables/hhea.rb +74 -0
  39. data/lib/fontisan/tables/maxp.rb +60 -0
  40. data/lib/fontisan/tables/name.rb +76 -0
  41. data/lib/fontisan/tables/os2.rb +113 -0
  42. data/lib/fontisan/tables/post.rb +57 -0
  43. data/lib/fontisan/tables/sbix.rb +379 -0
  44. data/lib/fontisan/tables/svg.rb +301 -0
  45. data/lib/fontisan/true_type_font.rb +6 -0
  46. data/lib/fontisan/validators/basic_validator.rb +85 -0
  47. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  48. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  49. data/lib/fontisan/validators/profile_loader.rb +139 -0
  50. data/lib/fontisan/validators/validator.rb +484 -0
  51. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  52. data/lib/fontisan/version.rb +1 -1
  53. data/lib/fontisan/woff2/directory.rb +40 -11
  54. data/lib/fontisan/woff2/table_transformer.rb +506 -73
  55. data/lib/fontisan/woff2_font.rb +29 -9
  56. data/lib/fontisan/woff_font.rb +17 -4
  57. data/lib/fontisan.rb +90 -6
  58. metadata +20 -9
  59. data/lib/fontisan/config/validation_rules.yml +0 -149
  60. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  61. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  62. data/lib/fontisan/validation/structure_validator.rb +0 -198
  63. data/lib/fontisan/validation/table_validator.rb +0 -158
  64. data/lib/fontisan/validation/validator.rb +0 -152
  65. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
@@ -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
@@ -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