fontisan 0.4.17 → 0.4.19
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +21 -110
- data/lib/fontisan/collection/writer.rb +2 -16
- data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
- data/lib/fontisan/converters/type1_converter.rb +2 -4
- data/lib/fontisan/converters/woff2_encoder.rb +110 -32
- data/lib/fontisan/converters/woff_writer.rb +2 -7
- data/lib/fontisan/font_writer.rb +3 -9
- data/lib/fontisan/sfnt_builder.rb +1 -1
- data/lib/fontisan/sfnt_font.rb +1 -1
- data/lib/fontisan/tables/head.rb +30 -7
- data/lib/fontisan/ufo/compile/head.rb +2 -4
- data/lib/fontisan/utilities/brotli_wrapper.rb +1 -16
- data/lib/fontisan/utilities/padding.rb +60 -0
- data/lib/fontisan/utilities.rb +1 -0
- data/lib/fontisan/variable/table_updater.rb +1 -3
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/encoder_rules.rb +40 -0
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +89 -0
- data/lib/fontisan/woff2/sfnt_checksum.rb +106 -0
- data/lib/fontisan/woff2.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec589024241926918e56eb58fbe63cd2b14cc0d82fd92908337f7f2f7ef5c076
|
|
4
|
+
data.tar.gz: f3ef0a193655de4790717559c7bce5722f5d9e7b5548bc5b313cf400a3b2121d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b83304cf80e5f7a55ee98717b0f3bec987ad81cef329d0f1d2ab9ab91ecd66db58f6e2ed5eada8c3b4049665e3a37240a26477dbd25405a586d1f9ca7fc7dd61
|
|
7
|
+
data.tar.gz: ebc0f3e2c752e7752452280d83e4220aabb12c23a4766e431dbc3e13b480b85b0259a929656919da42e9993a8bd2354a9283fbef21a95ba49b666c54c8d0812b
|
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-
|
|
3
|
+
# on 2026-07-07 11:03:20 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,84 +12,20 @@ Gemspec/RequiredRubyVersion:
|
|
|
12
12
|
- 'docs/node_modules/speakingurl/speakingurl-rails.gemspec'
|
|
13
13
|
- 'fontisan.gemspec'
|
|
14
14
|
|
|
15
|
-
# Offense count:
|
|
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/cldr/unicode_set_parser.rb'
|
|
22
|
-
- 'lib/fontisan/hints.rb'
|
|
23
|
-
- 'lib/fontisan/models.rb'
|
|
24
|
-
- 'lib/fontisan/pipeline.rb'
|
|
25
|
-
- 'lib/fontisan/pipeline/strategies.rb'
|
|
26
|
-
- 'lib/fontisan/tables.rb'
|
|
27
|
-
- 'lib/fontisan/tables/cff.rb'
|
|
28
|
-
- 'spec/fontisan/cldr/config_spec.rb'
|
|
29
|
-
- 'spec/fontisan/cldr/unicode_set_parser_spec.rb'
|
|
30
|
-
- 'spec/fontisan/cli/cldr_cli_spec.rb'
|
|
31
|
-
|
|
32
|
-
# Offense count: 2
|
|
33
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
34
|
-
# Configuration parameters: EnforcedStyleAlignWith.
|
|
35
|
-
# SupportedStylesAlignWith: either, start_of_block, start_of_line
|
|
36
|
-
Layout/BlockAlignment:
|
|
37
|
-
Exclude:
|
|
38
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
39
|
-
- 'spec/fontisan/cldr/index_spec.rb'
|
|
40
|
-
|
|
41
|
-
# Offense count: 2
|
|
42
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
43
|
-
Layout/BlockEndNewline:
|
|
44
|
-
Exclude:
|
|
45
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
46
|
-
- 'spec/fontisan/cldr/index_spec.rb'
|
|
47
|
-
|
|
48
|
-
# Offense count: 2
|
|
49
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
50
|
-
Layout/EmptyLines:
|
|
51
|
-
Exclude:
|
|
52
|
-
- 'lib/fontisan/cli.rb'
|
|
53
|
-
|
|
54
|
-
# Offense count: 4
|
|
55
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
56
|
-
# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
|
|
57
|
-
# SupportedStylesAlignWith: start_of_line, relative_to_receiver
|
|
58
|
-
Layout/IndentationWidth:
|
|
59
|
-
Exclude:
|
|
60
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
61
|
-
- 'spec/fontisan/cldr/index_spec.rb'
|
|
62
|
-
|
|
63
|
-
# Offense count: 1641
|
|
15
|
+
# Offense count: 2090
|
|
64
16
|
# This cop supports safe autocorrection (--autocorrect).
|
|
65
17
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
66
18
|
# URISchemes: http, https
|
|
67
19
|
Layout/LineLength:
|
|
68
20
|
Enabled: false
|
|
69
21
|
|
|
70
|
-
# Offense count: 21
|
|
71
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
72
|
-
# Configuration parameters: AllowInHeredoc.
|
|
73
|
-
Layout/TrailingWhitespace:
|
|
74
|
-
Exclude:
|
|
75
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
76
|
-
- 'lib/fontisan/hints.rb'
|
|
77
|
-
- 'lib/fontisan/models.rb'
|
|
78
|
-
- 'lib/fontisan/pipeline.rb'
|
|
79
|
-
- 'lib/fontisan/pipeline/strategies.rb'
|
|
80
|
-
- 'lib/fontisan/tables.rb'
|
|
81
|
-
- 'lib/fontisan/tables/cff.rb'
|
|
82
|
-
- 'spec/fontisan/cldr/config_spec.rb'
|
|
83
|
-
- 'spec/fontisan/cldr/unicode_set_parser_spec.rb'
|
|
84
|
-
- 'spec/fontisan/cli/cldr_cli_spec.rb'
|
|
85
|
-
|
|
86
22
|
# Offense count: 2
|
|
87
23
|
Lint/CopDirectiveSyntax:
|
|
88
24
|
Exclude:
|
|
89
25
|
- 'lib/fontisan/woff2_font.rb'
|
|
90
26
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
91
27
|
|
|
92
|
-
# Offense count:
|
|
28
|
+
# Offense count: 52
|
|
93
29
|
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
|
94
30
|
Lint/DuplicateBranch:
|
|
95
31
|
Enabled: false
|
|
@@ -142,7 +78,7 @@ Lint/StructNewOverride:
|
|
|
142
78
|
Exclude:
|
|
143
79
|
- 'lib/fontisan/optimizers/pattern_analyzer.rb'
|
|
144
80
|
|
|
145
|
-
# Offense count:
|
|
81
|
+
# Offense count: 5
|
|
146
82
|
# This cop supports safe autocorrection (--autocorrect).
|
|
147
83
|
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
|
|
148
84
|
# NotImplementedExceptions: NotImplementedError
|
|
@@ -150,9 +86,7 @@ Lint/UnusedMethodArgument:
|
|
|
150
86
|
Exclude:
|
|
151
87
|
- 'lib/fontisan.rb'
|
|
152
88
|
- 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb'
|
|
153
|
-
- 'lib/fontisan/utilities/brotli_wrapper.rb'
|
|
154
89
|
- 'lib/fontisan/variation/table_accessor.rb'
|
|
155
|
-
- 'lib/fontisan/woff2/glyf_transformer.rb'
|
|
156
90
|
- 'lib/fontisan/woff_font.rb'
|
|
157
91
|
|
|
158
92
|
# Offense count: 5
|
|
@@ -161,21 +95,21 @@ Lint/UselessConstantScoping:
|
|
|
161
95
|
- 'lib/fontisan/conversion_options.rb'
|
|
162
96
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
163
97
|
|
|
164
|
-
# Offense count:
|
|
98
|
+
# Offense count: 703
|
|
165
99
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
166
100
|
Metrics/AbcSize:
|
|
167
101
|
Enabled: false
|
|
168
102
|
|
|
169
|
-
# Offense count:
|
|
103
|
+
# Offense count: 36
|
|
170
104
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
|
171
105
|
# AllowedMethods: refine
|
|
172
106
|
Metrics/BlockLength:
|
|
173
107
|
Max: 104
|
|
174
108
|
|
|
175
|
-
# Offense count:
|
|
109
|
+
# Offense count: 4
|
|
176
110
|
# Configuration parameters: CountBlocks, CountModifierForms.
|
|
177
111
|
Metrics/BlockNesting:
|
|
178
|
-
Max:
|
|
112
|
+
Max: 4
|
|
179
113
|
|
|
180
114
|
# Offense count: 2
|
|
181
115
|
# Configuration parameters: LengthThreshold.
|
|
@@ -184,28 +118,28 @@ Metrics/CollectionLiteralLength:
|
|
|
184
118
|
- 'lib/fontisan/type1/agl.rb'
|
|
185
119
|
- 'lib/fontisan/type1/encodings.rb'
|
|
186
120
|
|
|
187
|
-
# Offense count:
|
|
121
|
+
# Offense count: 366
|
|
188
122
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
189
123
|
Metrics/CyclomaticComplexity:
|
|
190
124
|
Enabled: false
|
|
191
125
|
|
|
192
|
-
# Offense count:
|
|
126
|
+
# Offense count: 977
|
|
193
127
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
194
128
|
Metrics/MethodLength:
|
|
195
129
|
Max: 135
|
|
196
130
|
|
|
197
|
-
# Offense count:
|
|
131
|
+
# Offense count: 27
|
|
198
132
|
# Configuration parameters: CountKeywordArgs.
|
|
199
133
|
Metrics/ParameterLists:
|
|
200
134
|
Max: 39
|
|
201
135
|
MaxOptionalParameters: 4
|
|
202
136
|
|
|
203
|
-
# Offense count:
|
|
137
|
+
# Offense count: 278
|
|
204
138
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
205
139
|
Metrics/PerceivedComplexity:
|
|
206
140
|
Enabled: false
|
|
207
141
|
|
|
208
|
-
# Offense count:
|
|
142
|
+
# Offense count: 208
|
|
209
143
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
210
144
|
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
211
145
|
Naming/MethodParameterName:
|
|
@@ -248,7 +182,7 @@ Naming/VariableName:
|
|
|
248
182
|
- 'lib/fontisan/type1/pfm_generator.rb'
|
|
249
183
|
- 'lib/fontisan/type1/pfm_parser.rb'
|
|
250
184
|
|
|
251
|
-
# Offense count:
|
|
185
|
+
# Offense count: 120
|
|
252
186
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
253
187
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
254
188
|
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
@@ -265,13 +199,13 @@ RSpec/AnyInstance:
|
|
|
265
199
|
- 'spec/fontisan/variation/parallel_generator_spec.rb'
|
|
266
200
|
- 'spec/integration/format_conversion_spec.rb'
|
|
267
201
|
|
|
268
|
-
# Offense count:
|
|
202
|
+
# Offense count: 127
|
|
269
203
|
# Configuration parameters: Prefixes, AllowedPatterns.
|
|
270
204
|
# Prefixes: when, with, without
|
|
271
205
|
RSpec/ContextWording:
|
|
272
206
|
Enabled: false
|
|
273
207
|
|
|
274
|
-
# Offense count:
|
|
208
|
+
# Offense count: 38
|
|
275
209
|
# Configuration parameters: IgnoredMetadata.
|
|
276
210
|
RSpec/DescribeClass:
|
|
277
211
|
Enabled: false
|
|
@@ -287,7 +221,7 @@ RSpec/DescribeMethod:
|
|
|
287
221
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
288
222
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
289
223
|
|
|
290
|
-
# Offense count:
|
|
224
|
+
# Offense count: 1660
|
|
291
225
|
# Configuration parameters: CountAsOne.
|
|
292
226
|
RSpec/ExampleLength:
|
|
293
227
|
Max: 66
|
|
@@ -364,11 +298,11 @@ RSpec/MultipleDescribes:
|
|
|
364
298
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
365
299
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
366
300
|
|
|
367
|
-
# Offense count:
|
|
301
|
+
# Offense count: 2097
|
|
368
302
|
RSpec/MultipleExpectations:
|
|
369
303
|
Max: 19
|
|
370
304
|
|
|
371
|
-
# Offense count:
|
|
305
|
+
# Offense count: 157
|
|
372
306
|
# Configuration parameters: AllowSubject.
|
|
373
307
|
RSpec/MultipleMemoizedHelpers:
|
|
374
308
|
Max: 13
|
|
@@ -396,7 +330,7 @@ RSpec/RepeatedExample:
|
|
|
396
330
|
- 'spec/fontisan/metrics_calculator_spec.rb'
|
|
397
331
|
- 'spec/fontisan/tables/os2_spec.rb'
|
|
398
332
|
|
|
399
|
-
# Offense count:
|
|
333
|
+
# Offense count: 47
|
|
400
334
|
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
401
335
|
# SupportedInflectors: default, active_support
|
|
402
336
|
RSpec/SpecFilePathFormat:
|
|
@@ -417,7 +351,7 @@ RSpec/SubjectStub:
|
|
|
417
351
|
Exclude:
|
|
418
352
|
- 'spec/fontisan/variation/delta_applier_spec.rb'
|
|
419
353
|
|
|
420
|
-
# Offense count:
|
|
354
|
+
# Offense count: 422
|
|
421
355
|
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
|
422
356
|
RSpec/VerifiedDoubles:
|
|
423
357
|
Enabled: false
|
|
@@ -427,23 +361,6 @@ Rake/MethodDefinitionInTask:
|
|
|
427
361
|
Exclude:
|
|
428
362
|
- 'Rakefile'
|
|
429
363
|
|
|
430
|
-
# Offense count: 2
|
|
431
|
-
Security/Open:
|
|
432
|
-
Exclude:
|
|
433
|
-
- 'Rakefile'
|
|
434
|
-
|
|
435
|
-
# Offense count: 2
|
|
436
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
437
|
-
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
|
|
438
|
-
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
|
439
|
-
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
|
440
|
-
# FunctionalMethods: let, let!, subject, watch
|
|
441
|
-
# AllowedMethods: lambda, proc, it
|
|
442
|
-
Style/BlockDelimiters:
|
|
443
|
-
Exclude:
|
|
444
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
445
|
-
- 'spec/fontisan/cldr/index_spec.rb'
|
|
446
|
-
|
|
447
364
|
# Offense count: 6
|
|
448
365
|
# This cop supports safe autocorrection (--autocorrect).
|
|
449
366
|
Style/ComparableClamp:
|
|
@@ -478,12 +395,6 @@ Style/HashLikeCase:
|
|
|
478
395
|
- 'lib/fontisan/commands/unpack_command.rb'
|
|
479
396
|
- 'lib/fontisan/models/validation_report.rb'
|
|
480
397
|
|
|
481
|
-
# Offense count: 9
|
|
482
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
483
|
-
Style/MultilineIfModifier:
|
|
484
|
-
Exclude:
|
|
485
|
-
- 'lib/fontisan/cldr/unicode_set_parser.rb'
|
|
486
|
-
|
|
487
398
|
# Offense count: 1
|
|
488
399
|
# Configuration parameters: AllowedMethods.
|
|
489
400
|
# AllowedMethods: respond_to_missing?
|
|
@@ -212,7 +212,7 @@ module Fontisan
|
|
|
212
212
|
binary << table_info[:data]
|
|
213
213
|
|
|
214
214
|
# Pad to 4-byte boundary
|
|
215
|
-
padding =
|
|
215
|
+
padding = Utilities::Padding.boundary(table_info[:data])
|
|
216
216
|
binary << ("\x00" * padding) if padding.positive?
|
|
217
217
|
end
|
|
218
218
|
end
|
|
@@ -244,26 +244,12 @@ module Fontisan
|
|
|
244
244
|
binary << ("\x00" * padding_needed)
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
-
# Calculate padding needed for 4-byte alignment
|
|
248
|
-
#
|
|
249
|
-
# @param size [Integer] Current size
|
|
250
|
-
# @return [Integer] Padding bytes needed
|
|
251
|
-
def calculate_padding(size)
|
|
252
|
-
remainder = size % 4
|
|
253
|
-
return 0 if remainder.zero?
|
|
254
|
-
|
|
255
|
-
4 - remainder
|
|
256
|
-
end
|
|
257
|
-
|
|
258
247
|
# Calculate table checksum
|
|
259
248
|
#
|
|
260
249
|
# @param data [String] Table data
|
|
261
250
|
# @return [Integer] Checksum
|
|
262
251
|
def calculate_table_checksum(data)
|
|
263
|
-
|
|
264
|
-
padded_data = data.dup
|
|
265
|
-
padding_length = calculate_padding(data.bytesize)
|
|
266
|
-
padded_data << ("\x00" * padding_length) if padding_length.positive?
|
|
252
|
+
padded_data = Utilities::Padding.pad(data)
|
|
267
253
|
|
|
268
254
|
# Sum all uint32 values
|
|
269
255
|
sum = 0
|
|
@@ -571,12 +571,10 @@ module Fontisan
|
|
|
571
571
|
data << [1000].pack("n")
|
|
572
572
|
|
|
573
573
|
# Created (LONGDATETIME) - use current time
|
|
574
|
-
|
|
575
|
-
data << [created_seconds].pack("Q>")
|
|
574
|
+
data << [Tables::Head.now_longdatetime].pack("Q>")
|
|
576
575
|
|
|
577
576
|
# Modified (LONGDATETIME) - use current time
|
|
578
|
-
|
|
579
|
-
data << [modified_seconds].pack("Q>")
|
|
577
|
+
data << [Tables::Head.now_longdatetime].pack("Q>")
|
|
580
578
|
|
|
581
579
|
# Bounding box (int16 each)
|
|
582
580
|
data << [font_bbox[0]].pack("s>") # x_min
|
|
@@ -28,6 +28,11 @@ module Fontisan
|
|
|
28
28
|
class Woff2Encoder
|
|
29
29
|
include ConversionStrategy
|
|
30
30
|
|
|
31
|
+
# Highest glyf offset addressable by short loca (uint16 max × 2).
|
|
32
|
+
# Above this, loca MUST use the long format. Public so specs can
|
|
33
|
+
# reference the same threshold the encoder uses.
|
|
34
|
+
SHORT_LOCA_GLYF_MAX = 0x1FFFE
|
|
35
|
+
|
|
31
36
|
# @return [Hash] Configuration settings
|
|
32
37
|
attr_reader :config
|
|
33
38
|
|
|
@@ -83,6 +88,13 @@ module Fontisan
|
|
|
83
88
|
transform_enabled,
|
|
84
89
|
glyf_transform:)
|
|
85
90
|
|
|
91
|
+
# Recompute head.checksumAdjustment for the SFNT the decoder
|
|
92
|
+
# reconstructs. Chrome's OTS validates this strictly; a stale
|
|
93
|
+
# source value fails once glyf/loca/head have been modified.
|
|
94
|
+
# Must run AFTER touch_head_modified! so the modified timestamp
|
|
95
|
+
# change is included in the checksum.
|
|
96
|
+
update_checksum_adjustment!(table_data, flavor:, glyf_transform:)
|
|
97
|
+
|
|
86
98
|
compressed_data = compress_tables(entries, table_data,
|
|
87
99
|
transformed_data, quality)
|
|
88
100
|
|
|
@@ -221,17 +233,17 @@ module Fontisan
|
|
|
221
233
|
|
|
222
234
|
# Apply the WOFF2 glyf/loca paired transform per spec section 5.1/5.3.
|
|
223
235
|
#
|
|
224
|
-
# Returns a hash with the transformed glyf bytes and
|
|
225
|
-
# loca
|
|
226
|
-
#
|
|
227
|
-
# are kept in `table_data` so its `origLength` is preserved; the
|
|
228
|
-
# transformed bytes are passed to the entries builder separately.
|
|
236
|
+
# Returns a hash with the transformed glyf bytes and reconstructed
|
|
237
|
+
# glyf/loca bytes the directory/checksum must reference, or nil for
|
|
238
|
+
# CFF fonts or fonts missing glyf/loca.
|
|
229
239
|
#
|
|
230
|
-
# glyf
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
240
|
+
# The source glyf is first canonicalized to 4-byte-per-glyph
|
|
241
|
+
# alignment via `Woff2::GlyfCanonicalizer`, matching fontTools'
|
|
242
|
+
# `_normaliseGlyfAndLoca(padding=4)`. Chrome's OTS fails to
|
|
243
|
+
# reconstruct "unpadded" glyf tables — see fontTools'
|
|
244
|
+
# WOFF2Writer.close() comment. The canonical glyf is the input to
|
|
245
|
+
# the WOFF2 transform, the value of `glyf.origLength` in the
|
|
246
|
+
# directory, and the bytes the SFNT checksum is computed over.
|
|
235
247
|
#
|
|
236
248
|
# @return [Hash{Symbol => Object}, nil]
|
|
237
249
|
def apply_glyf_loca_transform!(table_data, font)
|
|
@@ -246,32 +258,55 @@ module Fontisan
|
|
|
246
258
|
head = font.table("head")
|
|
247
259
|
return nil unless maxp && head
|
|
248
260
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
source_format = head.index_to_loc_format
|
|
262
|
+
|
|
263
|
+
# Pick the most compact loca format the canonical glyf fits in.
|
|
264
|
+
# The canonical glyf adds ≤ 3 padding bytes per glyph; estimate
|
|
265
|
+
# the upper bound so the format choice is safe. fontTools'
|
|
266
|
+
# encoder does the same — short loca halves the loca table size
|
|
267
|
+
# and Chrome's OTS accepts both formats.
|
|
268
|
+
max_canonical_glyf_size = glyf_data.bytesize + 3 * maxp.num_glyphs
|
|
269
|
+
target_format = if max_canonical_glyf_size <= SHORT_LOCA_GLYF_MAX
|
|
270
|
+
0
|
|
271
|
+
else
|
|
272
|
+
source_format
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
canonical = Woff2::GlyfCanonicalizer.new(
|
|
252
276
|
glyf_data:,
|
|
253
277
|
loca_data:,
|
|
254
278
|
num_glyphs: maxp.num_glyphs,
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
source_format:,
|
|
280
|
+
target_format:,
|
|
281
|
+
).canonical
|
|
282
|
+
canonical_glyf = canonical[:glyf]
|
|
283
|
+
canonical_loca = canonical[:loca]
|
|
257
284
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# small amounts (encoding variations per OFF section 5.3.3). Using
|
|
262
|
-
# the source bytesize causes fontTools' decoder to reject the
|
|
263
|
-
# WOFF2 with "not enough 'glyf' table data".
|
|
264
|
-
reconstructed = Woff2::GlyfLocaReconstruct.new(
|
|
265
|
-
transformed_glyf: transformed,
|
|
285
|
+
transformed = Woff2::GlyfLocaTransform.new(
|
|
286
|
+
glyf_data: canonical_glyf,
|
|
287
|
+
loca_data: canonical_loca,
|
|
266
288
|
num_glyphs: maxp.num_glyphs,
|
|
267
|
-
index_format
|
|
268
|
-
).
|
|
289
|
+
index_format: target_format,
|
|
290
|
+
).transform
|
|
269
291
|
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
if target_format != source_format
|
|
293
|
+
Woff2::EncoderRules.set_head_index_to_loc_format!(
|
|
294
|
+
table_data, target_format
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
glyf_orig_length = canonical_glyf.bytesize
|
|
299
|
+
loca_orig_length = canonical_loca.bytesize
|
|
300
|
+
table_data["glyf"] = canonical_glyf
|
|
272
301
|
table_data.delete("loca")
|
|
273
302
|
|
|
274
|
-
{
|
|
303
|
+
{
|
|
304
|
+
transformed_glyf: transformed,
|
|
305
|
+
reconstructed_glyf: canonical_glyf,
|
|
306
|
+
reconstructed_loca: canonical_loca,
|
|
307
|
+
glyf_orig_length:,
|
|
308
|
+
loca_orig_length:,
|
|
309
|
+
}
|
|
275
310
|
end
|
|
276
311
|
|
|
277
312
|
def get_table_data(font, tag)
|
|
@@ -383,19 +418,56 @@ module Fontisan
|
|
|
383
418
|
size += 16 if glyf_transform # synthetic loca directory entry
|
|
384
419
|
|
|
385
420
|
table_data.each_value do |data|
|
|
386
|
-
size += data
|
|
387
|
-
size += (4 - (data.bytesize % 4)) % 4 # pad to 4-byte boundary
|
|
421
|
+
size += Utilities::Padding.aligned_size(data)
|
|
388
422
|
end
|
|
389
423
|
|
|
390
424
|
if glyf_transform
|
|
391
425
|
loca_len = glyf_transform[:loca_orig_length]
|
|
392
|
-
size += loca_len
|
|
393
|
-
size += (4 - (loca_len % 4)) % 4
|
|
426
|
+
size += Utilities::Padding.aligned_size(loca_len)
|
|
394
427
|
end
|
|
395
428
|
|
|
396
429
|
size
|
|
397
430
|
end
|
|
398
431
|
|
|
432
|
+
# Recompute head.checksumAdjustment for the SFNT the WOFF2 decoder
|
|
433
|
+
# will reconstruct. The decoder assembles glyf/loca from the
|
|
434
|
+
# transformed stream and uses the source bytes for every other
|
|
435
|
+
# table; the resulting SFNT's whole-file uint32 sum must equal
|
|
436
|
+
# 0xB1B0AFBA once checksumAdjustment is applied. Source's value is
|
|
437
|
+
# stale after head.flags/glyf/indexToLocFormat edits and Chrome's
|
|
438
|
+
# OTS rejects files that fail the checksum check.
|
|
439
|
+
#
|
|
440
|
+
# Mutates `table_data["head"]` in place via the Head BinData model.
|
|
441
|
+
def update_checksum_adjustment!(table_data, flavor:, glyf_transform:)
|
|
442
|
+
return unless table_data.key?("head")
|
|
443
|
+
|
|
444
|
+
# The synthetic loca entry was removed from table_data by
|
|
445
|
+
# apply_glyf_loca_transform!, but it's still part of the
|
|
446
|
+
# reconstructed SFNT, so it must be included in the checksum.
|
|
447
|
+
tags = table_data.keys.sort
|
|
448
|
+
if glyf_transform && !tags.include?("loca")
|
|
449
|
+
loca_idx = tags.index { |t| t > "loca" } || tags.length
|
|
450
|
+
tags.insert(loca_idx, "loca")
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
tables = tags.map do |tag|
|
|
454
|
+
bytes = if tag == "glyf" && glyf_transform
|
|
455
|
+
glyf_transform[:reconstructed_glyf]
|
|
456
|
+
elsif tag == "loca" && glyf_transform
|
|
457
|
+
glyf_transform[:reconstructed_loca]
|
|
458
|
+
else
|
|
459
|
+
table_data[tag]
|
|
460
|
+
end
|
|
461
|
+
Woff2::SfntChecksum::Table.new(tag:, bytes:)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
adjustment = Woff2::SfntChecksum.new(flavor:, tables:).adjustment
|
|
465
|
+
|
|
466
|
+
head = Tables::Head.read(table_data["head"])
|
|
467
|
+
head.checksum_adjustment = adjustment
|
|
468
|
+
table_data["head"] = head.to_binary_s
|
|
469
|
+
end
|
|
470
|
+
|
|
399
471
|
def build_header(flavor:, num_tables:, total_sfnt_size:,
|
|
400
472
|
total_compressed_size:)
|
|
401
473
|
header = Woff2::Woff2Header.new
|
|
@@ -430,6 +502,12 @@ module Fontisan
|
|
|
430
502
|
end
|
|
431
503
|
|
|
432
504
|
woff2_data << compressed_data
|
|
505
|
+
# Per WOFF2 spec section 5.3, the table directory + compressed
|
|
506
|
+
# font block MUST be padded with null bytes so the total file
|
|
507
|
+
# length is a multiple of 4. fontTools' WOFF2Writer does the
|
|
508
|
+
# same: `pad(directory + compressedFont, size=4)`.
|
|
509
|
+
pad = Utilities::Padding.boundary(woff2_data)
|
|
510
|
+
woff2_data << ("\x00" * pad) if pad.positive?
|
|
433
511
|
update_woff2_length!(woff2_data)
|
|
434
512
|
woff2_data
|
|
435
513
|
end
|
|
@@ -234,11 +234,6 @@ module Fontisan
|
|
|
234
234
|
}
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
# Padding bytes needed to bring `length` up to a 4-byte boundary.
|
|
238
|
-
def padding_to_4(length)
|
|
239
|
-
(4 - (length % 4)) % 4
|
|
240
|
-
end
|
|
241
|
-
|
|
242
237
|
# Assemble complete WOFF binary.
|
|
243
238
|
#
|
|
244
239
|
# Computes the full layout (offsets + padding) up front so the header
|
|
@@ -262,7 +257,7 @@ module Fontisan
|
|
|
262
257
|
entries = []
|
|
263
258
|
cursor = data_start
|
|
264
259
|
compressed_tables.each do |tag, info|
|
|
265
|
-
pad =
|
|
260
|
+
pad = Utilities::Padding.boundary(info[:compressed_length])
|
|
266
261
|
entries << { tag:, info:, offset: cursor, pad_bytes: "\x00" * pad }
|
|
267
262
|
cursor += info[:compressed_length] + pad
|
|
268
263
|
end
|
|
@@ -284,7 +279,7 @@ module Fontisan
|
|
|
284
279
|
sfnt_header_size = 12
|
|
285
280
|
sfnt_dir_size = num_tables * 16
|
|
286
281
|
sfnt_tables_size = compressed_tables.values.sum do |t|
|
|
287
|
-
|
|
282
|
+
Utilities::Padding.aligned_size(t[:original_length])
|
|
288
283
|
end
|
|
289
284
|
total_sfnt_size = sfnt_header_size + sfnt_dir_size + sfnt_tables_size
|
|
290
285
|
|
data/lib/fontisan/font_writer.rb
CHANGED
|
@@ -165,7 +165,7 @@ module Fontisan
|
|
|
165
165
|
checksum = calculate_table_checksum(data)
|
|
166
166
|
|
|
167
167
|
# Calculate padding to 4-byte boundary
|
|
168
|
-
padding_length = (
|
|
168
|
+
padding_length = Utilities::Padding.boundary(data)
|
|
169
169
|
padding = "\0" * padding_length
|
|
170
170
|
|
|
171
171
|
entries << {
|
|
@@ -235,10 +235,7 @@ module Fontisan
|
|
|
235
235
|
# @param data [String] Table binary data
|
|
236
236
|
# @return [Integer] Table checksum
|
|
237
237
|
def calculate_table_checksum(data)
|
|
238
|
-
|
|
239
|
-
padded_data = data.dup
|
|
240
|
-
padding_length = (4 - (data.bytesize % 4)) % 4
|
|
241
|
-
padded_data << ("\0" * padding_length) if padding_length.positive?
|
|
238
|
+
padded_data = Utilities::Padding.pad(data)
|
|
242
239
|
|
|
243
240
|
# Sum all uint32 values
|
|
244
241
|
sum = 0
|
|
@@ -285,10 +282,7 @@ module Fontisan
|
|
|
285
282
|
# @param data [String] Complete font binary
|
|
286
283
|
# @return [Integer] Font checksum
|
|
287
284
|
def calculate_font_checksum(data)
|
|
288
|
-
|
|
289
|
-
padded_data = data.dup
|
|
290
|
-
padding_length = (4 - (data.bytesize % 4)) % 4
|
|
291
|
-
padded_data << ("\0" * padding_length) if padding_length.positive?
|
|
285
|
+
padded_data = Utilities::Padding.pad(data)
|
|
292
286
|
|
|
293
287
|
# Sum all uint32 values
|
|
294
288
|
sum = 0
|
|
@@ -34,7 +34,7 @@ module Fontisan
|
|
|
34
34
|
records = tables.map do |tag, data|
|
|
35
35
|
length = data.bytesize
|
|
36
36
|
checksum = Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
37
|
-
padding = (
|
|
37
|
+
padding = Utilities::Padding.boundary(length)
|
|
38
38
|
record = { tag:, checksum:, offset: cursor, length:, data:, padding: }
|
|
39
39
|
cursor += length + padding
|
|
40
40
|
record
|
data/lib/fontisan/sfnt_font.rb
CHANGED
|
@@ -714,7 +714,7 @@ module Fontisan
|
|
|
714
714
|
io.write(data)
|
|
715
715
|
|
|
716
716
|
# Add padding to align to 4-byte boundary
|
|
717
|
-
padding =
|
|
717
|
+
padding = Utilities::Padding.boundary(io.pos)
|
|
718
718
|
io.write(PADDING_BYTES[0, padding]) if padding.positive?
|
|
719
719
|
|
|
720
720
|
# Zero out checksumAdjustment field in head table
|
data/lib/fontisan/tables/head.rb
CHANGED
|
@@ -28,6 +28,12 @@ module Fontisan
|
|
|
28
28
|
# rejects WOFF2 files where it is not set.
|
|
29
29
|
FLAG_LOSSLESS_MODIFYING = 0x0800
|
|
30
30
|
|
|
31
|
+
# Difference between 1904-01-01 00:00:00 UTC (Mac epoch, used by
|
|
32
|
+
# LONGDATETIME fields) and 1970-01-01 00:00:00 UTC (Unix epoch).
|
|
33
|
+
# Single source of truth for converting between the two — head's
|
|
34
|
+
# created/modified fields are LONGDATETIME.
|
|
35
|
+
MAC_EPOCH_OFFSET = 2_082_844_800
|
|
36
|
+
|
|
31
37
|
# Version as 16.16 fixed-point (stored as int32)
|
|
32
38
|
int32 :version_raw
|
|
33
39
|
|
|
@@ -73,14 +79,14 @@ module Fontisan
|
|
|
73
79
|
#
|
|
74
80
|
# @return [Time] Creation time
|
|
75
81
|
def created
|
|
76
|
-
longdatetime_to_time(created_raw)
|
|
82
|
+
self.class.longdatetime_to_time(created_raw)
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
# Convert modified timestamp to Time object
|
|
80
86
|
#
|
|
81
87
|
# @return [Time] Modification time
|
|
82
88
|
def modified
|
|
83
|
-
longdatetime_to_time(modified_raw)
|
|
89
|
+
self.class.longdatetime_to_time(modified_raw)
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
# Validate that the magic number is correct
|
|
@@ -150,6 +156,26 @@ module Fontisan
|
|
|
150
156
|
glyph_data_format.zero?
|
|
151
157
|
end
|
|
152
158
|
|
|
159
|
+
# Current wall clock time as a LONGDATETIME integer (seconds since
|
|
160
|
+
# Mac epoch). Used wherever a `created`/`modified` timestamp needs
|
|
161
|
+
# to be set to a meaningful "now" value (WOFF2 encoder, Type 1
|
|
162
|
+
# converter, UFO compile fallback).
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer] Seconds since 1904-01-01 00:00:00 UTC
|
|
165
|
+
def self.now_longdatetime
|
|
166
|
+
to_longdatetime(Time.now)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Convert a Unix-epoch `Time` (or Integer seconds) to a LONGDATETIME
|
|
170
|
+
# integer (Mac-epoch seconds). Symmetric with `#longdatetime_to_time`
|
|
171
|
+
# — pair them whenever you convert across the epoch boundary.
|
|
172
|
+
#
|
|
173
|
+
# @param time [Time, Integer] Unix-epoch time, default `Time.now`
|
|
174
|
+
# @return [Integer] Seconds since 1904-01-01 00:00:00 UTC
|
|
175
|
+
def self.to_longdatetime(time = Time.now)
|
|
176
|
+
time.to_i + MAC_EPOCH_OFFSET
|
|
177
|
+
end
|
|
178
|
+
|
|
153
179
|
# Validate magic number and raise error if invalid
|
|
154
180
|
#
|
|
155
181
|
# @raise [Fontisan::CorruptedTableError] If magic number is invalid
|
|
@@ -167,15 +193,12 @@ module Fontisan
|
|
|
167
193
|
# Alias for backward compatibility
|
|
168
194
|
alias validate! validate_magic_number!
|
|
169
195
|
|
|
170
|
-
private
|
|
171
|
-
|
|
172
196
|
# Convert LONGDATETIME to Ruby Time
|
|
173
197
|
#
|
|
174
198
|
# @param seconds [Integer] Seconds since 1904-01-01 00:00:00
|
|
175
199
|
# @return [Time] Ruby Time object
|
|
176
|
-
def longdatetime_to_time(seconds)
|
|
177
|
-
|
|
178
|
-
Time.at(seconds - 2_082_844_800)
|
|
200
|
+
def self.longdatetime_to_time(seconds)
|
|
201
|
+
Time.at(seconds - MAC_EPOCH_OFFSET)
|
|
179
202
|
end
|
|
180
203
|
end
|
|
181
204
|
end
|
|
@@ -14,8 +14,6 @@ module Fontisan
|
|
|
14
14
|
LOCA_FORMAT_SHORT = 0
|
|
15
15
|
LOCA_FORMAT_LONG = 1
|
|
16
16
|
MAC_STYLE_REGULAR = 0
|
|
17
|
-
# Seconds between 1904-01-01 (Mac epoch) and 1970-01-01 (Unix epoch)
|
|
18
|
-
MAC_EPOCH_OFFSET = 2_082_844_800
|
|
19
17
|
|
|
20
18
|
# @param font [Fontisan::Ufo::Font]
|
|
21
19
|
# @param glyphs [Array<Fontisan::Ufo::Glyph>] (unused here;
|
|
@@ -76,7 +74,7 @@ module Fontisan
|
|
|
76
74
|
# OpenType head.created/modified are LONGDATETIME seconds
|
|
77
75
|
# since 1904-01-01. If the UFO field is absent, use now.
|
|
78
76
|
def self.time_to_longdatetime(value)
|
|
79
|
-
return
|
|
77
|
+
return Tables::Head.now_longdatetime if value.nil?
|
|
80
78
|
|
|
81
79
|
require "time"
|
|
82
80
|
parsed = nil
|
|
@@ -89,7 +87,7 @@ module Fontisan
|
|
|
89
87
|
parsed = Time.now
|
|
90
88
|
end
|
|
91
89
|
end
|
|
92
|
-
parsed
|
|
90
|
+
Tables::Head.to_longdatetime(parsed)
|
|
93
91
|
end
|
|
94
92
|
private_class_method :time_to_longdatetime
|
|
95
93
|
end
|
|
@@ -48,9 +48,7 @@ module Fontisan
|
|
|
48
48
|
validate_data!(data)
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
|
-
|
|
52
|
-
# The brotli gem doesn't expose mode constants, only quality
|
|
53
|
-
Brotli.deflate(data, quality: quality)
|
|
51
|
+
Brotli.deflate(data, quality: quality, mode: mode)
|
|
54
52
|
rescue StandardError => e
|
|
55
53
|
raise Fontisan::Error,
|
|
56
54
|
"Brotli compression failed: #{e.message}"
|
|
@@ -140,19 +138,6 @@ module Fontisan
|
|
|
140
138
|
"Data must be a String-like object, got #{data.class}"
|
|
141
139
|
end
|
|
142
140
|
end
|
|
143
|
-
|
|
144
|
-
# Convert mode symbol to Brotli constant
|
|
145
|
-
#
|
|
146
|
-
# NOTE: The brotli gem doesn't expose mode constants
|
|
147
|
-
# This method is kept for API compatibility but unused
|
|
148
|
-
#
|
|
149
|
-
# @param mode [Symbol] Mode symbol
|
|
150
|
-
# @return [Integer] Mode value (unused)
|
|
151
|
-
def brotli_mode(_mode)
|
|
152
|
-
# The brotli gem only accepts quality parameter
|
|
153
|
-
# Mode is not configurable in current version
|
|
154
|
-
0
|
|
155
|
-
end
|
|
156
141
|
end
|
|
157
142
|
end
|
|
158
143
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Utilities
|
|
5
|
+
# Stateless helpers for SFNT byte-alignment padding.
|
|
6
|
+
#
|
|
7
|
+
# OpenType and WOFF/WOFF2 require table data and certain directory
|
|
8
|
+
# blocks to be aligned to 4-byte boundaries with trailing null
|
|
9
|
+
# bytes. The same `(N - (size % N)) % N` arithmetic was duplicated
|
|
10
|
+
# across nine call sites before extraction; this module is the
|
|
11
|
+
# single source of truth.
|
|
12
|
+
#
|
|
13
|
+
# @example Get padding length
|
|
14
|
+
# Padding.boundary(13) # => 3
|
|
15
|
+
# Padding.boundary(13, boundary: 4) # => 3
|
|
16
|
+
# Padding.boundary(16, boundary: 4) # => 0
|
|
17
|
+
#
|
|
18
|
+
# @example Get padded bytes
|
|
19
|
+
# Padding.pad("abc") # => "abc\x00"
|
|
20
|
+
# Padding.pad("abcd") # => "abcd"
|
|
21
|
+
module Padding
|
|
22
|
+
# Count of trailing pad bytes needed to align `size` to `boundary`.
|
|
23
|
+
#
|
|
24
|
+
# @param size [Integer, #bytesize] Either an Integer size, or any
|
|
25
|
+
# object responding to `#bytesize` (e.g. a String).
|
|
26
|
+
# @param boundary [Integer] Alignment boundary in bytes (default
|
|
27
|
+
# `Constants::TABLE_ALIGNMENT`).
|
|
28
|
+
# @return [Integer] Number of pad bytes in 0...boundary-1
|
|
29
|
+
def self.boundary(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
30
|
+
length = size.respond_to?(:bytesize) ? size.bytesize : size
|
|
31
|
+
(boundary - (length % boundary)) % boundary
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Return `bytes` followed by null padding to the requested boundary.
|
|
35
|
+
#
|
|
36
|
+
# @param bytes [String] Binary string to pad.
|
|
37
|
+
# @param boundary [Integer] Alignment boundary in bytes.
|
|
38
|
+
# @return [String] The original string if already aligned, else a
|
|
39
|
+
# new binary string with trailing nulls.
|
|
40
|
+
def self.pad(bytes, boundary: Constants::TABLE_ALIGNMENT)
|
|
41
|
+
pad_count = boundary(bytes, boundary:)
|
|
42
|
+
return bytes if pad_count.zero?
|
|
43
|
+
|
|
44
|
+
out = bytes.dup.force_encoding(Encoding::BINARY)
|
|
45
|
+
out << ("\x00" * pad_count)
|
|
46
|
+
out
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Aligned size of `size` after padding to `boundary`.
|
|
50
|
+
#
|
|
51
|
+
# @param size [Integer, #bytesize] Original size or bytes-like.
|
|
52
|
+
# @param boundary [Integer] Alignment boundary in bytes.
|
|
53
|
+
# @return [Integer] Size after alignment (multiple of `boundary`).
|
|
54
|
+
def self.aligned_size(size, boundary: Constants::TABLE_ALIGNMENT)
|
|
55
|
+
length = size.respond_to?(:bytesize) ? size.bytesize : size
|
|
56
|
+
length + boundary(length, boundary:)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/fontisan/utilities.rb
CHANGED
|
@@ -168,9 +168,7 @@ module Fontisan
|
|
|
168
168
|
_old_modified = io.read(8) # Skip old modified timestamp
|
|
169
169
|
rest = io.read # Remaining data
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
# Difference between 1904 and 1970 (Unix epoch) is 2082844800 seconds
|
|
173
|
-
longdatetime = timestamp.to_i + 2_082_844_800
|
|
171
|
+
longdatetime = Tables::Head.to_longdatetime(timestamp)
|
|
174
172
|
|
|
175
173
|
# Build updated head binary
|
|
176
174
|
output = String.new(encoding: Encoding::BINARY)
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Fontisan
|
|
|
33
33
|
def self.apply!(table_data)
|
|
34
34
|
exclude_tables!(table_data)
|
|
35
35
|
mark_lossless_modifying!(table_data)
|
|
36
|
+
touch_head_modified!(table_data)
|
|
36
37
|
table_data
|
|
37
38
|
end
|
|
38
39
|
|
|
@@ -61,6 +62,45 @@ module Fontisan
|
|
|
61
62
|
head.flags |= Tables::Head::FLAG_LOSSLESS_MODIFYING
|
|
62
63
|
table_data["head"] = head.to_binary_s
|
|
63
64
|
end
|
|
65
|
+
|
|
66
|
+
# Set head.indexToLocFormat to match the chosen output loca format.
|
|
67
|
+
#
|
|
68
|
+
# fontTools' WOFF2 encoder re-picks the loca format (preferring short
|
|
69
|
+
# when glyf fits). The reconstructed SFNT must carry the matching
|
|
70
|
+
# indexToLocFormat in head, or Chrome's OTS rejects the file with
|
|
71
|
+
# "Failed to convert WOFF 2.0 font to SFNT".
|
|
72
|
+
#
|
|
73
|
+
# @param table_data [Hash{String => String}]
|
|
74
|
+
# @param format_code [Integer] 0 (short) or 1 (long)
|
|
75
|
+
def self.set_head_index_to_loc_format!(table_data, format_code)
|
|
76
|
+
return unless table_data.key?("head")
|
|
77
|
+
|
|
78
|
+
head = Tables::Head.read(table_data["head"])
|
|
79
|
+
head.index_to_loc_format = format_code
|
|
80
|
+
table_data["head"] = head.to_binary_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Set head.modified to the current time.
|
|
84
|
+
#
|
|
85
|
+
# Chrome's OTS rejects WOFF2 fonts whose head.modified is not
|
|
86
|
+
# meaningfully later than head.created. Source fonts frequently
|
|
87
|
+
# carry modified == created (or modified within seconds of it),
|
|
88
|
+
# which Chrome rejects. Setting modified to the actual current
|
|
89
|
+
# wall-clock time matches fontTools' behaviour (it sets
|
|
90
|
+
# head.modified = timestampNow() on every save) and gives Chrome
|
|
91
|
+
# a delta large enough to accept.
|
|
92
|
+
#
|
|
93
|
+
# Must be called BEFORE checksum recompute so the modified bytes
|
|
94
|
+
# are included in the checksum.
|
|
95
|
+
#
|
|
96
|
+
# @param table_data [Hash{String => String}]
|
|
97
|
+
def self.touch_head_modified!(table_data)
|
|
98
|
+
return unless table_data.key?("head")
|
|
99
|
+
|
|
100
|
+
head = Tables::Head.read(table_data["head"])
|
|
101
|
+
head.modified_raw = Tables::Head.now_longdatetime
|
|
102
|
+
table_data["head"] = head.to_binary_s
|
|
103
|
+
end
|
|
64
104
|
end
|
|
65
105
|
end
|
|
66
106
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Canonicalizes a source glyf table by padding each glyph to a 4-byte
|
|
6
|
+
# boundary, matching fontTools' WOFF2 encoder behavior.
|
|
7
|
+
#
|
|
8
|
+
# fontTools' WOFF2 writer calls `_normaliseGlyfAndLoca(padding=4)` before
|
|
9
|
+
# transforming the glyf table (see Lib/fontTools/ttLib/woff2.py). Each
|
|
10
|
+
# glyph is recompiled with `recalcBBoxes=False`, which makes
|
|
11
|
+
# `Glyph#compile` return the source glyph bytes verbatim, then the
|
|
12
|
+
# glyf-table-level compile pads each glyph to the configured alignment.
|
|
13
|
+
# The result is a glyf whose bytes match the source byte-for-byte
|
|
14
|
+
# except for added padding (≤ 3 bytes per glyph).
|
|
15
|
+
#
|
|
16
|
+
# Chrome's OpenType Sanitizer (OTS) fails to reconstruct "unpadded"
|
|
17
|
+
# glyf tables — see the long comment in WOFF2Writer.close():
|
|
18
|
+
# https://github.com/google/woff2/issues/15
|
|
19
|
+
# https://github.com/khaledhosny/ots/issues/60
|
|
20
|
+
#
|
|
21
|
+
# The canonical glyf is the bytes the WOFF2 transform encodes, the
|
|
22
|
+
# bytes the decoder reconstructs, the bytes `glyf.origLength` reports,
|
|
23
|
+
# and the bytes the SFNT checksum is computed over. All four must
|
|
24
|
+
# agree.
|
|
25
|
+
class GlyfCanonicalizer
|
|
26
|
+
# @param glyf_data [String] Source glyf table bytes
|
|
27
|
+
# @param loca_data [String] Source loca table bytes
|
|
28
|
+
# @param num_glyphs [Integer] Glyph count from maxp
|
|
29
|
+
# @param source_format [Integer] 0 (short loca) or 1 (long loca) —
|
|
30
|
+
# how to interpret the source loca
|
|
31
|
+
# @param target_format [Integer] 0 (short loca) or 1 (long loca) —
|
|
32
|
+
# how to emit the canonical loca. Defaults to `source_format`.
|
|
33
|
+
def initialize(glyf_data:, loca_data:, num_glyphs:, source_format:,
|
|
34
|
+
target_format: nil)
|
|
35
|
+
@glyf_data = glyf_data
|
|
36
|
+
@loca_data = loca_data
|
|
37
|
+
@num_glyphs = num_glyphs
|
|
38
|
+
@source_format = source_format
|
|
39
|
+
@target_format = target_format || source_format
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Hash{Symbol => String}] `{ glyf:, loca: }` where glyf is
|
|
43
|
+
# padded to 4-byte alignment per glyph and loca is emitted in
|
|
44
|
+
# `target_format`.
|
|
45
|
+
def canonical
|
|
46
|
+
glyf = String.new(encoding: Encoding::BINARY)
|
|
47
|
+
new_offsets = [0]
|
|
48
|
+
|
|
49
|
+
@num_glyphs.times do |i|
|
|
50
|
+
start_offset = source_offsets[i]
|
|
51
|
+
end_offset = source_offsets[i + 1]
|
|
52
|
+
if start_offset < end_offset
|
|
53
|
+
glyf << @glyf_data.byteslice(start_offset, end_offset - start_offset)
|
|
54
|
+
end
|
|
55
|
+
remainder = glyf.bytesize % 4
|
|
56
|
+
glyf << ("\x00" * (4 - remainder)) if remainder.positive?
|
|
57
|
+
new_offsets << glyf.bytesize
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ glyf:, loca: build_loca(new_offsets) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def source_offsets
|
|
66
|
+
@source_offsets ||= begin
|
|
67
|
+
offsets = if @source_format.zero?
|
|
68
|
+
@loca_data.unpack("n*").map { |v| v * 2 }
|
|
69
|
+
else
|
|
70
|
+
@loca_data.unpack("N*")
|
|
71
|
+
end
|
|
72
|
+
if offsets.length != @num_glyphs + 1
|
|
73
|
+
raise InvalidFontError,
|
|
74
|
+
"loca has #{offsets.length} entries, expected #{@num_glyphs + 1}"
|
|
75
|
+
end
|
|
76
|
+
offsets
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_loca(offsets)
|
|
81
|
+
if @target_format.zero?
|
|
82
|
+
offsets.map { |o| o / 2 }.pack("n*")
|
|
83
|
+
else
|
|
84
|
+
offsets.pack("N*")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Computes `head.checksumAdjustment` for the SFNT the WOFF2 decoder
|
|
6
|
+
# reconstructs at runtime.
|
|
7
|
+
#
|
|
8
|
+
# Per OpenType spec ("Calculating Checksums"), checksumAdjustment
|
|
9
|
+
# must be set so the uint32-wise sum of the entire SFNT file (offset
|
|
10
|
+
# table + table directory + all tables) equals the magic
|
|
11
|
+
# 0xB1B0AFBA. Chrome's OTS validates this; a stale value from the
|
|
12
|
+
# source font causes rejection with "Failed to convert WOFF 2.0
|
|
13
|
+
# font to SFNT" once any table has been modified (head.flags bit 11,
|
|
14
|
+
# glyf/loca transformation, etc.).
|
|
15
|
+
#
|
|
16
|
+
# Delegates the per-table uint32 sum to {Utilities::ChecksumCalculator}
|
|
17
|
+
# — that is the single source of truth for the OpenType checksum
|
|
18
|
+
# algorithm. This class orchestrates the per-table layout: building
|
|
19
|
+
# the offset table + table directory bytes, zeroing head's
|
|
20
|
+
# checksumAdjustment field for its own checksum per spec, then
|
|
21
|
+
# subtracting the total from the magic.
|
|
22
|
+
class SfntChecksum
|
|
23
|
+
# Per-table data the checksum is computed over.
|
|
24
|
+
Table = Struct.new(:tag, :bytes, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
# @param flavor [Integer] sfnt version (0x00010000 for TrueType,
|
|
27
|
+
# 0x4F54544F for CFF)
|
|
28
|
+
# @param tables [Array<Table>] reconstructed tables in alphabetical
|
|
29
|
+
# tag order
|
|
30
|
+
def initialize(flavor:, tables:)
|
|
31
|
+
@flavor = flavor
|
|
32
|
+
@tables = tables
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Compute the head.checksumAdjustment value (uint32).
|
|
36
|
+
#
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
def adjustment
|
|
39
|
+
Utilities::ChecksumCalculator.calculate_adjustment(total_checksum)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Whole-SFNT checksum with head.checksumAdjustment treated as 0
|
|
45
|
+
# (per spec, head's own checksum field uses that placeholder).
|
|
46
|
+
def total_checksum
|
|
47
|
+
sum = offset_table_checksum
|
|
48
|
+
sum = (sum + directory_checksum) & 0xFFFFFFFF
|
|
49
|
+
@tables.each do |t|
|
|
50
|
+
sum = (sum + per_table_checksum(t)) & 0xFFFFFFFF
|
|
51
|
+
end
|
|
52
|
+
sum
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checksum of the 12-byte offset table (sfnt version + numTables +
|
|
56
|
+
# searchRange + entrySelector + rangeShift).
|
|
57
|
+
def offset_table_checksum
|
|
58
|
+
Utilities::ChecksumCalculator.calculate_table_checksum(offset_table_bytes)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def offset_table_bytes
|
|
62
|
+
n = @tables.size
|
|
63
|
+
search_range = (2**Integer(Math.log2(n))) * 16
|
|
64
|
+
entry_selector = Integer(Math.log2(n))
|
|
65
|
+
range_shift = n * 16 - search_range
|
|
66
|
+
[@flavor, n, search_range, entry_selector, range_shift]
|
|
67
|
+
.pack("I>S>S>S>S>")
|
|
68
|
+
.ljust(12, "\x00")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Checksum of the table directory (one 16-byte entry per table).
|
|
72
|
+
# Each entry is tag(4) + checksum(4) + offset(4) + length(4).
|
|
73
|
+
def directory_checksum
|
|
74
|
+
Utilities::ChecksumCalculator.calculate_table_checksum(directory_bytes)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def directory_bytes
|
|
78
|
+
cursor = offset_table_bytes.bytesize + @tables.size * 16
|
|
79
|
+
offsets = {}
|
|
80
|
+
@tables.each do |t|
|
|
81
|
+
offsets[t.tag] = cursor
|
|
82
|
+
cursor += Utilities::Padding.aligned_size(t.bytes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
bytes = String.new(encoding: Encoding::BINARY)
|
|
86
|
+
@tables.each do |t|
|
|
87
|
+
bytes << t.tag.encode(Encoding::BINARY).ljust(4, " ")
|
|
88
|
+
bytes << [per_table_checksum(t)].pack("I>")
|
|
89
|
+
bytes << [offsets[t.tag]].pack("I>")
|
|
90
|
+
bytes << [t.bytes.bytesize].pack("I>")
|
|
91
|
+
end
|
|
92
|
+
bytes
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# OpenType per-table checksum. The head table's checksumAdjustment
|
|
96
|
+
# field (offset 8-11) is treated as zero per spec.
|
|
97
|
+
def per_table_checksum(table)
|
|
98
|
+
data = table.bytes
|
|
99
|
+
if table.tag == "head"
|
|
100
|
+
data = "#{data.byteslice(0, 8)}#{['00000000'].pack('H8')}#{data.byteslice(12..)}"
|
|
101
|
+
end
|
|
102
|
+
Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/fontisan/woff2.rb
CHANGED
|
@@ -8,9 +8,11 @@ module Fontisan
|
|
|
8
8
|
autoload :CollectionEncoder, "fontisan/woff2/collection_encoder"
|
|
9
9
|
autoload :Directory, "fontisan/woff2/directory"
|
|
10
10
|
autoload :EncoderRules, "fontisan/woff2/encoder_rules"
|
|
11
|
+
autoload :GlyfCanonicalizer, "fontisan/woff2/glyf_canonicalizer"
|
|
11
12
|
autoload :GlyfLocaReconstruct, "fontisan/woff2/glyf_loca_reconstruct"
|
|
12
13
|
autoload :GlyfLocaTransform, "fontisan/woff2/glyf_loca_transform"
|
|
13
14
|
autoload :HmtxTransformer, "fontisan/woff2/hmtx_transformer"
|
|
15
|
+
autoload :SfntChecksum, "fontisan/woff2/sfnt_checksum"
|
|
14
16
|
autoload :TableTransformer, "fontisan/woff2/table_transformer"
|
|
15
17
|
autoload :TripletCodec, "fontisan/woff2/triplet_codec"
|
|
16
18
|
autoload :UInt255, "fontisan/woff2/uint255"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -673,6 +673,7 @@ files:
|
|
|
673
673
|
- lib/fontisan/utilities.rb
|
|
674
674
|
- lib/fontisan/utilities/brotli_wrapper.rb
|
|
675
675
|
- lib/fontisan/utilities/checksum_calculator.rb
|
|
676
|
+
- lib/fontisan/utilities/padding.rb
|
|
676
677
|
- lib/fontisan/utils.rb
|
|
677
678
|
- lib/fontisan/utils/thread_pool.rb
|
|
678
679
|
- lib/fontisan/validation.rb
|
|
@@ -723,10 +724,12 @@ files:
|
|
|
723
724
|
- lib/fontisan/woff2/collection_encoder.rb
|
|
724
725
|
- lib/fontisan/woff2/directory.rb
|
|
725
726
|
- lib/fontisan/woff2/encoder_rules.rb
|
|
727
|
+
- lib/fontisan/woff2/glyf_canonicalizer.rb
|
|
726
728
|
- lib/fontisan/woff2/glyf_loca_reconstruct.rb
|
|
727
729
|
- lib/fontisan/woff2/glyf_loca_transform.rb
|
|
728
730
|
- lib/fontisan/woff2/header.rb
|
|
729
731
|
- lib/fontisan/woff2/hmtx_transformer.rb
|
|
732
|
+
- lib/fontisan/woff2/sfnt_checksum.rb
|
|
730
733
|
- lib/fontisan/woff2/table_transformer.rb
|
|
731
734
|
- lib/fontisan/woff2/triplet_codec.rb
|
|
732
735
|
- lib/fontisan/woff2/uint255.rb
|