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
@@ -0,0 +1,484 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/validation_report"
4
+
5
+ module Fontisan
6
+ module Validators
7
+ # Base class for all validators using block-based DSL
8
+ #
9
+ # This class provides a declarative DSL for defining validation checks
10
+ # and an execution engine that runs those checks against font files.
11
+ # Subclasses define their validation logic by implementing define_checks.
12
+ #
13
+ # @example Creating a custom validator
14
+ # class MyValidator < Validator
15
+ # private
16
+ #
17
+ # def define_checks
18
+ # check_table :name_table, 'name' do
19
+ # check_field :family_name, :family_name do |table, value|
20
+ # !value.nil? && !value.empty?
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # @example Running validation
27
+ # validator = MyValidator.new
28
+ # report = validator.validate(font)
29
+ # puts report.valid?
30
+ class Validator
31
+ # Initialize validator and define checks
32
+ def initialize
33
+ @checks = []
34
+ @current_table_context = nil
35
+ define_checks
36
+ end
37
+
38
+ # Validate a font and return a ValidationReport
39
+ #
40
+ # @param font [TrueTypeFont, OpenTypeFont] Font object to validate
41
+ # @return [ValidationReport] Complete validation report
42
+ def validate(font)
43
+ start_time = Time.now
44
+ all_results = []
45
+
46
+ @checks.each do |check_def|
47
+ result = execute_check(font, check_def)
48
+ all_results << result
49
+ end
50
+
51
+ elapsed = Time.now - start_time
52
+ build_report(font, all_results, elapsed)
53
+ end
54
+
55
+ protected
56
+
57
+ # DSL method: Define a table-level check
58
+ #
59
+ # @param check_id [Symbol] Unique identifier for this check
60
+ # @param table_tag [String] OpenType table tag (e.g., 'name', 'head')
61
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
62
+ # @param block [Proc] Check logic that receives table as parameter
63
+ def check_table(check_id, table_tag, severity: :error, &block)
64
+ @checks << {
65
+ type: :table,
66
+ id: check_id,
67
+ table_tag: table_tag,
68
+ severity: severity,
69
+ block: block,
70
+ }
71
+ end
72
+
73
+ # DSL method: Define a field-level check
74
+ #
75
+ # Must be called within a check_table block to establish table context
76
+ #
77
+ # @param check_id [Symbol] Unique identifier for this check
78
+ # @param field_key [Symbol] Field name to check
79
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
80
+ # @param block [Proc] Check logic that receives (table, value) as parameters
81
+ def check_field(check_id, field_key, severity: :error, &block)
82
+ unless @current_table_context
83
+ raise ArgumentError, "check_field must be called within check_table block"
84
+ end
85
+
86
+ @checks << {
87
+ type: :field,
88
+ id: check_id,
89
+ table_tag: @current_table_context,
90
+ field: field_key,
91
+ severity: severity,
92
+ block: block,
93
+ }
94
+ end
95
+
96
+ # DSL method: Define a structural validation check
97
+ #
98
+ # Used for checks that validate font structure and relationships
99
+ #
100
+ # @param check_id [Symbol] Unique identifier for this check
101
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
102
+ # @param block [Proc] Check logic that receives font as parameter
103
+ def check_structure(check_id, severity: :error, &block)
104
+ @checks << {
105
+ type: :structure,
106
+ id: check_id,
107
+ severity: severity,
108
+ block: block,
109
+ }
110
+ end
111
+
112
+ # DSL method: Define a usability check
113
+ #
114
+ # Used for checks that validate font usability and best practices
115
+ #
116
+ # @param check_id [Symbol] Unique identifier for this check
117
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
118
+ # @param block [Proc] Check logic that receives font as parameter
119
+ def check_usability(check_id, severity: :warning, &block)
120
+ @checks << {
121
+ type: :usability,
122
+ id: check_id,
123
+ severity: severity,
124
+ block: block,
125
+ }
126
+ end
127
+
128
+ # DSL method: Define an instruction validation check
129
+ #
130
+ # Used for checks that validate TrueType instructions/hinting
131
+ #
132
+ # @param check_id [Symbol] Unique identifier for this check
133
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
134
+ # @param block [Proc] Check logic that receives font as parameter
135
+ def check_instructions(check_id, severity: :warning, &block)
136
+ @checks << {
137
+ type: :instructions,
138
+ id: check_id,
139
+ severity: severity,
140
+ block: block,
141
+ }
142
+ end
143
+
144
+ # DSL method: Define a glyph-level check
145
+ #
146
+ # Used for checks that validate individual glyphs
147
+ #
148
+ # @param check_id [Symbol] Unique identifier for this check
149
+ # @param severity [Symbol] Severity level (:info, :warning, :error, :fatal)
150
+ # @param block [Proc] Check logic that receives font as parameter
151
+ def check_glyphs(check_id, severity: :error, &block)
152
+ @checks << {
153
+ type: :glyphs,
154
+ id: check_id,
155
+ severity: severity,
156
+ block: block,
157
+ }
158
+ end
159
+
160
+ private
161
+
162
+ # Template method: Subclasses implement this to define their checks
163
+ #
164
+ # @return [void]
165
+ def define_checks
166
+ # Subclasses override this method
167
+ end
168
+
169
+ # Execute a single check using strategy pattern
170
+ #
171
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
172
+ # @param check_def [Hash] Check definition
173
+ # @return [Hash] Check result with :passed, :severity, :messages, :issues
174
+ def execute_check(font, check_def)
175
+ case check_def[:type]
176
+ when :table
177
+ execute_table_check(font, check_def)
178
+ when :field
179
+ execute_field_check(font, check_def)
180
+ when :structure
181
+ execute_structure_check(font, check_def)
182
+ when :usability
183
+ execute_usability_check(font, check_def)
184
+ when :instructions
185
+ execute_instruction_check(font, check_def)
186
+ when :glyphs
187
+ execute_glyph_check(font, check_def)
188
+ else
189
+ {
190
+ check_id: check_def[:id],
191
+ passed: false,
192
+ severity: :fatal,
193
+ messages: ["Unknown check type: #{check_def[:type]}"],
194
+ issues: [],
195
+ }
196
+ end
197
+ rescue => e
198
+ {
199
+ check_id: check_def[:id],
200
+ passed: false,
201
+ severity: :fatal,
202
+ messages: ["Check execution failed: #{e.message}"],
203
+ issues: [{
204
+ severity: "fatal",
205
+ category: "check_execution",
206
+ message: "Exception during check execution: #{e.class} - #{e.message}",
207
+ }],
208
+ }
209
+ end
210
+
211
+ # Execute a table-level check
212
+ #
213
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
214
+ # @param check_def [Hash] Check definition
215
+ # @return [Hash] Check result
216
+ def execute_table_check(font, check_def)
217
+ table_tag = check_def[:table_tag]
218
+ table = font.table(table_tag)
219
+
220
+ unless table
221
+ return {
222
+ check_id: check_def[:id],
223
+ passed: false,
224
+ severity: check_def[:severity].to_s,
225
+ messages: ["Table '#{table_tag}' not found in font"],
226
+ table: table_tag,
227
+ issues: [{
228
+ severity: check_def[:severity].to_s,
229
+ category: "table_presence",
230
+ table: table_tag,
231
+ message: "Required table '#{table_tag}' is missing",
232
+ }],
233
+ }
234
+ end
235
+
236
+ # Set context for nested field checks
237
+ old_context = @current_table_context
238
+ @current_table_context = table_tag
239
+
240
+ begin
241
+ result = check_def[:block].call(table)
242
+ passed = result != false && result != nil
243
+
244
+ {
245
+ check_id: check_def[:id],
246
+ passed: passed,
247
+ severity: check_def[:severity].to_s,
248
+ messages: passed ? [] : ["Table '#{table_tag}' validation failed"],
249
+ table: table_tag,
250
+ issues: passed ? [] : [{
251
+ severity: check_def[:severity].to_s,
252
+ category: "table_validation",
253
+ table: table_tag,
254
+ message: "Table '#{table_tag}' failed validation",
255
+ }],
256
+ }
257
+ ensure
258
+ @current_table_context = old_context
259
+ end
260
+ end
261
+
262
+ # Execute a field-level check
263
+ #
264
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
265
+ # @param check_def [Hash] Check definition
266
+ # @return [Hash] Check result
267
+ def execute_field_check(font, check_def)
268
+ table_tag = check_def[:table_tag]
269
+ field_key = check_def[:field]
270
+ table = font.table(table_tag)
271
+
272
+ unless table
273
+ return {
274
+ check_id: check_def[:id],
275
+ passed: false,
276
+ severity: check_def[:severity].to_s,
277
+ messages: ["Table '#{table_tag}' not found"],
278
+ table: table_tag,
279
+ field: field_key.to_s,
280
+ issues: [{
281
+ severity: check_def[:severity].to_s,
282
+ category: "table_presence",
283
+ table: table_tag,
284
+ field: field_key.to_s,
285
+ message: "Cannot validate field '#{field_key}': table '#{table_tag}' missing",
286
+ }],
287
+ }
288
+ end
289
+
290
+ # Get field value
291
+ value = if table.respond_to?(field_key)
292
+ table.public_send(field_key)
293
+ else
294
+ nil
295
+ end
296
+
297
+ result = check_def[:block].call(table, value)
298
+ passed = result != false && result != nil
299
+
300
+ {
301
+ check_id: check_def[:id],
302
+ passed: passed,
303
+ severity: check_def[:severity].to_s,
304
+ messages: passed ? [] : ["Field '#{field_key}' validation failed"],
305
+ table: table_tag,
306
+ field: field_key.to_s,
307
+ issues: passed ? [] : [{
308
+ severity: check_def[:severity].to_s,
309
+ category: "field_validation",
310
+ table: table_tag,
311
+ field: field_key.to_s,
312
+ message: "Field '#{field_key}' in table '#{table_tag}' failed validation",
313
+ }],
314
+ }
315
+ end
316
+
317
+ # Execute a structure check
318
+ #
319
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
320
+ # @param check_def [Hash] Check definition
321
+ # @return [Hash] Check result
322
+ def execute_structure_check(font, check_def)
323
+ result = check_def[:block].call(font)
324
+ passed = result != false && result != nil
325
+
326
+ {
327
+ check_id: check_def[:id],
328
+ passed: passed,
329
+ severity: check_def[:severity].to_s,
330
+ messages: passed ? [] : ["Structure validation failed"],
331
+ issues: passed ? [] : [{
332
+ severity: check_def[:severity].to_s,
333
+ category: "structure",
334
+ message: "Font structure validation failed for check '#{check_def[:id]}'",
335
+ }],
336
+ }
337
+ end
338
+
339
+ # Execute a usability check
340
+ #
341
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
342
+ # @param check_def [Hash] Check definition
343
+ # @return [Hash] Check result
344
+ def execute_usability_check(font, check_def)
345
+ result = check_def[:block].call(font)
346
+ passed = result != false && result != nil
347
+
348
+ {
349
+ check_id: check_def[:id],
350
+ passed: passed,
351
+ severity: check_def[:severity].to_s,
352
+ messages: passed ? [] : ["Usability check failed"],
353
+ issues: passed ? [] : [{
354
+ severity: check_def[:severity].to_s,
355
+ category: "usability",
356
+ message: "Font usability check failed for '#{check_def[:id]}'",
357
+ }],
358
+ }
359
+ end
360
+
361
+ # Execute an instruction check
362
+ #
363
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
364
+ # @param check_def [Hash] Check definition
365
+ # @return [Hash] Check result
366
+ def execute_instruction_check(font, check_def)
367
+ result = check_def[:block].call(font)
368
+ passed = result != false && result != nil
369
+
370
+ {
371
+ check_id: check_def[:id],
372
+ passed: passed,
373
+ severity: check_def[:severity].to_s,
374
+ messages: passed ? [] : ["Instruction validation failed"],
375
+ issues: passed ? [] : [{
376
+ severity: check_def[:severity].to_s,
377
+ category: "instructions",
378
+ message: "TrueType instruction check failed for '#{check_def[:id]}'",
379
+ }],
380
+ }
381
+ end
382
+
383
+ # Execute a glyph check
384
+ #
385
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
386
+ # @param check_def [Hash] Check definition
387
+ # @return [Hash] Check result
388
+ def execute_glyph_check(font, check_def)
389
+ result = check_def[:block].call(font)
390
+ passed = result != false && result != nil
391
+
392
+ {
393
+ check_id: check_def[:id],
394
+ passed: passed,
395
+ severity: check_def[:severity].to_s,
396
+ messages: passed ? [] : ["Glyph validation failed"],
397
+ issues: passed ? [] : [{
398
+ severity: check_def[:severity].to_s,
399
+ category: "glyphs",
400
+ message: "Glyph validation failed for check '#{check_def[:id]}'",
401
+ }],
402
+ }
403
+ end
404
+
405
+ # Build ValidationReport from check results
406
+ #
407
+ # @param font [TrueTypeFont, OpenTypeFont] Font that was validated
408
+ # @param all_results [Array<Hash>] All check results
409
+ # @param elapsed [Float] Elapsed time in seconds
410
+ # @return [ValidationReport] Complete report
411
+ def build_report(font, all_results, elapsed)
412
+ # Extract font path from font object
413
+ font_path = if font.respond_to?(:path)
414
+ font.path
415
+ elsif font.respond_to?(:filename)
416
+ font.filename
417
+ elsif font.instance_variable_defined?(:@filename)
418
+ font.instance_variable_get(:@filename)
419
+ else
420
+ "unknown"
421
+ end
422
+
423
+ report = Models::ValidationReport.new(
424
+ font_path: font_path,
425
+ valid: true,
426
+ )
427
+
428
+ # Build CheckResult objects
429
+ all_results.each do |result|
430
+ check_result = Models::ValidationReport::CheckResult.new(
431
+ check_id: result[:check_id].to_s,
432
+ passed: result[:passed],
433
+ severity: result[:severity],
434
+ messages: result[:messages] || [],
435
+ table: result[:table],
436
+ field: result[:field],
437
+ )
438
+ report.check_results << check_result
439
+
440
+ # Add issues to main report
441
+ if result[:issues]
442
+ result[:issues].each do |issue_data|
443
+ case issue_data[:severity]
444
+ when "error", "fatal"
445
+ report.add_error(
446
+ issue_data[:category] || "validation",
447
+ issue_data[:message],
448
+ issue_data[:table] || issue_data[:field],
449
+ )
450
+ when "warning"
451
+ report.add_warning(
452
+ issue_data[:category] || "validation",
453
+ issue_data[:message],
454
+ issue_data[:table] || issue_data[:field],
455
+ )
456
+ when "info"
457
+ report.add_info(
458
+ issue_data[:category] || "validation",
459
+ issue_data[:message],
460
+ issue_data[:table] || issue_data[:field],
461
+ )
462
+ end
463
+ end
464
+ end
465
+ end
466
+
467
+ # Mark checks performed
468
+ report.checks_performed = all_results.map { |r| r[:check_id].to_s }
469
+
470
+ # Set status based on results
471
+ if report.has_errors?
472
+ report.status = "invalid"
473
+ report.valid = false
474
+ elsif report.has_warnings?
475
+ report.status = "valid_with_warnings"
476
+ else
477
+ report.status = "valid"
478
+ end
479
+
480
+ report
481
+ end
482
+ end
483
+ end
484
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "basic_validator"
4
+
5
+ module Fontisan
6
+ module Validators
7
+ # WebFontValidator provides web font optimization and embedding compatibility checks
8
+ #
9
+ # This validator extends BasicValidator with checks specific to web font use cases.
10
+ # Unlike FontBookValidator, it focuses on web embedding permissions, file size,
11
+ # and WOFF/WOFF2 conversion readiness rather than desktop installation.
12
+ #
13
+ # The validator inherits 8 checks from BasicValidator and adds 10 new checks:
14
+ # - Embedding permissions (OS/2 fsType)
15
+ # - File size and glyph complexity for web performance
16
+ # - Character coverage for web use
17
+ # - Glyph accessibility
18
+ # - WOFF/WOFF2 conversion readiness
19
+ #
20
+ # @example Using WebFontValidator
21
+ # validator = WebFontValidator.new
22
+ # report = validator.validate(font)
23
+ # puts "Font is web-ready" if report.valid?
24
+ class WebFontValidator < BasicValidator
25
+ private
26
+
27
+ # Define web font validation checks
28
+ #
29
+ # Calls super to inherit BasicValidator's 8 checks, then adds 10 new checks.
30
+ # All checks use helpers from Week 1 table implementations.
31
+ def define_checks
32
+ # Inherit BasicValidator checks (8 checks)
33
+ super
34
+
35
+ # Check 9: OS/2 embedding permissions must allow web use
36
+ check_table :embedding_permissions, 'OS/2', severity: :error do |table|
37
+ table.has_embedding_permissions?
38
+ end
39
+
40
+ # Check 10: OS/2 version should be present
41
+ check_table :os2_version_web, 'OS/2', severity: :warning do |table|
42
+ table.valid_version?
43
+ end
44
+
45
+ # Check 11: Glyph complexity should be reasonable for web
46
+ check_glyphs :no_complex_glyphs, severity: :warning do |font|
47
+ maxp = font.table('maxp')
48
+ next true unless maxp.version_1_0?
49
+
50
+ # Check max points and contours are reasonable for web rendering
51
+ maxp.max_points && maxp.max_points < 3000 &&
52
+ maxp.max_contours && maxp.max_contours < 500
53
+ end
54
+
55
+ # Check 12: Cmap must have Unicode mapping for web
56
+ check_table :character_coverage, 'cmap', severity: :error do |table|
57
+ table.has_unicode_mapping?
58
+ end
59
+
60
+ # Check 13: Cmap should have BMP coverage
61
+ check_table :cmap_bmp_web, 'cmap', severity: :warning do |table|
62
+ table.has_bmp_coverage?
63
+ end
64
+
65
+ # Check 14: Glyf glyphs must be accessible (web browsers need this)
66
+ check_glyphs :glyph_accessible_web, severity: :error do |font|
67
+ glyf = font.table('glyf')
68
+ next true unless glyf
69
+
70
+ loca = font.table('loca')
71
+ head = font.table('head')
72
+ maxp = font.table('maxp')
73
+ glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
74
+ end
75
+
76
+ # Check 15: Head table must have valid bounding box
77
+ check_table :head_bbox_web, 'head', severity: :error do |table|
78
+ table.valid_bounding_box?
79
+ end
80
+
81
+ # Check 16: Hhea metrics must be valid for web rendering
82
+ check_table :hhea_metrics_web, 'hhea', severity: :error do |table|
83
+ table.valid_ascent_descent? && table.valid_number_of_h_metrics?
84
+ end
85
+
86
+ # Check 17: WOFF conversion readiness
87
+ check_structure :woff_conversion_ready, severity: :info do |font|
88
+ # Check font can be converted to WOFF
89
+ # All required tables present
90
+ %w[name head maxp hhea].all? { |tag| font.table(tag) }
91
+ end
92
+
93
+ # Check 18: WOFF2 conversion readiness
94
+ check_structure :woff2_conversion_ready, severity: :info do |font|
95
+ # Check font can be converted to WOFF2
96
+ # Same requirements as WOFF
97
+ %w[name head maxp hhea].all? { |tag| font.table(tag) }
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.5"
5
5
  end
@@ -47,9 +47,14 @@ module Fontisan
47
47
  ].freeze
48
48
 
49
49
  # Transformation versions
50
- TRANSFORM_NONE = 0
51
- TRANSFORM_GLYF_LOCA = 0 # Applied to both glyf and loca
52
- TRANSFORM_HMTX = 0 # Applied to hmtx
50
+ # According to WOFF2 spec:
51
+ # - glyf/loca: version 0 or 3 WITH transformLength = transformed
52
+ # - glyf/loca: version 1 or 2 WITHOUT transformLength = not transformed
53
+ # - hmtx: version 1 WITH transformLength = transformed
54
+ # - hmtx: version 0, 2, or 3 WITHOUT transformLength = not transformed
55
+ TRANSFORM_NONE = 3 # Use version 3 when not transformed (works for all tables)
56
+ TRANSFORM_GLYF_LOCA = 0 # glyf/loca use version 0 when transformed
57
+ TRANSFORM_HMTX = 1 # hmtx uses version 1 when transformed
53
58
 
54
59
  # Custom tag indicator
55
60
  CUSTOM_TAG_INDEX = 0x3F
@@ -91,7 +96,7 @@ module Fontisan
91
96
  #
92
97
  # @return [Boolean] True if transformed
93
98
  def transformed?
94
- transform_version != TRANSFORM_NONE && transform_length
99
+ !transform_length.nil? && transform_length.positive?
95
100
  end
96
101
 
97
102
  # Get transformation version from flags
@@ -108,16 +113,40 @@ module Fontisan
108
113
  flags & 0x3F
109
114
  end
110
115
 
111
- # Determine if this table should be transformed
116
+ # Determine transformation version for this table
112
117
  #
113
- # For Phase 2 Milestone 2.1, we support transformation flags
114
- # but don't implement the actual transformations yet.
118
+ # Returns the appropriate version based on:
119
+ # 1. Whether table has transform_length set (is transformed)
120
+ # 2. Which table it is (glyf/loca vs hmtx vs other)
115
121
  #
116
- # @return [Integer] Transform version
122
+ # @return [Integer] Transform version (0-3)
117
123
  def determine_transform_version
118
- # For this milestone, we don't apply transformations
119
- # but we recognize which tables could be transformed
120
- TRANSFORM_NONE
124
+ if transformed?
125
+ # Table IS transformed - use appropriate transform version
126
+ case tag
127
+ when "glyf", "loca"
128
+ TRANSFORM_GLYF_LOCA # Version 0 for transformed glyf/loca
129
+ when "hmtx"
130
+ TRANSFORM_HMTX # Version 1 for transformed hmtx
131
+ else
132
+ TRANSFORM_NONE # Shouldn't happen, but use safe default
133
+ end
134
+ else
135
+ # Table is NOT transformed - use version that indicates no transformation
136
+ case tag
137
+ when "glyf", "loca"
138
+ # For glyf/loca, version 0 means transformed
139
+ # so use version 3 to indicate NOT transformed
140
+ TRANSFORM_NONE # Version 3
141
+ when "hmtx"
142
+ # For hmtx, version 1 means transformed
143
+ # so use version 0 to indicate NOT transformed
144
+ 0
145
+ else
146
+ # All other tables use version 0 (no transformation)
147
+ 0
148
+ end
149
+ end
121
150
  end
122
151
 
123
152
  # Check if table can be transformed (glyf, loca, hmtx)