fontisan 0.4.18 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2991840074aedcf587e7bbc673cd6b26b3c887f5e6b481b83c47d6a43151d287
4
- data.tar.gz: 8cd0901c7e2fe8953e7ea24349c11c63b651ca989abbdfc4243e0ed3dba280f8
3
+ metadata.gz: ec589024241926918e56eb58fbe63cd2b14cc0d82fd92908337f7f2f7ef5c076
4
+ data.tar.gz: f3ef0a193655de4790717559c7bce5722f5d9e7b5548bc5b313cf400a3b2121d
5
5
  SHA512:
6
- metadata.gz: 37053820640d91782777be8982ac22cf0bc09cdcaff60b484161ce711785ebb9462d3c333fe4564699e381db4557656d049f77394b64ecb2935d54e1f80d93be
7
- data.tar.gz: 92d7207a5babbbfb155b4aec2cd9f4adfff4efcbe84886f58480a14596fcfa9941f6e406e4c7e4b3f9a822e7169277767cfd69ed8841ab4de3acfb8bdd64cb9e
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-06-29 07:08:46 UTC using RuboCop version 1.86.1.
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: 21
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: 49
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: 7
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: 557
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: 31
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: 8
109
+ # Offense count: 4
176
110
  # Configuration parameters: CountBlocks, CountModifierForms.
177
111
  Metrics/BlockNesting:
178
- Max: 5
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: 308
121
+ # Offense count: 366
188
122
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
189
123
  Metrics/CyclomaticComplexity:
190
124
  Enabled: false
191
125
 
192
- # Offense count: 849
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: 24
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: 231
137
+ # Offense count: 278
204
138
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
205
139
  Metrics/PerceivedComplexity:
206
140
  Enabled: false
207
141
 
208
- # Offense count: 166
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: 111
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: 128
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: 28
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: 1458
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: 1902
301
+ # Offense count: 2097
368
302
  RSpec/MultipleExpectations:
369
303
  Max: 19
370
304
 
371
- # Offense count: 154
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: 23
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: 427
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 = calculate_padding(table_info[:data].bytesize)
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
- # Pad to 4-byte boundary
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
@@ -34,7 +34,7 @@ module Fontisan
34
34
  glyf_data << glyph_data
35
35
 
36
36
  # Add padding to 4-byte boundary
37
- padding = (4 - (glyf_data.bytesize % 4)) % 4
37
+ padding = Utilities::Padding.boundary(glyf_data)
38
38
  glyf_data << ("\x00" * padding) if padding.positive?
39
39
  end
40
40
 
@@ -571,12 +571,10 @@ module Fontisan
571
571
  data << [1000].pack("n")
572
572
 
573
573
  # Created (LONGDATETIME) - use current time
574
- created_seconds = Time.now.to_i + 2_082_844_800
575
- data << [created_seconds].pack("Q>")
574
+ data << [Tables::Head.now_longdatetime].pack("Q>")
576
575
 
577
576
  # Modified (LONGDATETIME) - use current time
578
- modified_seconds = Time.now.to_i + 2_082_844_800
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
@@ -418,14 +418,12 @@ module Fontisan
418
418
  size += 16 if glyf_transform # synthetic loca directory entry
419
419
 
420
420
  table_data.each_value do |data|
421
- size += data.bytesize
422
- size += (4 - (data.bytesize % 4)) % 4 # pad to 4-byte boundary
421
+ size += Utilities::Padding.aligned_size(data)
423
422
  end
424
423
 
425
424
  if glyf_transform
426
425
  loca_len = glyf_transform[:loca_orig_length]
427
- size += loca_len
428
- size += (4 - (loca_len % 4)) % 4
426
+ size += Utilities::Padding.aligned_size(loca_len)
429
427
  end
430
428
 
431
429
  size
@@ -504,6 +502,12 @@ module Fontisan
504
502
  end
505
503
 
506
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?
507
511
  update_woff2_length!(woff2_data)
508
512
  woff2_data
509
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 = padding_to_4(info[:compressed_length])
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
- t[:original_length] + padding_to_4(t[:original_length])
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
 
@@ -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 = (4 - (data.bytesize % 4)) % 4
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
- # Pad to 4-byte boundary
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
- # Pad to 4-byte boundary
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 = (4 - (length % 4)) % 4
37
+ padding = Utilities::Padding.boundary(length)
38
38
  record = { tag:, checksum:, offset: cursor, length:, data:, padding: }
39
39
  cursor += length + padding
40
40
  record
@@ -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 = (Constants::TABLE_ALIGNMENT - (io.pos % Constants::TABLE_ALIGNMENT)) % Constants::TABLE_ALIGNMENT
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
@@ -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
- # Difference between 1904 and 1970 (Unix epoch) is 2082844800 seconds
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 Time.now.to_i + MAC_EPOCH_OFFSET if value.nil?
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.to_i + MAC_EPOCH_OFFSET
90
+ Tables::Head.to_longdatetime(parsed)
93
91
  end
94
92
  private_class_method :time_to_longdatetime
95
93
  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
@@ -6,5 +6,6 @@ module Fontisan
6
6
  module Utilities
7
7
  autoload :BrotliWrapper, "fontisan/utilities/brotli_wrapper"
8
8
  autoload :ChecksumCalculator, "fontisan/utilities/checksum_calculator"
9
+ autoload :Padding, "fontisan/utilities/padding"
9
10
  end
10
11
  end
@@ -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
- # Convert Time to LONGDATETIME (seconds since 1904-01-01)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.18"
4
+ VERSION = "0.4.19"
5
5
  end
@@ -80,24 +80,25 @@ module Fontisan
80
80
  table_data["head"] = head.to_binary_s
81
81
  end
82
82
 
83
- # Bump head.modified past head.created.
83
+ # Set head.modified to the current time.
84
84
  #
85
- # Chrome's OTS rejects fonts whose modified timestamp is not strictly
86
- # greater than created (source fonts often have modified == created,
87
- # which fails this check). Setting modified = created + 1 is the
88
- # minimal change that satisfies the constraint.
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.
89
92
  #
90
- # Must be called BEFORE checksum recompute so the modified bytes are
91
- # included in the checksum.
93
+ # Must be called BEFORE checksum recompute so the modified bytes
94
+ # are included in the checksum.
92
95
  #
93
96
  # @param table_data [Hash{String => String}]
94
97
  def self.touch_head_modified!(table_data)
95
98
  return unless table_data.key?("head")
96
99
 
97
100
  head = Tables::Head.read(table_data["head"])
98
- return if head.modified_raw.to_i > head.created_raw.to_i
99
-
100
- head.modified_raw = head.created_raw + 1
101
+ head.modified_raw = Tables::Head.now_longdatetime
101
102
  table_data["head"] = head.to_binary_s
102
103
  end
103
104
  end
@@ -1,28 +1,32 @@
1
- # frozen_string_literal: true
1
+ # frozen_string: true
2
2
 
3
3
  module Fontisan
4
4
  module Woff2
5
- # Computes head.checksumAdjustment for the SFNT that the WOFF2 decoder
5
+ # Computes `head.checksumAdjustment` for the SFNT the WOFF2 decoder
6
6
  # reconstructs at runtime.
7
7
  #
8
- # Per OpenType spec, head.checksumAdjustment must be set so that the
9
- # uint32-wise sum of the entire SFNT file (offset table + table
10
- # directory + all tables) equals the magic 0xB1B0AFBA. Chrome's OTS
11
- # validates this; a stale value from the source font causes rejection
12
- # with "Failed to convert WOFF 2.0 font to SFNT" once any table has
13
- # been modified (head.flags bit 11, glyf/loca transformation, etc.).
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.).
14
15
  #
15
- # Reference: OpenType spec, "Calculating Checksums".
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.
16
22
  class SfntChecksum
17
- # Per-table data the checksum is computed over. tag is the 4-byte
18
- # table tag, bytes is the (already reconstructed, post-transform)
19
- # table data the decoder will see.
23
+ # Per-table data the checksum is computed over.
20
24
  Table = Struct.new(:tag, :bytes, keyword_init: true)
21
25
 
22
26
  # @param flavor [Integer] sfnt version (0x00010000 for TrueType,
23
27
  # 0x4F54544F for CFF)
24
28
  # @param tables [Array<Table>] reconstructed tables in alphabetical
25
- # tag order
29
+ # tag order
26
30
  def initialize(flavor:, tables:)
27
31
  @flavor = flavor
28
32
  @tables = tables
@@ -32,12 +36,13 @@ module Fontisan
32
36
  #
33
37
  # @return [Integer]
34
38
  def adjustment
35
- (Constants::CHECKSUM_ADJUSTMENT_MAGIC - total_checksum) & 0xFFFFFFFF
39
+ Utilities::ChecksumCalculator.calculate_adjustment(total_checksum)
36
40
  end
37
41
 
38
42
  private
39
43
 
40
- # Whole-SFNT checksum with head.checksumAdjustment treated as 0.
44
+ # Whole-SFNT checksum with head.checksumAdjustment treated as 0
45
+ # (per spec, head's own checksum field uses that placeholder).
41
46
  def total_checksum
42
47
  sum = offset_table_checksum
43
48
  sum = (sum + directory_checksum) & 0xFFFFFFFF
@@ -50,58 +55,51 @@ module Fontisan
50
55
  # Checksum of the 12-byte offset table (sfnt version + numTables +
51
56
  # searchRange + entrySelector + rangeShift).
52
57
  def offset_table_checksum
58
+ Utilities::ChecksumCalculator.calculate_table_checksum(offset_table_bytes)
59
+ end
60
+
61
+ def offset_table_bytes
53
62
  n = @tables.size
54
63
  search_range = (2**Integer(Math.log2(n))) * 16
55
64
  entry_selector = Integer(Math.log2(n))
56
65
  range_shift = n * 16 - search_range
57
- bytes = [@flavor, n, search_range, entry_selector, range_shift]
66
+ [@flavor, n, search_range, entry_selector, range_shift]
58
67
  .pack("I>S>S>S>S>")
59
- uint32_sum(bytes.ljust(12, "\x00"))
68
+ .ljust(12, "\x00")
60
69
  end
61
70
 
62
71
  # Checksum of the table directory (one 16-byte entry per table).
63
72
  # Each entry is tag(4) + checksum(4) + offset(4) + length(4).
64
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
65
79
  offsets = {}
66
- cursor = 12 + @tables.size * 16
67
80
  @tables.each do |t|
68
81
  offsets[t.tag] = cursor
69
- cursor += padded_size(t.bytes.bytesize)
82
+ cursor += Utilities::Padding.aligned_size(t.bytes)
70
83
  end
84
+
71
85
  bytes = String.new(encoding: Encoding::BINARY)
72
86
  @tables.each do |t|
73
- tag_bytes = t.tag.encode(Encoding::BINARY).ljust(4, " ")
74
- bytes << tag_bytes
87
+ bytes << t.tag.encode(Encoding::BINARY).ljust(4, " ")
75
88
  bytes << [per_table_checksum(t)].pack("I>")
76
89
  bytes << [offsets[t.tag]].pack("I>")
77
90
  bytes << [t.bytes.bytesize].pack("I>")
78
91
  end
79
- uint32_sum(bytes)
92
+ bytes
80
93
  end
81
94
 
82
95
  # OpenType per-table checksum. The head table's checksumAdjustment
83
- # field (offset 8-11) is treated as zero per spec. Tables are
84
- # zero-padded to a 4-byte boundary for checksum, even when their
85
- # actual length is shorter.
96
+ # field (offset 8-11) is treated as zero per spec.
86
97
  def per_table_checksum(table)
87
- bytes = table.bytes.dup.force_encoding(Encoding::BINARY)
98
+ data = table.bytes
88
99
  if table.tag == "head"
89
- bytes = "#{bytes.byteslice(0, 8)}\u0000\u0000\u0000\u0000#{bytes.byteslice(12..)}"
100
+ data = "#{data.byteslice(0, 8)}#{['00000000'].pack('H8')}#{data.byteslice(12..)}"
90
101
  end
91
- uint32_sum(pad_to_4(bytes))
92
- end
93
-
94
- def uint32_sum(bytes)
95
- bytes.unpack("I>*").sum & 0xFFFFFFFF
96
- end
97
-
98
- def pad_to_4(bytes)
99
- pad = (-bytes.bytesize) % 4
100
- pad.zero? ? bytes : bytes + ("\x00" * pad)
101
- end
102
-
103
- def padded_size(size)
104
- (size + 3) & ~3
102
+ Utilities::ChecksumCalculator.calculate_table_checksum(data)
105
103
  end
106
104
  end
107
105
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.18
4
+ version: 0.4.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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