fontisan 0.4.22 → 0.4.24

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +240 -24
  3. data/Gemfile +6 -0
  4. data/Rakefile +9 -6
  5. data/benchmark/compile_benchmark.rb +13 -5
  6. data/docs/.vitepress/config.ts +1 -0
  7. data/docs/STITCHER_GUIDE.adoc +47 -3
  8. data/docs/api/layer.md +89 -0
  9. data/docs/cli/subset.md +25 -6
  10. data/docs/guide/cli/subset.md +31 -0
  11. data/docs/guide/color-fonts/bitmaps.md +17 -0
  12. data/lib/fontisan/cli.rb +2 -1
  13. data/lib/fontisan/collection/writer.rb +2 -1
  14. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  15. data/lib/fontisan/commands.rb +2 -1
  16. data/lib/fontisan/config/subset_profiles.yml +2 -0
  17. data/lib/fontisan/converters/collection_converter.rb +6 -1
  18. data/lib/fontisan/converters/svg_generator.rb +3 -1
  19. data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
  20. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  21. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  22. data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
  23. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  24. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  25. data/lib/fontisan/stitcher/source.rb +2 -1
  26. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  27. data/lib/fontisan/stitcher.rb +4 -1
  28. data/lib/fontisan/subset/shared_state.rb +42 -0
  29. data/lib/fontisan/subset/subset_context.rb +20 -0
  30. data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
  31. data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
  32. data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
  33. data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
  34. data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
  35. data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
  36. data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
  37. data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
  38. data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
  39. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +146 -0
  40. data/lib/fontisan/subset/table_strategy/head.rb +42 -0
  41. data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
  42. data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
  43. data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
  44. data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
  45. data/lib/fontisan/subset/table_strategy/name.rb +19 -0
  46. data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
  47. data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
  48. data/lib/fontisan/subset/table_strategy/post.rb +37 -0
  49. data/lib/fontisan/subset/table_strategy.rb +75 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +49 -616
  51. data/lib/fontisan/subset.rb +3 -0
  52. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  53. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  54. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  55. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  56. data/lib/fontisan/tables/cbdt.rb +66 -121
  57. data/lib/fontisan/tables/cblc.rb +110 -231
  58. data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
  59. data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
  60. data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
  61. data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
  62. data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
  63. data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
  64. data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
  65. data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
  66. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  67. data/lib/fontisan/tables/cff.rb +2 -1
  68. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  69. data/lib/fontisan/tables.rb +12 -0
  70. data/lib/fontisan/ufo/cli.rb +4 -1
  71. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  72. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  73. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  74. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  75. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  76. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  77. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  78. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  79. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  80. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  81. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  82. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  83. data/lib/fontisan/ufo/compile/head.rb +2 -1
  84. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  85. data/lib/fontisan/ufo/compile/name.rb +3 -1
  86. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  87. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  88. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  89. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  90. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  91. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  92. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  93. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  94. data/lib/fontisan/ufo/convert.rb +4 -1
  95. data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
  96. data/lib/fontisan/ufo/info.rb +4 -2
  97. data/lib/fontisan/ufo/layer.rb +42 -2
  98. data/lib/fontisan/ufo/plist.rb +7 -2
  99. data/lib/fontisan/ufo/reader.rb +3 -1
  100. data/lib/fontisan/ufo/writer.rb +13 -5
  101. data/lib/fontisan/ufo.rb +2 -1
  102. data/lib/fontisan/version.rb +1 -1
  103. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  104. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  105. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  106. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  107. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  108. data/lib/fontisan.rb +0 -1
  109. metadata +36 -4
  110. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  111. data/lib/fontisan/tasks.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 208fa8e2c44bdea2e800ecb13639d50e06e6914406822f6e3c92152d21414fd7
4
- data.tar.gz: cdce57d3a157d5a5549b45104f93de38263697989bae6a66572907bc54b58bb0
3
+ metadata.gz: 556befd056a62c96c03bfaab023a7027b098df4145148415f4735db53da719ee
4
+ data.tar.gz: 41ddfe5df546ee2b1855644d0eddb1caed472f61e88030ad653038e536393ee6
5
5
  SHA512:
6
- metadata.gz: c87e9ec10e9cadd168dda76b38ef225c1e286bc63501b75268a7cd29fcdfbef3d86abacac211052d529d153aebf1f8db29aaab7445c51a5a83e3eb091f752ab4
7
- data.tar.gz: 4632d882fe8ab4897ed32722ec17efb942819ded33e2005bf51c3032384466164d2c2c09a1a8f4388f1889b5c169f42b9dc14ae5cafce72ef5680c94d2333c02
6
+ metadata.gz: 057fc2598f2ad7a02b1847d502f6c0363f6f23382806de361a5dd87bb2b4c99d00993f2e272ab3f2c35697dc0b8843235df05301047b1c3cdbf0e0147498282d
7
+ data.tar.gz: 1669c95792cc2a7731f4495e2d91e2e89b545412e213f8aff78e0dfc625a60c9f9403dc9960e4e487da1563e794fb2d3a6d5126f4ab6ef35e6ebe42e9950fd5d
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-07-08 08:05:45 UTC using RuboCop version 1.86.1.
3
+ # on 2026-07-09 14:55:09 UTC using RuboCop version 1.86.1.
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,20 +12,190 @@ Gemspec/RequiredRubyVersion:
12
12
  - 'docs/node_modules/speakingurl/speakingurl-rails.gemspec'
13
13
  - 'fontisan.gemspec'
14
14
 
15
- # Offense count: 2116
15
+ # Offense count: 276
16
+ # This cop supports safe autocorrection (--autocorrect).
17
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
18
+ # SupportedStyles: with_first_argument, with_fixed_indentation
19
+ Layout/ArgumentAlignment:
20
+ Enabled: false
21
+
22
+ # Offense count: 22
23
+ # This cop supports safe autocorrection (--autocorrect).
24
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
25
+ # SupportedStyles: with_first_element, with_fixed_indentation
26
+ Layout/ArrayAlignment:
27
+ Exclude:
28
+ - 'lib/fontisan/collection/writer.rb'
29
+ - 'lib/fontisan/tables/cff2/index_builder.rb'
30
+ - 'lib/fontisan/ufo/compile/cbdt_cblc.rb'
31
+ - 'lib/fontisan/ufo/compile/cff.rb'
32
+ - 'lib/fontisan/ufo/compile/cmap.rb'
33
+ - 'lib/fontisan/ufo/compile/cpal.rb'
34
+ - 'lib/fontisan/ufo/compile/gvar.rb'
35
+ - 'lib/fontisan/ufo/compile/item_variation_store.rb'
36
+ - 'lib/fontisan/ufo/compile/sbix.rb'
37
+ - 'lib/fontisan/ufo/compile/stat.rb'
38
+ - 'lib/fontisan/ufo/info.rb'
39
+ - 'spec/fontisan/stitcher/glyph_signature_spec.rb'
40
+ - 'spec/fontisan/svg_to_glyf/contour_builder_spec.rb'
41
+ - 'spec/fontisan/ufo/compile/feature_writers/mkmk_spec.rb'
42
+ - 'spec/fontisan/ufo/compile/filters/transformations_spec.rb'
43
+
44
+ # Offense count: 1
45
+ # This cop supports safe autocorrection (--autocorrect).
46
+ # Configuration parameters: IndentationWidth.
47
+ Layout/AssignmentIndentation:
48
+ Exclude:
49
+ - 'lib/fontisan/woff2/glyf_loca_reconstruct.rb'
50
+
51
+ # Offense count: 58
52
+ # This cop supports safe autocorrection (--autocorrect).
53
+ # Configuration parameters: EnforcedStyleAlignWith.
54
+ # SupportedStylesAlignWith: either, start_of_block, start_of_line
55
+ Layout/BlockAlignment:
56
+ Enabled: false
57
+
58
+ # Offense count: 55
59
+ # This cop supports safe autocorrection (--autocorrect).
60
+ Layout/BlockEndNewline:
61
+ Enabled: false
62
+
63
+ # Offense count: 29
64
+ # This cop supports safe autocorrection (--autocorrect).
65
+ Layout/ClosingParenthesisIndentation:
66
+ Exclude:
67
+ - 'benchmark/compile_benchmark.rb'
68
+ - 'lib/fontisan/ufo/compile/fvar.rb'
69
+ - 'lib/fontisan/ufo/writer.rb'
70
+ - 'spec/fontisan/stitcher/deduplicator_spec.rb'
71
+ - 'spec/fontisan/stitcher/include_codepoints_map_spec.rb'
72
+ - 'spec/fontisan/svg_to_glyf/affine_transform_spec.rb'
73
+ - 'spec/fontisan/ufo/compile/filters/propagate_anchors_spec.rb'
74
+ - 'spec/fontisan/ufo/compile_gvar_spec.rb'
75
+ - 'spec/fontisan/ufo/compile_item_variation_store_spec.rb'
76
+ - 'spec/fontisan/ufo/filters_spec.rb'
77
+
78
+ # Offense count: 2
79
+ # This cop supports safe autocorrection (--autocorrect).
80
+ # Configuration parameters: AllowForAlignment.
81
+ Layout/CommentIndentation:
82
+ Exclude:
83
+ - 'lib/fontisan/ufo/compile/cff.rb'
84
+
85
+ # Offense count: 4
86
+ # This cop supports safe autocorrection (--autocorrect).
87
+ Layout/ElseAlignment:
88
+ Exclude:
89
+ - 'lib/fontisan/commands/validate_collection_command.rb'
90
+ - 'lib/fontisan/converters/collection_converter.rb'
91
+ - 'spec/fontisan/woff2/sfnt_checksum_spec.rb'
92
+
93
+ # Offense count: 4
94
+ # This cop supports safe autocorrection (--autocorrect).
95
+ # Configuration parameters: EnforcedStyleAlignWith.
96
+ # SupportedStylesAlignWith: keyword, variable, start_of_line
97
+ Layout/EndAlignment:
98
+ Exclude:
99
+ - 'lib/fontisan/commands/validate_collection_command.rb'
100
+ - 'lib/fontisan/converters/collection_converter.rb'
101
+ - 'spec/fontisan/woff2/sfnt_checksum_spec.rb'
102
+
103
+ # Offense count: 29
104
+ # This cop supports safe autocorrection (--autocorrect).
105
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
106
+ # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
107
+ Layout/FirstArgumentIndentation:
108
+ Exclude:
109
+ - 'benchmark/compile_benchmark.rb'
110
+ - 'lib/fontisan/ufo/compile/fvar.rb'
111
+ - 'lib/fontisan/ufo/writer.rb'
112
+ - 'spec/fontisan/stitcher/deduplicator_spec.rb'
113
+ - 'spec/fontisan/stitcher/include_codepoints_map_spec.rb'
114
+ - 'spec/fontisan/svg_to_glyf/affine_transform_spec.rb'
115
+ - 'spec/fontisan/ufo/compile/filters/propagate_anchors_spec.rb'
116
+ - 'spec/fontisan/ufo/compile_gvar_spec.rb'
117
+ - 'spec/fontisan/ufo/compile_item_variation_store_spec.rb'
118
+ - 'spec/fontisan/ufo/filters_spec.rb'
119
+
120
+ # Offense count: 14
121
+ # This cop supports safe autocorrection (--autocorrect).
122
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
123
+ # SupportedStyles: special_inside_parentheses, consistent, align_brackets
124
+ Layout/FirstArrayElementIndentation:
125
+ Exclude:
126
+ - 'spec/fontisan/stitcher/glyph_signature_spec.rb'
127
+ - 'spec/fontisan/ufo/compile/feature_writers/mark_spec.rb'
128
+
129
+ # Offense count: 194
130
+ # This cop supports safe autocorrection (--autocorrect).
131
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
132
+ # SupportedHashRocketStyles: key, separator, table
133
+ # SupportedColonStyles: key, separator, table
134
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
135
+ Layout/HashAlignment:
136
+ Enabled: false
137
+
138
+ # Offense count: 1
139
+ # This cop supports safe autocorrection (--autocorrect).
140
+ # Configuration parameters: EnforcedStyle.
141
+ # SupportedStyles: normal, indented_internal_methods
142
+ Layout/IndentationConsistency:
143
+ Exclude:
144
+ - 'lib/fontisan/ufo/compile/cff.rb'
145
+
146
+ # Offense count: 121
147
+ # This cop supports safe autocorrection (--autocorrect).
148
+ # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
149
+ # SupportedStylesAlignWith: start_of_line, relative_to_receiver
150
+ Layout/IndentationWidth:
151
+ Enabled: false
152
+
153
+ # Offense count: 2235
16
154
  # This cop supports safe autocorrection (--autocorrect).
17
155
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
18
156
  # URISchemes: http, https
19
157
  Layout/LineLength:
20
158
  Enabled: false
21
159
 
160
+ # Offense count: 7
161
+ # This cop supports safe autocorrection (--autocorrect).
162
+ # Configuration parameters: EnforcedStyle.
163
+ # SupportedStyles: symmetrical, new_line, same_line
164
+ Layout/MultilineArrayBraceLayout:
165
+ Exclude:
166
+ - 'spec/fontisan/stitcher/glyph_signature_spec.rb'
167
+ - 'spec/fontisan/ufo/compile/feature_writers/mark_spec.rb'
168
+
169
+ # Offense count: 29
170
+ # This cop supports safe autocorrection (--autocorrect).
171
+ # Configuration parameters: EnforcedStyle.
172
+ # SupportedStyles: symmetrical, new_line, same_line
173
+ Layout/MultilineMethodCallBraceLayout:
174
+ Exclude:
175
+ - 'benchmark/compile_benchmark.rb'
176
+ - 'lib/fontisan/ufo/compile/fvar.rb'
177
+ - 'lib/fontisan/ufo/writer.rb'
178
+ - 'spec/fontisan/stitcher/deduplicator_spec.rb'
179
+ - 'spec/fontisan/stitcher/include_codepoints_map_spec.rb'
180
+ - 'spec/fontisan/svg_to_glyf/affine_transform_spec.rb'
181
+ - 'spec/fontisan/ufo/compile/filters/propagate_anchors_spec.rb'
182
+ - 'spec/fontisan/ufo/compile_gvar_spec.rb'
183
+ - 'spec/fontisan/ufo/compile_item_variation_store_spec.rb'
184
+ - 'spec/fontisan/ufo/filters_spec.rb'
185
+
186
+ # Offense count: 225
187
+ # This cop supports safe autocorrection (--autocorrect).
188
+ # Configuration parameters: AllowInHeredoc.
189
+ Layout/TrailingWhitespace:
190
+ Enabled: false
191
+
22
192
  # Offense count: 2
23
193
  Lint/CopDirectiveSyntax:
24
194
  Exclude:
25
195
  - 'lib/fontisan/woff2_font.rb'
26
196
  - 'spec/fontisan/variation/cache_spec.rb'
27
197
 
28
- # Offense count: 52
198
+ # Offense count: 54
29
199
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
30
200
  Lint/DuplicateBranch:
31
201
  Enabled: false
@@ -78,16 +248,12 @@ Lint/StructNewOverride:
78
248
  Exclude:
79
249
  - 'lib/fontisan/optimizers/pattern_analyzer.rb'
80
250
 
81
- # Offense count: 5
251
+ # Offense count: 24
82
252
  # This cop supports safe autocorrection (--autocorrect).
83
253
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
84
254
  # NotImplementedExceptions: NotImplementedError
85
255
  Lint/UnusedMethodArgument:
86
- Exclude:
87
- - 'lib/fontisan.rb'
88
- - 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb'
89
- - 'lib/fontisan/variation/table_accessor.rb'
90
- - 'lib/fontisan/woff_font.rb'
256
+ Enabled: false
91
257
 
92
258
  # Offense count: 5
93
259
  Lint/UselessConstantScoping:
@@ -95,12 +261,12 @@ Lint/UselessConstantScoping:
95
261
  - 'lib/fontisan/conversion_options.rb'
96
262
  - 'lib/fontisan/type1/charstrings.rb'
97
263
 
98
- # Offense count: 705
264
+ # Offense count: 715
99
265
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
100
266
  Metrics/AbcSize:
101
267
  Enabled: false
102
268
 
103
- # Offense count: 36
269
+ # Offense count: 34
104
270
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
105
271
  # AllowedMethods: refine
106
272
  Metrics/BlockLength:
@@ -118,28 +284,28 @@ Metrics/CollectionLiteralLength:
118
284
  - 'lib/fontisan/type1/agl.rb'
119
285
  - 'lib/fontisan/type1/encodings.rb'
120
286
 
121
- # Offense count: 366
287
+ # Offense count: 367
122
288
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
123
289
  Metrics/CyclomaticComplexity:
124
290
  Enabled: false
125
291
 
126
- # Offense count: 978
292
+ # Offense count: 990
127
293
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
128
294
  Metrics/MethodLength:
129
295
  Max: 135
130
296
 
131
- # Offense count: 27
297
+ # Offense count: 30
132
298
  # Configuration parameters: CountKeywordArgs.
133
299
  Metrics/ParameterLists:
134
300
  Max: 39
135
301
  MaxOptionalParameters: 4
136
302
 
137
- # Offense count: 278
303
+ # Offense count: 279
138
304
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
139
305
  Metrics/PerceivedComplexity:
140
306
  Enabled: false
141
307
 
142
- # Offense count: 208
308
+ # Offense count: 202
143
309
  # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
144
310
  # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
145
311
  Naming/MethodParameterName:
@@ -182,7 +348,7 @@ Naming/VariableName:
182
348
  - 'lib/fontisan/type1/pfm_generator.rb'
183
349
  - 'lib/fontisan/type1/pfm_parser.rb'
184
350
 
185
- # Offense count: 120
351
+ # Offense count: 124
186
352
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
187
353
  # SupportedStyles: snake_case, normalcase, non_integer
188
354
  # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
@@ -199,13 +365,13 @@ RSpec/AnyInstance:
199
365
  - 'spec/fontisan/variation/parallel_generator_spec.rb'
200
366
  - 'spec/integration/format_conversion_spec.rb'
201
367
 
202
- # Offense count: 127
368
+ # Offense count: 128
203
369
  # Configuration parameters: Prefixes, AllowedPatterns.
204
370
  # Prefixes: when, with, without
205
371
  RSpec/ContextWording:
206
372
  Enabled: false
207
373
 
208
- # Offense count: 38
374
+ # Offense count: 39
209
375
  # Configuration parameters: IgnoredMetadata.
210
376
  RSpec/DescribeClass:
211
377
  Enabled: false
@@ -221,7 +387,7 @@ RSpec/DescribeMethod:
221
387
  - 'spec/fontisan/type1/seac_expander_spec.rb'
222
388
  - 'spec/fontisan/type1_real_fonts_spec.rb'
223
389
 
224
- # Offense count: 1673
390
+ # Offense count: 1710
225
391
  # Configuration parameters: CountAsOne.
226
392
  RSpec/ExampleLength:
227
393
  Max: 66
@@ -273,7 +439,7 @@ RSpec/IteratedExpectation:
273
439
  Exclude:
274
440
  - 'spec/fontisan/tables/glyf_spec.rb'
275
441
 
276
- # Offense count: 25
442
+ # Offense count: 27
277
443
  # Configuration parameters: EnforcedStyle.
278
444
  # SupportedStyles: have_received, receive
279
445
  RSpec/MessageSpies:
@@ -289,6 +455,7 @@ RSpec/MessageSpies:
289
455
  - 'spec/fontisan/subset/builder_spec.rb'
290
456
  - 'spec/fontisan/variation/instance_writer_spec.rb'
291
457
  - 'spec/fontisan/woff2_font_spec.rb'
458
+ - 'spec/support/fixture_downloader_spec.rb'
292
459
 
293
460
  # Offense count: 4
294
461
  RSpec/MultipleDescribes:
@@ -298,7 +465,7 @@ RSpec/MultipleDescribes:
298
465
  - 'spec/fontisan/utils/thread_pool_spec.rb'
299
466
  - 'spec/fontisan/variation/cache_spec.rb'
300
467
 
301
- # Offense count: 2105
468
+ # Offense count: 2126
302
469
  RSpec/MultipleExpectations:
303
470
  Max: 19
304
471
 
@@ -330,7 +497,7 @@ RSpec/RepeatedExample:
330
497
  - 'spec/fontisan/metrics_calculator_spec.rb'
331
498
  - 'spec/fontisan/tables/os2_spec.rb'
332
499
 
333
- # Offense count: 48
500
+ # Offense count: 51
334
501
  # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
335
502
  # SupportedInflectors: default, active_support
336
503
  RSpec/SpecFilePathFormat:
@@ -341,10 +508,11 @@ RSpec/SpecFilePathSuffix:
341
508
  Exclude:
342
509
  - 'spec/benchmarks/subroutine_optimization_benchmark.rb'
343
510
 
344
- # Offense count: 1
511
+ # Offense count: 2
345
512
  RSpec/StubbedMock:
346
513
  Exclude:
347
514
  - 'spec/fontisan/woff2_font_spec.rb'
515
+ - 'spec/support/fixture_downloader_spec.rb'
348
516
 
349
517
  # Offense count: 10
350
518
  RSpec/SubjectStub:
@@ -361,6 +529,16 @@ Rake/MethodDefinitionInTask:
361
529
  Exclude:
362
530
  - 'Rakefile'
363
531
 
532
+ # Offense count: 68
533
+ # This cop supports safe autocorrection (--autocorrect).
534
+ # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
535
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
536
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
537
+ # FunctionalMethods: let, let!, subject, watch
538
+ # AllowedMethods: lambda, proc, it
539
+ Style/BlockDelimiters:
540
+ Enabled: false
541
+
364
542
  # Offense count: 6
365
543
  # This cop supports safe autocorrection (--autocorrect).
366
544
  Style/ComparableClamp:
@@ -395,9 +573,47 @@ Style/HashLikeCase:
395
573
  - 'lib/fontisan/commands/unpack_command.rb'
396
574
  - 'lib/fontisan/models/validation_report.rb'
397
575
 
576
+ # Offense count: 16
577
+ # This cop supports safe autocorrection (--autocorrect).
578
+ Style/MultilineIfModifier:
579
+ Exclude:
580
+ - 'lib/fontisan/svg/standalone_glyph.rb'
581
+ - 'lib/fontisan/svg_to_glyf/geometry/transform_parser.rb'
582
+ - 'lib/fontisan/ufo/cli.rb'
583
+ - 'lib/fontisan/ufo/convert.rb'
584
+ - 'lib/fontisan/ufo/convert/from_bin_data.rb'
585
+ - 'lib/fontisan/ufo/convert/to_dfont.rb'
586
+ - 'lib/fontisan/ufo/convert/to_otc.rb'
587
+ - 'lib/fontisan/ufo/convert/to_postscript.rb'
588
+ - 'lib/fontisan/ufo/convert/to_woff.rb'
589
+ - 'lib/fontisan/ufo/convert/to_woff2.rb'
590
+ - 'lib/fontisan/ufo/plist.rb'
591
+ - 'lib/fontisan/woff2/collection_decoder.rb'
592
+ - 'lib/fontisan/woff2/collection_encoder.rb'
593
+ - 'spec/support/fixture_downloader.rb'
594
+
595
+ # Offense count: 4
596
+ # This cop supports safe autocorrection (--autocorrect).
597
+ Style/MultilineTernaryOperator:
598
+ Exclude:
599
+ - 'lib/fontisan/commands/validate_collection_command.rb'
600
+ - 'lib/fontisan/converters/collection_converter.rb'
601
+ - 'spec/fontisan/woff2/sfnt_checksum_spec.rb'
602
+
398
603
  # Offense count: 1
399
604
  # Configuration parameters: AllowedMethods.
400
605
  # AllowedMethods: respond_to_missing?
401
606
  Style/OptionalBooleanParameter:
402
607
  Exclude:
403
608
  - 'lib/fontisan/conversion_options.rb'
609
+
610
+ # Offense count: 22
611
+ # This cop supports safe autocorrection (--autocorrect).
612
+ # Configuration parameters: EnforcedStyleForMultiline.
613
+ # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
614
+ Style/TrailingCommaInArguments:
615
+ Exclude:
616
+ - 'benchmark/compile_benchmark.rb'
617
+ - 'spec/fontisan/ufo/compile/filters/propagate_anchors_spec.rb'
618
+ - 'spec/fontisan/ufo/compile_gvar_spec.rb'
619
+ - 'spec/fontisan/ufo/filters_spec.rb'
data/Gemfile CHANGED
@@ -10,6 +10,12 @@ gem "benchmark"
10
10
  gem "bigdecimal"
11
11
  gem "canon", "~> 0.1.3"
12
12
  gem "get_process_mem", "~> 0.2"
13
+ # Octokit provides authenticated GitHub API access for the test-only
14
+ # fixture downloader (spec/support/fixture_downloader.rb). CI sets
15
+ # GITHUB_TOKEN, the downloader auto-detects it and routes GitHub URLs
16
+ # through Octokit to lift the 60/hr anonymous rate limit. NOT in
17
+ # fontisan.gemspec — production installs never pull this in.
18
+ gem "octokit", group: :test
13
19
  gem "openssl", "~> 3.0"
14
20
  gem "rake"
15
21
  gem "rspec"
data/Rakefile CHANGED
@@ -10,21 +10,24 @@ RuboCop::RakeTask.new
10
10
 
11
11
  # rubocop:disable Metrics/BlockLength
12
12
  namespace :fixtures do
13
- # Load centralized fixture configuration
13
+ # Load centralized fixture configuration and the test-only downloader.
14
+ # The downloader lives under spec/support (not lib/) because it is
15
+ # purely test infrastructure — fontisan-the-gem never downloads
16
+ # fonts at runtime.
14
17
  require_relative "spec/support/fixture_fonts"
15
- require "fontisan/tasks"
18
+ require_relative "spec/support/fixture_downloader"
16
19
 
17
20
  # Helper method to download a single file
18
21
  def download_single_file(name, url, target_path)
19
22
  puts "[fixtures:download] Downloading #{name}..."
20
23
 
21
- Fontisan::Tasks::FixtureDownloader.new(
24
+ FixtureFonts::Downloader.new(
22
25
  url: url,
23
26
  destination: target_path,
24
27
  ).call
25
28
 
26
29
  puts "[fixtures:download] #{name} downloaded successfully"
27
- rescue Fontisan::Tasks::FixtureDownloader::Error => e
30
+ rescue FixtureFonts::Downloader::Error => e
28
31
  warn "[fixtures:download] #{name} failed after retries: #{e.message}"
29
32
  raise
30
33
  end
@@ -41,7 +44,7 @@ namespace :fixtures do
41
44
  "fontisan_#{name}_#{Process.pid}_#{rand(10000)}.zip")
42
45
 
43
46
  begin
44
- Fontisan::Tasks::FixtureDownloader.new(
47
+ FixtureFonts::Downloader.new(
45
48
  url: url,
46
49
  destination: temp_path,
47
50
  ).call
@@ -89,7 +92,7 @@ namespace :fixtures do
89
92
  end
90
93
 
91
94
  puts "[fixtures:download] #{name} downloaded successfully"
92
- rescue Fontisan::Tasks::FixtureDownloader::Error => e
95
+ rescue FixtureFonts::Downloader::Error => e
93
96
  warn "[fixtures:download] #{name} failed after retries: #{e.message}"
94
97
  raise
95
98
  rescue LoadError => e
@@ -24,10 +24,17 @@ module FontisanBench
24
24
  g.width = 500
25
25
  g.add_unicode(0xE000 + i) if i < 0x1000
26
26
  g.add_contour(Fontisan::Ufo::Contour.new([
27
- Fontisan::Ufo::Point.new(x: 0, y: 0, type: "line"),
28
- Fontisan::Ufo::Point.new(x: 100, y: 0, type: "line"),
29
- Fontisan::Ufo::Point.new(x: 100, y: 100, type: "offcurve"),
30
- Fontisan::Ufo::Point.new(x: 50, y: 150, type: "curve"),
27
+ Fontisan::Ufo::Point.new(x: 0,
28
+ y: 0, type: "line"),
29
+ Fontisan::Ufo::Point.new(
30
+ x: 100, y: 0, type: "line",
31
+ ),
32
+ Fontisan::Ufo::Point.new(
33
+ x: 100, y: 100, type: "offcurve",
34
+ ),
35
+ Fontisan::Ufo::Point.new(
36
+ x: 50, y: 150, type: "curve",
37
+ ),
31
38
  ]))
32
39
  font.glyphs["g#{i}"] = g
33
40
  end
@@ -38,7 +45,8 @@ module FontisanBench
38
45
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
46
  yield
40
47
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
41
- puts format("%-40<label>s %8.<elapsed>.2f ms", label: label, elapsed: elapsed * 1000)
48
+ puts format("%-40<label>s %8.<elapsed>.2f ms", label: label,
49
+ elapsed: elapsed * 1000)
42
50
  elapsed
43
51
  end
44
52
  end
@@ -322,6 +322,7 @@ export default defineConfig({
322
322
  { text: "ConversionOptions", link: "/api/conversion-options" },
323
323
  { text: "SfntFont", link: "/api/sfnt-font" },
324
324
  { text: "Type1Font", link: "/api/type1-font" },
325
+ { text: "Ufo::Layer", link: "/api/layer" },
325
326
  ],
326
327
  },
327
328
  {
@@ -364,12 +364,56 @@ stitcher.write_to("out.ttf", format: :ttf, subfont: :main)
364
364
  # → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
365
365
  ----
366
366
 
367
- Constraints:
367
+ === Outline-first cmap priority
368
+
369
+ When a codepoint is covered by BOTH an outline donor and the CBDT
370
+ donor, the cmap maps it to the outline glyph — never to the CBDT
371
+ placeholder. The Cmap compiler follows first-wins semantics, and
372
+ `GlyphCopier` runs before `CbdtPropagator` so the outline GID is
373
+ registered first. This was the fix for the Emoticons cmap-loss bug
374
+ where shared codepoints silently dropped out of the cmap.
375
+
376
+ === Placeholder name deconfliction
377
+
378
+ CBDT placeholders are named after the source's glyph IDs
379
+ (`"gid1"`, `"gid2"`, …), which is the SAME naming scheme
380
+ `Source#extract_truetype_glyph` uses for outline glyphs from TTF
381
+ donors. Without protection, a placeholder at `"gid110"` would
382
+ silently overwrite an outline glyph at `"gid110"` from a different
383
+ donor via `Layer#add` — the original cmap-loss vector for the
384
+ Essenfont CJK Ext G regression.
385
+
386
+ The Stitcher resolves this with two pieces:
387
+
388
+ * `Ufo::Layer#add` RAISES `Ufo::GlyphExistsError` on name conflict.
389
+ Silent overwrites are no longer possible. Callers that want to
390
+ replace an existing glyph use the explicit `Ufo::Layer#put`.
391
+ * `Stitcher::UniqueGlyphName.in(target, base)` allocates a
392
+ non-colliding name (`"gid110"`, then `"gid110.1"`, `"gid110.2"`,
393
+ …). `GlyphCopier` and `CbdtPropagator` both call it before every
394
+ `Layer#add`, so each glyph lands at its own GID slot without
395
+ touching the others.
396
+
397
+ === Limitations
368
398
 
369
399
  * Only ONE CBDT source per Stitcher. Multiple CBDT sources raise
370
400
  `Fontisan::MultipleCbdtSourcesError`.
371
- * The CBDT source is processed first (GIDs 0..N-1) so the CBLC's GID
372
- references remain valid in the output.
401
+ * **GID stability for overlapping codepoint ranges.** The raw-bytes
402
+ CBDT/CBLC propagation (`CbdtPropagator#propagate_tables_into`)
403
+ copies the source's CBLC verbatim. CBLC indexes glyphs by SOURCE
404
+ GID. After placeholder renaming (`"gid110"` → `"gid110.1"`), the
405
+ compiled GID no longer matches the source GID, so CBLC's source-
406
+ GID-indexed bitmaps can dangle. This is masked in the typical
407
+ Essenfont case (CBDT donor NotoColorEmoji and outline donors
408
+ cover disjoint codepoint ranges) but would produce wrong bitmaps
409
+ for any stitch where CBDT and outline sources share codepoint
410
+ ranges. A proper fix requires a CBLC rebuild pass that walks the
411
+ compiled cmap to remap source GIDs to compiled GIDs — tracked as
412
+ follow-up.
413
+ * **Collection-mode single-face coverage.** Each face is compiled
414
+ independently; `CbdtPropagator` currently requires at least one
415
+ outline codepoint per face. Collection-mode coverage of the
416
+ outline-first cmap-priority invariant is tracked as follow-up.
373
417
 
374
418
  == CLI
375
419
 
data/docs/api/layer.md ADDED
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Ufo::Layer
3
+ ---
4
+
5
+ # `Ufo::Layer`
6
+
7
+ A single layer in a UFO source. A `Layer` holds a set of glyphs keyed by name. The default layer is `public.default` per UFO 3.
8
+
9
+ ```ruby
10
+ layer = Fontisan::Ufo::Layer.new # => "public.default"
11
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
12
+ layer["A"] # => the glyph
13
+ layer.size # => 1
14
+ ```
15
+
16
+ ## Naming contract
17
+
18
+ Glyph names are the layer's primary key. Two distinct glyphs with the same name cannot coexist — adding the second would silently destroy the first, a class of bug that has historically cost real data (e.g. the CJK Ext G cmap loss when CBDT placeholders collided with outline glyphs sharing `"gid{N}"` names).
19
+
20
+ To make that contract unbreakable, the API exposes two methods with named semantics:
21
+
22
+ | Method | Behavior on conflict |
23
+ |--------|----------------------|
24
+ | `Layer#add(glyph)` | **Raises** `Ufo::GlyphExistsError` (the safe default) |
25
+ | `Layer#put(glyph)` | Overwrites the existing glyph (explicit replace) |
26
+
27
+ Callers that need auto-renaming — insert without giving up on a collision — call `Stitcher::UniqueGlyphName.in(target, base)` first to allocate a free name, then `Layer#add` the glyph under that name.
28
+
29
+ ## API
30
+
31
+ ### `Layer.new(name = "public.default")`
32
+
33
+ Initialize an empty layer.
34
+
35
+ ### `#add(glyph) -> Glyph`
36
+
37
+ Insert `glyph`. Raises `Ufo::GlyphExistsError` if a glyph with the same name is already present.
38
+
39
+ ```ruby
40
+ layer = Fontisan::Ufo::Layer.new
41
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
42
+
43
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
44
+ # => raises Fontisan::Ufo::GlyphExistsError: a glyph named "A" already exists
45
+ ```
46
+
47
+ The original glyph is preserved when the second `add` raises — the layer is never partially mutated.
48
+
49
+ ### `#put(glyph) -> Glyph`
50
+
51
+ Insert `glyph`, replacing any existing glyph with the same name. Use this when the caller has positively decided the previous glyph (if any) should be discarded.
52
+
53
+ ```ruby
54
+ layer.put(Fontisan::Ufo::Glyph.new(name: "A")) # overwrites the prior "A"
55
+ ```
56
+
57
+ ### `#[](glyph_name) -> Glyph, nil`
58
+
59
+ Look up a glyph by name.
60
+
61
+ ### `#each { |glyph| ... }`
62
+
63
+ Iterate over glyphs (no ordering guarantee).
64
+
65
+ ### `#size -> Integer`
66
+
67
+ Number of glyphs in the layer.
68
+
69
+ ## `Ufo::GlyphExistsError`
70
+
71
+ Raised by `Layer#add` on a name conflict. Carries the colliding name so callers can branch programmatically:
72
+
73
+ ```ruby
74
+ begin
75
+ layer.add(glyph)
76
+ rescue Fontisan::Ufo::GlyphExistsError => e
77
+ puts "name #{e.name.inspect} already taken"
78
+ # Decide: layer.put(glyph) to overwrite, or
79
+ # unique = Fontisan::Stitcher::UniqueGlyphName.in(target, e.name)
80
+ # layer.add(glyph.dup_as(name: unique))
81
+ end
82
+ ```
83
+
84
+ The error class is a sibling of `Layer` (not nested inside it) so the namespace stays flat — both `Ufo::Layer` and `Ufo::GlyphExistsError` live directly under `Fontisan::Ufo`.
85
+
86
+ ## See also
87
+
88
+ - `docs/STITCHER_GUIDE.adoc` — `UniqueGlyphName`, `GlyphCopier`, and `CbdtPropagator` all build on the Layer naming contract.
89
+ - `docs/UFO_COMPILATION.adoc` — how layers feed the TTF/OTF/CFF2 compilers.