fontisan 0.2.3 → 0.2.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +92 -40
  3. data/README.adoc +262 -3
  4. data/Rakefile +20 -7
  5. data/lib/fontisan/commands/base_command.rb +2 -19
  6. data/lib/fontisan/commands/convert_command.rb +16 -13
  7. data/lib/fontisan/commands/info_command.rb +88 -0
  8. data/lib/fontisan/config/conversion_matrix.yml +58 -20
  9. data/lib/fontisan/converters/outline_converter.rb +6 -3
  10. data/lib/fontisan/converters/svg_generator.rb +45 -0
  11. data/lib/fontisan/converters/woff2_encoder.rb +106 -13
  12. data/lib/fontisan/models/bitmap_glyph.rb +123 -0
  13. data/lib/fontisan/models/bitmap_strike.rb +94 -0
  14. data/lib/fontisan/models/color_glyph.rb +57 -0
  15. data/lib/fontisan/models/color_layer.rb +53 -0
  16. data/lib/fontisan/models/color_palette.rb +60 -0
  17. data/lib/fontisan/models/font_info.rb +26 -0
  18. data/lib/fontisan/models/svg_glyph.rb +89 -0
  19. data/lib/fontisan/open_type_font.rb +6 -0
  20. data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
  21. data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
  22. data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
  23. data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
  24. data/lib/fontisan/pipeline/output_writer.rb +2 -2
  25. data/lib/fontisan/tables/cbdt.rb +169 -0
  26. data/lib/fontisan/tables/cblc.rb +290 -0
  27. data/lib/fontisan/tables/cff.rb +6 -12
  28. data/lib/fontisan/tables/colr.rb +291 -0
  29. data/lib/fontisan/tables/cpal.rb +281 -0
  30. data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
  31. data/lib/fontisan/tables/sbix.rb +379 -0
  32. data/lib/fontisan/tables/svg.rb +301 -0
  33. data/lib/fontisan/true_type_font.rb +6 -0
  34. data/lib/fontisan/validation/woff2_header_validator.rb +278 -0
  35. data/lib/fontisan/validation/woff2_table_validator.rb +270 -0
  36. data/lib/fontisan/validation/woff2_validator.rb +248 -0
  37. data/lib/fontisan/version.rb +1 -1
  38. data/lib/fontisan/woff2/directory.rb +40 -11
  39. data/lib/fontisan/woff2/table_transformer.rb +506 -73
  40. data/lib/fontisan/woff2_font.rb +29 -9
  41. data/lib/fontisan/woff_font.rb +17 -4
  42. data/lib/fontisan.rb +12 -0
  43. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d342cbc82376aa9327747fd679edf9199ab0e1fbed9a729d4385aae52d6c5de9
4
- data.tar.gz: 8427c56d4e65ca035a5bff415ef7020e3f6e17c31f4c43d8b28b5ca8e41e4fe4
3
+ metadata.gz: efdc28872530847f76e7d586ed2ac859d0860275a9289936e742a467f6139799
4
+ data.tar.gz: b1af1580a3ef2d2b1dbb449c3c7295194497b358d8819d5f18496282b7730ed5
5
5
  SHA512:
6
- metadata.gz: e5c9b89e658113edcd96e98c4c56733538a4e47eb18220d49ca8e21c5117285795759a6e3dd5b4f2a0b29f7a4a66c0bf0bf25757780dd438045eb705feef97ef
7
- data.tar.gz: 5ec8c48befb405a5d9911a6f00a2287a9cae461847b52094abe1367cd5f1fd5c451363182ed3c9a924c96d13299659869c6ffa2f192951301d1218eb840e0623
6
+ metadata.gz: 533a181937b1b46ea380a14a9c108ba19678f9bf1c0fcb8a5b51f0e0fb43630c69180c74679ad91da49fa73d136ec783e69b460ce8f031834e3fc500ca666139
7
+ data.tar.gz: '09e9d4ff6a4694df5d6756dc9f4dc056f1951d66ca456079ad40336a41e32b804b15aa597f4089d20a20474a75faf680782c6cca2bf5ee8c2d5c30eb41e12623'
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-12-30 09:54:17 UTC using RuboCop version 1.81.7.
3
+ # on 2026-01-03 02:11:10 UTC using RuboCop version 1.81.7.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -12,6 +12,14 @@ Gemspec/RequiredRubyVersion:
12
12
  Exclude:
13
13
  - 'fontisan.gemspec'
14
14
 
15
+ # Offense count: 1
16
+ # This cop supports safe autocorrection (--autocorrect).
17
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
18
+ # SupportedStyles: with_first_argument, with_fixed_indentation
19
+ Layout/ArgumentAlignment:
20
+ Exclude:
21
+ - 'lib/fontisan/converters/woff2_encoder.rb'
22
+
15
23
  # Offense count: 1
16
24
  # This cop supports safe autocorrection (--autocorrect).
17
25
  Layout/EmptyLineAfterGuardClause:
@@ -24,21 +32,32 @@ Layout/EmptyLinesAroundExceptionHandlingKeywords:
24
32
  Exclude:
25
33
  - 'lib/fontisan/hints/hint_validator.rb'
26
34
 
27
- # Offense count: 2
35
+ # Offense count: 23
28
36
  # This cop supports safe autocorrection (--autocorrect).
29
37
  # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
30
38
  Layout/ExtraSpacing:
31
39
  Exclude:
40
+ - 'lib/fontisan/commands/info_command.rb'
32
41
  - 'lib/fontisan/hints/truetype_instruction_analyzer.rb'
42
+ - 'lib/fontisan/tables/sbix.rb'
43
+ - 'spec/fontisan/tables/sbix_spec.rb'
44
+ - 'spec/integration/color_emoji_fonts_spec.rb'
33
45
 
34
- # Offense count: 1109
46
+ # Offense count: 1270
35
47
  # This cop supports safe autocorrection (--autocorrect).
36
48
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
37
49
  # URISchemes: http, https
38
50
  Layout/LineLength:
39
51
  Enabled: false
40
52
 
41
- # Offense count: 26
53
+ # Offense count: 18
54
+ # This cop supports safe autocorrection (--autocorrect).
55
+ # Configuration parameters: AllowInHeredoc.
56
+ Layout/TrailingWhitespace:
57
+ Exclude:
58
+ - 'spec/fontisan/converters/woff2_encoder_integration_spec.rb'
59
+
60
+ # Offense count: 27
42
61
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
43
62
  Lint/DuplicateBranch:
44
63
  Enabled: false
@@ -79,17 +98,26 @@ Lint/StructNewOverride:
79
98
  Exclude:
80
99
  - 'lib/fontisan/optimizers/pattern_analyzer.rb'
81
100
 
82
- # Offense count: 5
101
+ # Offense count: 1
102
+ # This cop supports safe autocorrection (--autocorrect).
103
+ # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
104
+ Lint/UnusedBlockArgument:
105
+ Exclude:
106
+ - 'spec/fontisan/tables/sbix_spec.rb'
107
+
108
+ # Offense count: 8
83
109
  # This cop supports safe autocorrection (--autocorrect).
84
110
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
85
111
  # NotImplementedExceptions: NotImplementedError
86
112
  Lint/UnusedMethodArgument:
87
113
  Exclude:
88
114
  - 'lib/fontisan/font_loader.rb'
115
+ - 'lib/fontisan/optimizers/charstring_rewriter.rb'
89
116
  - 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb'
90
117
  - 'lib/fontisan/utilities/brotli_wrapper.rb'
91
118
  - 'lib/fontisan/variation/table_accessor.rb'
92
119
  - 'lib/fontisan/woff2/glyf_transformer.rb'
120
+ - 'lib/fontisan/woff_font.rb'
93
121
 
94
122
  # Offense count: 3
95
123
  # This cop supports safe autocorrection (--autocorrect).
@@ -98,39 +126,39 @@ Lint/UselessAssignment:
98
126
  - 'lib/fontisan/hints/truetype_instruction_analyzer.rb'
99
127
  - 'spec/fontisan/hints/hint_round_trip_spec.rb'
100
128
 
101
- # Offense count: 432
129
+ # Offense count: 462
102
130
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
103
131
  Metrics/AbcSize:
104
132
  Enabled: false
105
133
 
106
- # Offense count: 27
134
+ # Offense count: 29
107
135
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
108
136
  # AllowedMethods: refine
109
137
  Metrics/BlockLength:
110
- Max: 80
138
+ Max: 91
111
139
 
112
140
  # Offense count: 8
113
141
  # Configuration parameters: CountBlocks, CountModifierForms.
114
142
  Metrics/BlockNesting:
115
143
  Max: 5
116
144
 
117
- # Offense count: 223
145
+ # Offense count: 240
118
146
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
119
147
  Metrics/CyclomaticComplexity:
120
148
  Enabled: false
121
149
 
122
- # Offense count: 648
150
+ # Offense count: 699
123
151
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
124
152
  Metrics/MethodLength:
125
153
  Max: 135
126
154
 
127
- # Offense count: 17
155
+ # Offense count: 18
128
156
  # Configuration parameters: CountKeywordArgs.
129
157
  Metrics/ParameterLists:
130
158
  Max: 39
131
159
  MaxOptionalParameters: 4
132
160
 
133
- # Offense count: 157
161
+ # Offense count: 174
134
162
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
135
163
  Metrics/PerceivedComplexity:
136
164
  Enabled: false
@@ -145,29 +173,14 @@ Naming/MethodParameterName:
145
173
  - 'lib/fontisan/tables/glyf/curve_converter.rb'
146
174
  - 'lib/fontisan/variation/optimizer.rb'
147
175
 
148
- # Offense count: 71
176
+ # Offense count: 84
149
177
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
150
178
  # SupportedStyles: snake_case, normalcase, non_integer
151
179
  # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
152
180
  Naming/VariableNumber:
153
- Exclude:
154
- - 'lib/fontisan/models/ttx/tables/os2_table.rb'
155
- - 'lib/fontisan/subset/table_subsetter.rb'
156
- - 'lib/fontisan/tables/cff/charset.rb'
157
- - 'lib/fontisan/tables/cff/encoding.rb'
158
- - 'lib/fontisan/tables/cmap.rb'
159
- - 'lib/fontisan/tables/glyf.rb'
160
- - 'lib/fontisan/tables/glyf/compound_glyph.rb'
161
- - 'lib/fontisan/tables/glyf/glyph_builder.rb'
162
- - 'lib/fontisan/tables/glyf/simple_glyph.rb'
163
- - 'spec/fontisan/collection/table_deduplicator_spec.rb'
164
- - 'spec/fontisan/tables/cff/charset_spec.rb'
165
- - 'spec/fontisan/tables/glyf/glyph_builder_spec.rb'
166
- - 'spec/fontisan/tables/glyf_spec.rb'
167
- - 'spec/fontisan/tables/hmtx_spec.rb'
168
- - 'spec/fontisan/tables/maxp_spec.rb'
181
+ Enabled: false
169
182
 
170
- # Offense count: 22
183
+ # Offense count: 25
171
184
  # Configuration parameters: MinSize.
172
185
  Performance/CollectionLiteralInLoop:
173
186
  Exclude:
@@ -177,8 +190,10 @@ Performance/CollectionLiteralInLoop:
177
190
  - 'lib/fontisan/tables/cff/private_dict_writer.rb'
178
191
  - 'lib/fontisan/tables/cff2.rb'
179
192
  - 'lib/fontisan/tables/cff2/region_matcher.rb'
193
+ - 'lib/fontisan/tables/sbix.rb'
180
194
  - 'lib/fontisan/true_type_font.rb'
181
195
  - 'lib/fontisan/variation/validator.rb'
196
+ - 'lib/fontisan/woff2/table_transformer.rb'
182
197
  - 'spec/fontisan/cli_spec.rb'
183
198
  - 'spec/fontisan/commands/glyphs_command_spec.rb'
184
199
  - 'spec/fontisan/commands/info_command_spec.rb'
@@ -202,36 +217,40 @@ RSpec/AnyInstance:
202
217
  - 'spec/fontisan/variation/parallel_generator_spec.rb'
203
218
  - 'spec/integration/format_conversion_spec.rb'
204
219
 
205
- # Offense count: 100
220
+ # Offense count: 105
206
221
  # Configuration parameters: Prefixes, AllowedPatterns.
207
222
  # Prefixes: when, with, without
208
223
  RSpec/ContextWording:
209
224
  Enabled: false
210
225
 
211
- # Offense count: 17
226
+ # Offense count: 21
212
227
  # Configuration parameters: IgnoredMetadata.
213
228
  RSpec/DescribeClass:
214
229
  Enabled: false
215
230
 
216
- # Offense count: 1
231
+ # Offense count: 2
217
232
  RSpec/DescribeMethod:
218
233
  Exclude:
219
234
  - 'spec/fontisan/collection/variable_font_builder_spec.rb'
235
+ - 'spec/fontisan/converters/woff2_encoder_integration_spec.rb'
220
236
 
221
- # Offense count: 1156
237
+ # Offense count: 1263
222
238
  # Configuration parameters: CountAsOne.
223
239
  RSpec/ExampleLength:
224
240
  Max: 66
225
241
 
226
- # Offense count: 4
242
+ # Offense count: 7
227
243
  # This cop supports safe autocorrection (--autocorrect).
228
244
  RSpec/ExpectActual:
229
245
  Exclude:
230
246
  - '**/spec/routing/**/*'
231
247
  - 'spec/fontisan/commands/subset_command_spec.rb'
248
+ - 'spec/fontisan/converters/extended_woff2_spec.rb'
249
+ - 'spec/fontisan/converters/woff2_round_trip_spec.rb'
232
250
  - 'spec/fontisan/subset/builder_spec.rb'
233
251
  - 'spec/fontisan/utils/thread_pool_spec.rb'
234
252
  - 'spec/fontisan/variation/validator_spec.rb'
253
+ - 'spec/fontisan/woff2/table_transformer_spec.rb'
235
254
 
236
255
  # Offense count: 1
237
256
  RSpec/IdenticalEqualityAssertion:
@@ -280,19 +299,20 @@ RSpec/MessageSpies:
280
299
  - 'spec/fontisan/variation/instance_writer_spec.rb'
281
300
  - 'spec/fontisan/woff2_font_spec.rb'
282
301
 
283
- # Offense count: 4
302
+ # Offense count: 5
284
303
  RSpec/MultipleDescribes:
285
304
  Exclude:
286
305
  - 'spec/fontisan/loading_modes_spec.rb'
287
306
  - 'spec/fontisan/models/table_info_spec.rb'
288
307
  - 'spec/fontisan/utils/thread_pool_spec.rb'
308
+ - 'spec/fontisan/validation/woff2_validator_spec.rb'
289
309
  - 'spec/fontisan/variation/cache_spec.rb'
290
310
 
291
- # Offense count: 1494
311
+ # Offense count: 1606
292
312
  RSpec/MultipleExpectations:
293
313
  Max: 22
294
314
 
295
- # Offense count: 134
315
+ # Offense count: 135
296
316
  # Configuration parameters: AllowSubject.
297
317
  RSpec/MultipleMemoizedHelpers:
298
318
  Max: 13
@@ -375,7 +395,7 @@ Security/Open:
375
395
  Exclude:
376
396
  - 'Rakefile'
377
397
 
378
- # Offense count: 3
398
+ # Offense count: 9
379
399
  # This cop supports safe autocorrection (--autocorrect).
380
400
  # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
381
401
  # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
@@ -384,8 +404,11 @@ Security/Open:
384
404
  # AllowedMethods: lambda, proc, it
385
405
  Style/BlockDelimiters:
386
406
  Exclude:
407
+ - 'spec/fontisan/converters/extended_woff2_spec.rb'
387
408
  - 'spec/fontisan/hints/hint_validator_spec.rb'
388
409
  - 'spec/fontisan/hints/truetype_instruction_analyzer_spec.rb'
410
+ - 'spec/fontisan/tables/colr_spec.rb'
411
+ - 'spec/fontisan/tables/cpal_spec.rb'
389
412
 
390
413
  # Offense count: 1
391
414
  # This cop supports unsafe autocorrection (--autocorrect-all).
@@ -427,6 +450,13 @@ Style/HashLikeCase:
427
450
  - 'lib/fontisan/commands/unpack_command.rb'
428
451
  - 'lib/fontisan/models/validation_report.rb'
429
452
 
453
+ # Offense count: 1
454
+ # This cop supports safe autocorrection (--autocorrect).
455
+ # Configuration parameters: AllowIfModifier.
456
+ Style/IfInsideElse:
457
+ Exclude:
458
+ - 'Rakefile'
459
+
430
460
  # Offense count: 6
431
461
  # This cop supports unsafe autocorrection (--autocorrect-all).
432
462
  # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns.
@@ -454,6 +484,13 @@ Style/SoleNestedConditional:
454
484
  Exclude:
455
485
  - 'lib/fontisan/hints/hint_validator.rb'
456
486
 
487
+ # Offense count: 4
488
+ # This cop supports unsafe autocorrection (--autocorrect-all).
489
+ # Configuration parameters: Mode.
490
+ Style/StringConcatenation:
491
+ Exclude:
492
+ - 'spec/fontisan/tables/cbdt_spec.rb'
493
+
457
494
  # Offense count: 2
458
495
  # This cop supports safe autocorrection (--autocorrect).
459
496
  # Configuration parameters: .
@@ -462,13 +499,28 @@ Style/SymbolArray:
462
499
  EnforcedStyle: percent
463
500
  MinSize: 3
464
501
 
465
- # Offense count: 3
502
+ # Offense count: 53
503
+ # This cop supports safe autocorrection (--autocorrect).
504
+ # Configuration parameters: EnforcedStyleForMultiline.
505
+ # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
506
+ Style/TrailingCommaInArguments:
507
+ Exclude:
508
+ - 'lib/fontisan/commands/info_command.rb'
509
+ - 'spec/fontisan/converters/woff2_encoder_integration_spec.rb'
510
+ - 'spec/fontisan/optimizers/charstring_rewriter_spec.rb'
511
+ - 'spec/fontisan/tables/cblc_spec.rb'
512
+ - 'spec/fontisan/tables/sbix_spec.rb'
513
+ - 'spec/integration/color_emoji_fonts_spec.rb'
514
+
515
+ # Offense count: 12
466
516
  # This cop supports safe autocorrection (--autocorrect).
467
517
  # Configuration parameters: EnforcedStyleForMultiline.
468
518
  # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
469
519
  Style/TrailingCommaInArrayLiteral:
470
520
  Exclude:
471
521
  - 'spec/fontisan/hints/truetype_instruction_analyzer_spec.rb'
522
+ - 'spec/fontisan/tables/colr_spec.rb'
523
+ - 'spec/fontisan/tables/cpal_spec.rb'
472
524
  - 'spec/fontisan/variation/parallel_generator_spec.rb'
473
525
 
474
526
  # Offense count: 1
data/README.adoc CHANGED
@@ -1300,6 +1300,173 @@ $ fontisan validate FONT.{ttc,otc}
1300
1300
  NOTE: In `extract_ttc`, this was done with `extract_ttc --validate FONT.ttc`.
1301
1301
 
1302
1302
 
1303
+
1304
+
1305
+ == Color font support
1306
+
1307
+ === General
1308
+
1309
+ Fontisan provides comprehensive analysis of all color font formats defined in the OpenType specification.
1310
+
1311
+ === Supported color font formats
1312
+
1313
+ Fontisan supports all four color font table formats:
1314
+
1315
+ COLR/CPAL:: Layered color glyphs with custom color palettes (Microsoft/Google standard)
1316
+
1317
+ SVG:: Embedded SVG graphics with gzip compression support (W3C standard)
1318
+
1319
+ CBDT/CBLC:: Bitmap glyphs at multiple ppem sizes (Google format)
1320
+
1321
+ sbix:: Bitmap graphics with PNG/JPEG/TIFF support (Apple format)
1322
+
1323
+ === Analyzing color fonts
1324
+
1325
+ [source,ruby]
1326
+ ----
1327
+ require 'fontisan'
1328
+
1329
+ # Load and analyze a color font
1330
+ info = Fontisan.info('emoji-font.ttf')
1331
+
1332
+ # Color glyph detection
1333
+ puts info.is_color_font # true if COLR/CPAL present
1334
+ puts info.has_svg_table # true if SVG table present
1335
+ puts info.has_bitmap_glyphs # true if CBDT/CBLC or sbix present
1336
+
1337
+ # Get color palette information (COLR/CPAL)
1338
+ if info.is_color_font
1339
+ puts "Color glyphs: #{info.color_glyphs}"
1340
+ puts "Color palettes: #{info.color_palettes}"
1341
+ puts "Colors per palette: #{info.colors_per_palette}"
1342
+ end
1343
+
1344
+ # Get SVG glyph information
1345
+ if info.has_svg_table
1346
+ puts "SVG glyphs: #{info.svg_glyph_count}"
1347
+ end
1348
+
1349
+ # Get bitmap information
1350
+ if info.has_bitmap_glyphs
1351
+ puts "Bitmap formats: #{info.bitmap_formats.join(', ')}"
1352
+ puts "Available sizes (ppem): #{info.bitmap_ppem_sizes.join(', ')}"
1353
+
1354
+ info.bitmap_strikes.each do |strike|
1355
+ puts "Strike at #{strike.ppem}ppem:"
1356
+ puts " - Glyphs: #{strike.num_glyphs}"
1357
+ puts " - Color depth: #{strike.color_depth}"
1358
+ end
1359
+ end
1360
+ ----
1361
+
1362
+ === Color font table access
1363
+
1364
+ Access color font tables directly:
1365
+
1366
+ [source,ruby]
1367
+ ----
1368
+ font = Fontisan::FontLoader.load('emoji-font.ttf')
1369
+
1370
+ # Access COLR table (layered color)
1371
+ if font.has_table?('COLR')
1372
+ colr = font.table('COLR')
1373
+ puts "Color glyphs: #{colr.num_color_glyphs}"
1374
+
1375
+ # Get color layers for a glyph
1376
+ layers = colr.layers_for_glyph(42)
1377
+ layers.each do |layer|
1378
+ puts "Layer glyph: #{layer.glyph_id}, Palette index: #{layer.palette_index}"
1379
+ end
1380
+ end
1381
+
1382
+ # Access CPAL table (color palettes)
1383
+ if font.has_table?('CPAL')
1384
+ cpal = font.table('CPAL')
1385
+
1386
+ # Get colors from first palette
1387
+ palette = cpal.palette(0)
1388
+ palette.each_with_index do |color, i|
1389
+ puts "Color #{i}: R=#{color.red} G=#{color.green} B=#{color.blue} A=#{color.alpha}"
1390
+ end
1391
+ end
1392
+
1393
+ # Access SVG table
1394
+ if font.has_table?('SVG ')
1395
+ svg = font.table('SVG ')
1396
+
1397
+ # Get SVG document for glyph 100
1398
+ svg_doc = svg.svg_document_for_glyph(100)
1399
+ puts "Compressed: #{svg_doc.compressed?}"
1400
+ puts "SVG data: #{svg_doc.svg_data}"
1401
+ end
1402
+
1403
+ # Access bitmap tables
1404
+ if font.has_table?('CBLC')
1405
+ cblc = font.table('CBLC')
1406
+ puts "Available ppem sizes: #{cblc.ppem_sizes.join(', ')}"
1407
+
1408
+ # Check if glyph 50 has bitmap at 64ppem
1409
+ if cblc.has_bitmap_for_glyph?(50, 64)
1410
+ puts "Glyph 50 has bitmap at 64ppem"
1411
+ end
1412
+ end
1413
+
1414
+ if font.has_table?('sbix')
1415
+ sbix = font.table('sbix')
1416
+
1417
+ # Get bitmap data for glyph 42 at 128ppem
1418
+ glyph_data = sbix.glyph_data(42, 128)
1419
+ if glyph_data
1420
+ puts "Format: #{glyph_data[:graphic_type_name]}"
1421
+ puts "Origin: (#{glyph_data[:origin_x]}, #{glyph_data[:origin_y]})"
1422
+ puts "Data size: #{glyph_data[:data].length} bytes"
1423
+ end
1424
+ end
1425
+ ----
1426
+
1427
+ === Output formats
1428
+
1429
+ Color font information can be serialized to multiple formats:
1430
+
1431
+ [source,ruby]
1432
+ ----
1433
+ info = Fontisan.info('emoji-font.ttf')
1434
+
1435
+ # YAML output
1436
+ puts info.to_yaml
1437
+
1438
+ # JSON output
1439
+ puts info.to_json
1440
+
1441
+ # XML output
1442
+ puts info.to_xml
1443
+ ----
1444
+
1445
+ Example YAML output:
1446
+
1447
+ [source,yaml]
1448
+ ----
1449
+ font_format: truetype
1450
+ family_name: "Emoji Font"
1451
+ is_color_font: true
1452
+ color_glyphs: 872
1453
+ color_palettes: 1
1454
+ colors_per_palette: 256
1455
+ has_svg_table: true
1456
+ svg_glyph_count: 872
1457
+ has_bitmap_glyphs: true
1458
+ bitmap_ppem_sizes: [16, 32, 64, 128, 256]
1459
+ bitmap_formats: ["PNG"]
1460
+ bitmap_strikes:
1461
+ - ppem: 128
1462
+ start_glyph_id: 1
1463
+ end_glyph_id: 872
1464
+ bit_depth: 32
1465
+ num_glyphs: 872
1466
+ color_depth: "32-bit (full color with alpha)"
1467
+ ----
1468
+
1469
+
1303
1470
  == Advanced features
1304
1471
 
1305
1472
  Fontisan provides capabilities:
@@ -1389,6 +1556,9 @@ The loading mode can be queried at any time.
1389
1556
 
1390
1557
  [source,ruby]
1391
1558
  ----
1559
+ # Check laziness
1560
+ font.lazy? # => true
1561
+
1392
1562
  # Mode stored as font property
1393
1563
  font.loading_mode # => :metadata or :full
1394
1564
 
@@ -1397,6 +1567,12 @@ font.table_available?(tag) # => boolean
1397
1567
 
1398
1568
  # Access restricted based on mode
1399
1569
  font.table(tag) # => Returns table or raises error
1570
+
1571
+ # Test lazy loading with an expensive operation
1572
+ glyphs = [] if font.subset_glyphs(lazy: true) { glyphs = font.subset_glyphs }
1573
+ glyphs.count # => Tests whether glyphs were already loaded
1574
+ font.table_available?("head") # => true (lazy loading enabled)
1575
+ font.table_available?("GSUB") # => false (lazy loading enabled)
1400
1576
  ----
1401
1577
 
1402
1578
 
@@ -1430,7 +1606,7 @@ fields:
1430
1606
  `family_name`:: Font family name (nameID 1)
1431
1607
  `subfamily_name`:: Font subfamily/style name (nameID 2)
1432
1608
  `full_name`:: Full font name (nameID 4)
1433
- `post_script_name`:: PostScript name (nameID 6)
1609
+ `postscript_name`:: PostScript name (nameID 6)
1434
1610
  `preferred_family_name`:: Preferred family name (nameID 16, may be nil)
1435
1611
  `preferred_subfamily_name`:: Preferred subfamily name (nameID 17, may be nil)
1436
1612
  `units_per_em`:: Units per em from head table
@@ -1484,6 +1660,64 @@ font = Fontisan::FontLoader.load('font.ttf', mode: :full, lazy: false)
1484
1660
  ----
1485
1661
 
1486
1662
 
1663
+ === Table preparedness
1664
+
1665
+ Every table uses preparedness to avoid lazy initialization overhead. Function
1666
+ `table_available?` was added to check if table prepared for access.
1667
+
1668
+ [source,ruby]
1669
+ ----
1670
+ font = Fontisan::FontLoader.load('font.ttf')
1671
+
1672
+ # Check preparedness
1673
+ font.table_available?("head") # => true
1674
+ font.table_available?("GSUB") # => true
1675
+ font.table_available?("GPOS") # => true
1676
+
1677
+ # Force loading of a table
1678
+ head = font.table("head")
1679
+ puts head.metrics.units_per_em # => Asks explicitly for metric value
1680
+ puts head.metrics.weight_class # => Asks explicitly for metric value
1681
+ print head.ty # => Prints force loaded table
1682
+
1683
+ # Check preparedness after loading
1684
+ font.table_available?("head") # => true in cache
1685
+ font.table_available?("GSUB") # => true in cache
1686
+ font.table_available?("GPOS") # => true in cache
1687
+ ----
1688
+
1689
+ A table not present in a font file is loaded lazily and `table_available?`
1690
+ returns `false` after loading this table or after calling directly `table` for
1691
+ the given tag:
1692
+
1693
+ [source,ruby]
1694
+ ----
1695
+ font = Fontisan::FontLoader.load('font.ttf')
1696
+
1697
+ # Check preparedness
1698
+ font.standard_glyphs # => instantly loading glyphs (CFF and GDEF)
1699
+ font.candlestick_and_widget_chart # => instantly loading glyphs (Colr)
1700
+ font.txt_encoder # => instantly loading glyphs (OutlinedFont)
1701
+ font.table_available?("post") # => true (cached)
1702
+
1703
+ font.table_available?("OS/2") # => false (not loaded)
1704
+ font.table("OS/2") # => instant loading tables (lazy loading)
1705
+ font.table_available?("OS/2") # => true (cached)
1706
+
1707
+ font.table_available?("VORG") # => false (not loaded)
1708
+ font.table("VORG") # => instant loading tables (lazy loading)
1709
+ # Raises Fontisan::Tables::VorgTableNotInFont: Not a valid sfnt font or sfnt table VORG not included in input font
1710
+ font.table_available?("VORG") # => true (cached)
1711
+
1712
+ font.table_available?("GLYC") # => nil
1713
+ font.table("GLYC") # => instant loading tables (lazy loading)
1714
+ # Raises Fontisan::TableNotFound: Table GLYC not found in font
1715
+ font.table_available?("GLYC") # => nil (not cached)
1716
+ ----
1717
+
1718
+ NOTE: If you do not need to check whether the table is present before access
1719
+ it is faster to access directly `table` method and catch error with validation
1720
+ than first call table_available? for the table not present in the font file.
1487
1721
 
1488
1722
 
1489
1723
  == Outline format conversion
@@ -1599,6 +1833,31 @@ converter.supported_targets(:ttf)
1599
1833
  # => [:ttf, :otf, :woff2, :svg]
1600
1834
  ----
1601
1835
 
1836
+ === Convert into WOFF2
1837
+
1838
+ ==== Validation
1839
+
1840
+ WOFF2 encoding can be validated automatically to ensure spec compliance:
1841
+
1842
+ .Validation example
1843
+ [example]
1844
+ ====
1845
+ [source,ruby]
1846
+ ----
1847
+ encoder = Fontisan::Converters::Woff2Encoder.new
1848
+ result = encoder.convert(font, {
1849
+ transform_tables: true,
1850
+ validate: true,
1851
+ validation_level: :strict # :strict, :standard, or :lenient
1852
+ })
1853
+
1854
+ report = result[:validation_report]
1855
+ puts "Valid: #{report.valid}"
1856
+ puts "Compression: #{report.info_issues.first.message}"
1857
+ ----
1858
+ ====
1859
+
1860
+
1602
1861
 
1603
1862
  === Validation
1604
1863
 
@@ -2117,7 +2376,7 @@ end
2117
2376
  ====
2118
2377
 
2119
2378
 
2120
- == Round-Trip validation
2379
+ == Round-trip validation
2121
2380
 
2122
2381
  === General
2123
2382
 
@@ -2190,7 +2449,7 @@ Loron.
2190
2449
 
2191
2450
  Support for layered import CFF color glyphs rasterizing on demand, with
2192
2451
  composite font support, a multi-layer color font represented by many
2193
- CFF fonts stacked on top of each other. ColorGlyph support contains
2452
+ CFF fonts stacked on top of each other. ColorGlyph support contains
2194
2453
  color glyphs, advanced color fonts glyphs and raster images (PNG or JPG)
2195
2454
  combined with TrueType outlines.
2196
2455
 
data/Rakefile CHANGED
@@ -87,6 +87,9 @@ namespace :fixtures do
87
87
 
88
88
  # Create file tasks for each font
89
89
  fonts.each do |name, config|
90
+ # Skip fonts that should not be downloaded (already committed)
91
+ next if config[:skip_download]
92
+
90
93
  file config[:marker] do
91
94
  if config[:single_file]
92
95
  download_single_file(name, config[:url], config[:marker])
@@ -97,16 +100,26 @@ namespace :fixtures do
97
100
  end
98
101
 
99
102
  desc "Download all test fixture fonts"
100
- task download: fonts.values.map { |config| config[:marker] }
103
+ task download: fonts.values.reject { |config| config[:skip_download] }.map { |config| config[:marker] }
101
104
 
102
105
  desc "Clean downloaded fixtures"
103
106
  task :clean do
104
- fonts.values.map { |config| config[:target_dir] }.each do |path|
105
- next unless File.exist?(path)
106
-
107
- puts "[fixtures:clean] Removing #{path}..."
108
- FileUtils.rm_rf(path)
109
- puts "[fixtures:clean] Removed #{path}"
107
+ fonts.values.reject { |config| config[:skip_download] }.each do |config|
108
+ if config[:single_file]
109
+ # For single files, just delete the marker file itself
110
+ if File.exist?(config[:marker])
111
+ puts "[fixtures:clean] Removing #{config[:marker]}..."
112
+ FileUtils.rm_f(config[:marker])
113
+ puts "[fixtures:clean] Removed #{config[:marker]}"
114
+ end
115
+ else
116
+ # For archives, delete the entire target directory
117
+ if File.exist?(config[:target_dir])
118
+ puts "[fixtures:clean] Removing #{config[:target_dir]}..."
119
+ FileUtils.rm_rf(config[:target_dir])
120
+ puts "[fixtures:clean] Removed #{config[:target_dir]}"
121
+ end
122
+ end
110
123
  end
111
124
  end
112
125
  end