fontisan 0.2.7 → 0.2.9

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +65 -361
  4. data/CHANGELOG.md +116 -0
  5. data/Gemfile +1 -1
  6. data/README.adoc +106 -27
  7. data/Rakefile +12 -7
  8. data/benchmark/variation_quick_bench.rb +1 -1
  9. data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
  10. data/docs/COLLECTION_VALIDATION.adoc +143 -0
  11. data/docs/COLOR_FONTS.adoc +127 -0
  12. data/docs/DOCUMENTATION_SUMMARY.md +141 -0
  13. data/docs/FONT_HINTING.adoc +9 -1
  14. data/docs/VALIDATION.adoc +254 -0
  15. data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
  16. data/lib/fontisan/cli.rb +45 -13
  17. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  18. data/lib/fontisan/commands/convert_command.rb +2 -4
  19. data/lib/fontisan/commands/info_command.rb +3 -3
  20. data/lib/fontisan/commands/pack_command.rb +2 -1
  21. data/lib/fontisan/commands/validate_command.rb +157 -6
  22. data/lib/fontisan/converters/collection_converter.rb +22 -13
  23. data/lib/fontisan/converters/svg_generator.rb +2 -1
  24. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  25. data/lib/fontisan/converters/woff_writer.rb +3 -1
  26. data/lib/fontisan/font_loader.rb +7 -6
  27. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  28. data/lib/fontisan/hints/hint_converter.rb +1 -1
  29. data/lib/fontisan/hints/hint_validator.rb +13 -10
  30. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  31. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  32. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  33. data/lib/fontisan/models/font_report.rb +24 -0
  34. data/lib/fontisan/models/validation_report.rb +7 -2
  35. data/lib/fontisan/open_type_font.rb +18 -425
  36. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  37. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  38. data/lib/fontisan/sfnt_font.rb +699 -0
  39. data/lib/fontisan/sfnt_table.rb +264 -0
  40. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  41. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  42. data/lib/fontisan/tables/cblc.rb +8 -4
  43. data/lib/fontisan/tables/cff/index.rb +2 -0
  44. data/lib/fontisan/tables/cff.rb +6 -3
  45. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  46. data/lib/fontisan/tables/cff2.rb +1 -1
  47. data/lib/fontisan/tables/cmap.rb +5 -5
  48. data/lib/fontisan/tables/cmap_table.rb +231 -0
  49. data/lib/fontisan/tables/glyf.rb +8 -10
  50. data/lib/fontisan/tables/glyf_table.rb +255 -0
  51. data/lib/fontisan/tables/head.rb +3 -3
  52. data/lib/fontisan/tables/head_table.rb +111 -0
  53. data/lib/fontisan/tables/hhea.rb +4 -4
  54. data/lib/fontisan/tables/hhea_table.rb +255 -0
  55. data/lib/fontisan/tables/hmtx_table.rb +191 -0
  56. data/lib/fontisan/tables/loca_table.rb +212 -0
  57. data/lib/fontisan/tables/maxp.rb +2 -2
  58. data/lib/fontisan/tables/maxp_table.rb +258 -0
  59. data/lib/fontisan/tables/name.rb +1 -1
  60. data/lib/fontisan/tables/name_table.rb +176 -0
  61. data/lib/fontisan/tables/os2.rb +8 -8
  62. data/lib/fontisan/tables/os2_table.rb +329 -0
  63. data/lib/fontisan/tables/post.rb +2 -2
  64. data/lib/fontisan/tables/post_table.rb +183 -0
  65. data/lib/fontisan/tables/sbix.rb +5 -4
  66. data/lib/fontisan/true_type_font.rb +12 -464
  67. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  68. data/lib/fontisan/validation/collection_validator.rb +4 -2
  69. data/lib/fontisan/validators/basic_validator.rb +11 -21
  70. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  71. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  72. data/lib/fontisan/validators/validator.rb +87 -66
  73. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  74. data/lib/fontisan/version.rb +1 -1
  75. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  76. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  77. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  78. data/lib/fontisan/woff2_font.rb +4 -2
  79. data/lib/fontisan/woff_font.rb +46 -30
  80. data/lib/fontisan.rb +2 -2
  81. data/scripts/compare_stack_aware.rb +1 -1
  82. data/scripts/measure_optimization.rb +1 -2
  83. metadata +23 -2
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sfnt_table"
4
+ require_relative "name"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ # OOP representation of the 'name' (Naming) table
9
+ #
10
+ # The name table contains all naming strings for the font, including
11
+ # font family name, style name, designer, license, etc.
12
+ #
13
+ # This class extends SfntTable to provide name-specific convenience
14
+ # methods for accessing common name records.
15
+ #
16
+ # @example Accessing name table data
17
+ # name = font.table("name") # Returns SfntTable instance
18
+ # name.family_name # => "Noto Sans"
19
+ # name.subfamily_name # => "Regular"
20
+ # name.full_name # => "Noto Sans Regular"
21
+ # name.postscript_name # => "NotoSans-Regular"
22
+ class NameTable < SfntTable
23
+ # Name record identifiers
24
+ #
25
+ # These are the name IDs defined in the OpenType spec
26
+ FAMILY = 1
27
+ SUBFAMILY = 2
28
+ FULL_NAME = 4
29
+ POSTSCRIPT_NAME = 6
30
+ PREFERRED_FAMILY = 16
31
+ PREFERRED_SUBFAMILY = 17
32
+ WWS_FAMILY = 21
33
+ WWS_SUBFAMILY = 22
34
+
35
+ # Platform IDs
36
+ PLATFORM_UNICODE = 0
37
+ PLATFORM_MACINTOSH = 1
38
+ PLATFORM_WINDOWS = 3
39
+
40
+ # Get font family name
41
+ #
42
+ # Attempts to get the preferred family name, falling back to the
43
+ # standard family name if preferred is not available.
44
+ #
45
+ # @return [String, nil] Family name or nil if not found
46
+ def family_name
47
+ english_name(PREFERRED_FAMILY) || english_name(FAMILY)
48
+ end
49
+
50
+ # Get font subfamily name
51
+ #
52
+ # Attempts to get the preferred subfamily name, falling back to the
53
+ # standard subfamily name if preferred is not available.
54
+ #
55
+ # @return [String, nil] Subfamily name or nil if not found
56
+ def subfamily_name
57
+ english_name(PREFERRED_SUBFAMILY) || english_name(SUBFAMILY)
58
+ end
59
+
60
+ # Get full font name
61
+ #
62
+ # @return [String, nil] Full name or nil if not found
63
+ def full_name
64
+ english_name(FULL_NAME)
65
+ end
66
+
67
+ # Get PostScript name
68
+ #
69
+ # @return [String, nil] PostScript name or nil if not found
70
+ def postscript_name
71
+ english_name(POSTSCRIPT_NAME)
72
+ end
73
+
74
+ # Get preferred family name
75
+ #
76
+ # @return [String, nil] Preferred family name or nil if not found
77
+ def preferred_family_name
78
+ english_name(PREFERRED_FAMILY)
79
+ end
80
+
81
+ # Get preferred subfamily name
82
+ #
83
+ # @return [String, nil] Preferred subfamily name or nil if not found
84
+ def preferred_subfamily_name
85
+ english_name(PREFERRED_SUBFAMILY)
86
+ end
87
+
88
+ # Get English name for a specific name ID
89
+ #
90
+ # Searches for an English name record with the given name ID.
91
+ # Prefers Windows (platform 3) over Mac (platform 1) over Unicode (platform 0).
92
+ #
93
+ # @param name_id [Integer] The name record ID
94
+ # @return [String, nil] The name string, or nil if not found
95
+ def english_name(name_id)
96
+ return nil unless parsed
97
+
98
+ # Find all name records with this name_id
99
+ records = parsed.name_records.select { |nr| nr.name_id == name_id }
100
+ return nil if records.empty?
101
+
102
+ # Try to find English Windows name first (platform 3, language 0x409)
103
+ windows = records.find do |nr|
104
+ nr.platform_id == PLATFORM_WINDOWS && nr.language_id == 0x409
105
+ end
106
+ return windows.string if windows&.string
107
+
108
+ # Try Mac English (platform 1, language 0)
109
+ mac = records.find do |nr|
110
+ nr.platform_id == PLATFORM_MACINTOSH && nr.language_id.zero?
111
+ end
112
+ return mac.string if mac&.string
113
+
114
+ # Try any English Unicode name (platform 0, language 0)
115
+ unicode = records.find do |nr|
116
+ nr.platform_id == PLATFORM_UNICODE && nr.language_id.zero?
117
+ end
118
+ return unicode.string if unicode&.string
119
+
120
+ # Fallback to first record with this name_id
121
+ first = records.first
122
+ first&.string
123
+ end
124
+
125
+ # Get all name records
126
+ #
127
+ # @return [Array<NameRecord>, nil] Array of name records, or nil if not parsed
128
+ def name_records
129
+ parsed&.name_records
130
+ end
131
+
132
+ # Get all names for a specific name ID
133
+ #
134
+ # @param name_id [Integer] The name record ID
135
+ # @return [Array<Hash>] Array of hashes with platform, encoding, language, and string
136
+ def all_names_for(name_id)
137
+ return [] unless parsed
138
+
139
+ parsed.name_records
140
+ .select { |nr| nr.name_id == name_id }
141
+ .map do |nr|
142
+ {
143
+ platform_id: nr.platform_id,
144
+ encoding_id: nr.encoding_id,
145
+ language_id: nr.language_id,
146
+ string: nr.string,
147
+ }
148
+ end
149
+ end
150
+
151
+ protected
152
+
153
+ # Validate the parsed name table
154
+ #
155
+ # @return [Boolean] true if valid
156
+ # @raise [InvalidFontError] if format identifier is invalid
157
+ def validate_parsed_table?
158
+ return true unless parsed
159
+
160
+ # Validate format selector
161
+ unless [0, 1].include?(parsed.format)
162
+ raise InvalidFontError,
163
+ "Invalid name table format: #{parsed.format} (must be 0 or 1)"
164
+ end
165
+
166
+ # Validate that we have at least some name records
167
+ if parsed.name_records.empty?
168
+ raise InvalidFontError,
169
+ "Name table has no name records"
170
+ end
171
+
172
+ true
173
+ end
174
+ end
175
+ end
176
+ end
@@ -177,7 +177,7 @@ module Fontisan
177
177
  #
178
178
  # @return [Boolean] True if version is 0-5
179
179
  def valid_version?
180
- version && version.between?(0, 5)
180
+ version&.between?(0, 5)
181
181
  end
182
182
 
183
183
  # Validation helper: Check if weight class is valid
@@ -186,7 +186,7 @@ module Fontisan
186
186
  #
187
187
  # @return [Boolean] True if weight class is valid
188
188
  def valid_weight_class?
189
- us_weight_class && us_weight_class.between?(1, 1000)
189
+ us_weight_class&.between?(1, 1000)
190
190
  end
191
191
 
192
192
  # Validation helper: Check if width class is valid
@@ -195,7 +195,7 @@ module Fontisan
195
195
  #
196
196
  # @return [Boolean] True if width class is 1-9
197
197
  def valid_width_class?
198
- us_width_class && us_width_class.between?(1, 9)
198
+ us_width_class&.between?(1, 9)
199
199
  end
200
200
 
201
201
  # Validation helper: Check if vendor ID is present
@@ -213,7 +213,7 @@ module Fontisan
213
213
  #
214
214
  # @return [Boolean] True if typo metrics have correct signs
215
215
  def valid_typo_metrics?
216
- s_typo_ascender > 0 && s_typo_descender < 0 && s_typo_line_gap >= 0
216
+ s_typo_ascender.positive? && s_typo_descender.negative? && s_typo_line_gap >= 0
217
217
  end
218
218
 
219
219
  # Validation helper: Check if Win metrics are valid
@@ -222,7 +222,7 @@ module Fontisan
222
222
  #
223
223
  # @return [Boolean] True if Win ascent and descent are positive
224
224
  def valid_win_metrics?
225
- us_win_ascent > 0 && us_win_descent > 0
225
+ us_win_ascent.positive? && us_win_descent.positive?
226
226
  end
227
227
 
228
228
  # Validation helper: Check if Unicode ranges are set
@@ -240,7 +240,7 @@ module Fontisan
240
240
  #
241
241
  # @return [Boolean] True if PANOSE seems to be set
242
242
  def has_panose?
243
- panose && panose.any? { |val| val != 0 }
243
+ panose&.any? { |val| val != 0 }
244
244
  end
245
245
 
246
246
  # Validation helper: Check if embedding permissions are set
@@ -270,9 +270,9 @@ module Fontisan
270
270
  #
271
271
  # @return [Boolean] True if metrics are present (or not required)
272
272
  def has_x_height_cap_height?
273
- return true if version < 2 # Not required for v0-1
273
+ return true if version < 2 # Not required for v0-1
274
274
 
275
- !sx_height.nil? && !s_cap_height.nil? && sx_height > 0 && s_cap_height > 0
275
+ !sx_height.nil? && !s_cap_height.nil? && sx_height.positive? && s_cap_height.positive?
276
276
  end
277
277
 
278
278
  # Validation helper: Check if first/last char indices are reasonable
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sfnt_table"
4
+ require_relative "os2"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ # OOP representation of the 'OS/2' (OS/2 and Windows Metrics) table
9
+ #
10
+ # The OS/2 table contains OS/2 and Windows-specific metrics required by
11
+ # Windows and OS/2, including font metrics, character ranges, vendor
12
+ # information, and embedding permissions.
13
+ #
14
+ # This class extends SfntTable to provide OS/2-specific validation and
15
+ # convenience methods for accessing common OS/2 table fields.
16
+ #
17
+ # @example Accessing OS/2 table data
18
+ # os2 = font.sfnt_table("OS/2")
19
+ # os2.weight_class # => 400 (Normal)
20
+ # os2.width_class # => 5 (Medium)
21
+ # os2.vendor_id # => "APPL"
22
+ # os2.embedding_allowed? # => true
23
+ class Os2Table < SfntTable
24
+ # Weight class names (from OpenType spec)
25
+ WEIGHT_NAMES = {
26
+ 100 => "Thin",
27
+ 200 => "Extra-light (Ultra-light)",
28
+ 300 => "Light",
29
+ 400 => "Normal (Regular)",
30
+ 500 => "Medium",
31
+ 600 => "Semi-bold (Demi-bold)",
32
+ 700 => "Bold",
33
+ 800 => "Extra-bold (Ultra-bold)",
34
+ 900 => "Black (Heavy)",
35
+ }.freeze
36
+
37
+ # Width class names (from OpenType spec)
38
+ WIDTH_NAMES = {
39
+ 1 => "Ultra-condensed",
40
+ 2 => "Extra-condensed",
41
+ 3 => "Condensed",
42
+ 4 => "Semi-condensed",
43
+ 5 => "Medium (Normal)",
44
+ 6 => "Semi-expanded",
45
+ 7 => "Expanded",
46
+ 8 => "Extra-expanded",
47
+ 9 => "Ultra-expanded",
48
+ }.freeze
49
+
50
+ # Selection flags (bit field)
51
+ FS_ITALIC = 1 << 0
52
+ FS_UNDERSCORE = 1 << 1
53
+ FS_NEGATIVE = 1 << 2
54
+ FS_OUTLINED = 1 << 3
55
+ FS_STRIKEOUT = 1 << 4
56
+ FS_BOLD = 1 << 5
57
+ FS_REGULAR = 1 << 6
58
+ FS_USE_TYPO_METRICS = 1 << 7
59
+ FS_WWS = 1 << 8
60
+ FS_OBLIQUE = 1 << 9
61
+
62
+ # Get OS/2 table version
63
+ #
64
+ # @return [Integer, nil] Version number (0-5), or nil if not parsed
65
+ def version
66
+ parsed&.version
67
+ end
68
+
69
+ # Get weight class
70
+ #
71
+ # @return [Integer, nil] Weight class (100-900), or nil if not parsed
72
+ def weight_class
73
+ parsed&.us_weight_class
74
+ end
75
+
76
+ # Get weight class name
77
+ #
78
+ # @return [String, nil] Human-readable weight name, or nil if not parsed
79
+ def weight_class_name
80
+ return nil unless parsed
81
+
82
+ WEIGHT_NAMES[parsed.us_weight_class] || "Unknown"
83
+ end
84
+
85
+ # Get width class
86
+ #
87
+ # @return [Integer, nil] Width class (1-9), or nil if not parsed
88
+ def width_class
89
+ parsed&.us_width_class
90
+ end
91
+
92
+ # Get width class name
93
+ #
94
+ # @return [String, nil] Human-readable width name, or nil if not parsed
95
+ def width_class_name
96
+ return nil unless parsed
97
+
98
+ WIDTH_NAMES[parsed.us_width_class] || "Unknown"
99
+ end
100
+
101
+ # Get vendor ID
102
+ #
103
+ # @return [String, nil] 4-character vendor identifier, or nil if not parsed
104
+ def vendor_id
105
+ parsed&.vendor_id
106
+ end
107
+
108
+ # Check if font is italic
109
+ #
110
+ # @return [Boolean] true if italic flag is set
111
+ def italic?
112
+ parsed && (parsed.fs_selection & FS_ITALIC) != 0
113
+ end
114
+
115
+ # Check if font is bold
116
+ #
117
+ # @return [Boolean] true if bold flag is set
118
+ def bold?
119
+ parsed && (parsed.fs_selection & FS_BOLD) != 0
120
+ end
121
+
122
+ # Check if font uses regular style
123
+ #
124
+ # @return [Boolean] true if regular flag is set
125
+ def regular?
126
+ parsed && (parsed.fs_selection & FS_REGULAR) != 0
127
+ end
128
+
129
+ # Check if font uses typographic metrics
130
+ #
131
+ # @return [Boolean] true if use typo metrics flag is set
132
+ def use_typo_metrics?
133
+ parsed && (parsed.fs_selection & FS_USE_TYPO_METRICS) != 0
134
+ end
135
+
136
+ # Check if font is oblique
137
+ #
138
+ # @return [Boolean] true if oblique flag is set
139
+ def oblique?
140
+ parsed && (parsed.fs_selection & FS_OBLIQUE) != 0
141
+ end
142
+
143
+ # Get typographic ascent
144
+ #
145
+ # @return [Integer, nil] Typographic ascender, or nil if not parsed
146
+ def typo_ascender
147
+ parsed&.s_typo_ascender
148
+ end
149
+
150
+ # Get typographic descent
151
+ #
152
+ # @return [Integer, nil] Typographic descender (negative value), or nil if not parsed
153
+ def typo_descender
154
+ parsed&.s_typo_descender
155
+ end
156
+
157
+ # Get typographic line gap
158
+ #
159
+ # @return [Integer, nil] Line gap, or nil if not parsed
160
+ def typo_line_gap
161
+ parsed&.s_typo_line_gap
162
+ end
163
+
164
+ # Get Windows ascent
165
+ #
166
+ # @return [Integer, nil] Windows ascender, or nil if not parsed
167
+ def win_ascent
168
+ parsed&.us_win_ascent
169
+ end
170
+
171
+ # Get Windows descent
172
+ #
173
+ # @return [Integer, nil] Windows descender, or nil if not parsed
174
+ def win_descent
175
+ parsed&.us_win_descent
176
+ end
177
+
178
+ # Get x-height (version 2+)
179
+ #
180
+ # @return [Integer, nil] x-height value, or nil if not available
181
+ def x_height
182
+ parsed&.sx_height
183
+ end
184
+
185
+ # Get cap height (version 2+)
186
+ #
187
+ # @return [Integer, nil] Cap height value, or nil if not available
188
+ def cap_height
189
+ parsed&.s_cap_height
190
+ end
191
+
192
+ # Check if embedding is allowed
193
+ #
194
+ # @return [Boolean] true if embedding is permitted (fs_type & 0x8 == 0)
195
+ def embedding_allowed?
196
+ return false unless parsed
197
+
198
+ # fs_type bit 3 (0x8) = Embedding must not be allowed
199
+ # If bit 3 is NOT set, embedding is allowed
200
+ (parsed.fs_type & 0x8).zero?
201
+ end
202
+
203
+ # Check if embedding is restricted
204
+ #
205
+ # @return [Boolean] true if embedding is restricted
206
+ def embedding_restricted?
207
+ !embedding_allowed?
208
+ end
209
+
210
+ # Check if preview/print embedding is allowed
211
+ #
212
+ # @return [Boolean] true if preview and print embedding is permitted
213
+ def preview_print_allowed?
214
+ return false unless parsed
215
+
216
+ # fs_type bit 1 (0x2) = Preview & Print embedding allowed
217
+ (parsed.fs_type & 0x2) != 0
218
+ end
219
+
220
+ # Check if editable embedding is allowed
221
+ #
222
+ # @return [Boolean] true if editable embedding is permitted
223
+ def editable_allowed?
224
+ return false unless parsed
225
+
226
+ # fs_type bit 2 (0x4) = Editable embedding allowed
227
+ (parsed.fs_type & 0x4) != 0
228
+ end
229
+
230
+ # Check if subsetting is allowed
231
+ #
232
+ # @return [Boolean] true if subsetting is permitted (fs_type bit 8 is NOT set)
233
+ def subsetting_allowed?
234
+ return false unless parsed
235
+
236
+ # fs_type bit 8 (0x100) = No subsetting
237
+ (parsed.fs_type & 0x100).zero?
238
+ end
239
+
240
+ # Check if bitmap embedding only is allowed
241
+ #
242
+ # @return [Boolean] true if only bitmaps can be embedded
243
+ def bitmap_embedding_only?
244
+ return false unless parsed
245
+
246
+ # fs_type bit 9 (0x200) = Bitmap embedding only
247
+ (parsed.fs_type & 0x200) != 0
248
+ end
249
+
250
+ # Get PANOSE classification
251
+ #
252
+ # @return [Array<Integer>, nil] Array of 10 PANOSE bytes, or nil if not parsed
253
+ def panose
254
+ parsed&.panose&.to_a
255
+ end
256
+
257
+ # Get first character index
258
+ #
259
+ # @return [Integer, nil] First character Unicode value, or nil if not parsed
260
+ def first_char_index
261
+ parsed&.us_first_char_index
262
+ end
263
+
264
+ # Get last character index
265
+ #
266
+ # @return [Integer, nil] Last character Unicode value, or nil if not parsed
267
+ def last_char_index
268
+ parsed&.us_last_char_index
269
+ end
270
+
271
+ protected
272
+
273
+ # Validate the parsed OS/2 table
274
+ #
275
+ # @return [Boolean] true if valid
276
+ # @raise [InvalidFontError] if OS/2 table is invalid
277
+ def validate_parsed_table?
278
+ return true unless parsed
279
+
280
+ # Validate version
281
+ unless parsed.valid_version?
282
+ raise InvalidFontError,
283
+ "Invalid OS/2 table version: #{parsed.version} (must be 0-5)"
284
+ end
285
+
286
+ # Validate weight class
287
+ unless parsed.valid_weight_class?
288
+ raise InvalidFontError,
289
+ "Invalid OS/2 weight class: #{parsed.us_weight_class} (must be 1-1000)"
290
+ end
291
+
292
+ # Validate width class
293
+ unless parsed.valid_width_class?
294
+ raise InvalidFontError,
295
+ "Invalid OS/2 width class: #{parsed.us_width_class} (must be 1-9)"
296
+ end
297
+
298
+ # Validate vendor ID
299
+ unless parsed.has_vendor_id?
300
+ raise InvalidFontError,
301
+ "Invalid OS/2 vendor ID: empty or missing"
302
+ end
303
+
304
+ # Validate typo metrics
305
+ unless parsed.valid_typo_metrics?
306
+ raise InvalidFontError,
307
+ "Invalid OS/2 typo metrics: ascent=#{parsed.s_typo_ascender}, " \
308
+ "descent=#{parsed.s_typo_descender}, line_gap=#{parsed.s_typo_line_gap}"
309
+ end
310
+
311
+ # Validate Win metrics
312
+ unless parsed.valid_win_metrics?
313
+ raise InvalidFontError,
314
+ "Invalid OS/2 Win metrics: win_ascent=#{parsed.us_win_ascent}, " \
315
+ "win_descent=#{parsed.us_win_descent} (both must be positive)"
316
+ end
317
+
318
+ # Validate character range
319
+ unless parsed.valid_char_range?
320
+ raise InvalidFontError,
321
+ "Invalid OS/2 character range: first=#{parsed.us_first_char_index}, " \
322
+ "last=#{parsed.us_last_char_index} (first must be <= last)"
323
+ end
324
+
325
+ true
326
+ end
327
+ end
328
+ end
329
+ end
@@ -177,7 +177,7 @@ module Fontisan
177
177
  #
178
178
  # @return [Boolean] True if is_fixed_pitch is 0 or 1
179
179
  def valid_fixed_pitch_flag?
180
- is_fixed_pitch == 0 || is_fixed_pitch == 1
180
+ [0, 1].include?(is_fixed_pitch)
181
181
  end
182
182
 
183
183
  # Validation helper: Check if glyph names are available
@@ -198,7 +198,7 @@ module Fontisan
198
198
  def complete_version_2_data?
199
199
  return true unless version == 2.0
200
200
 
201
- !num_glyphs_v2.nil? && num_glyphs_v2 > 0 && !remaining_data.empty?
201
+ !num_glyphs_v2.nil? && num_glyphs_v2.positive? && !remaining_data.empty?
202
202
  end
203
203
  end
204
204
  end