fontisan 0.4.13 → 0.4.14

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.
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Woff2
7
+ # Reconstructs glyf and loca tables from the WOFF2 transformed glyf
8
+ # stream per spec section 5.1. This is the decoder counterpart of
9
+ # {Woff2::GlyfLocaTransform}.
10
+ #
11
+ # The transformed glyf table is split into 7 streams preceded by a
12
+ # 36-byte header (uint16 version + uint16 optionFlags + uint16 numGlyphs
13
+ # + uint16 indexFormat + 7 × uint32 stream sizes). An optional
14
+ # overlapSimpleBitmap follows when optionFlags bit 0 is set.
15
+ #
16
+ # Reference: W3C WOFF2 spec, section 5.1.
17
+ class GlyfLocaReconstruct
18
+ # TrueType simple-glyph flag bits per OpenType spec.
19
+ FLAG_ON_CURVE = 0x01
20
+ FLAG_X_SHORT = 0x02
21
+ FLAG_Y_SHORT = 0x04
22
+ FLAG_REPEAT = 0x08
23
+ FLAG_X_SAME_OR_POS = 0x10
24
+ FLAG_Y_SAME_OR_POS = 0x20
25
+ FLAG_OVERLAP_SIMPLE = 0x40
26
+
27
+ # TrueType composite-glyph flag bits.
28
+ ARG_1_AND_2_ARE_WORDS = 0x0001
29
+ WE_HAVE_A_SCALE = 0x0008
30
+ MORE_COMPONENTS = 0x0020
31
+ WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
32
+ WE_HAVE_A_TWO_BY_TWO = 0x0080
33
+ WE_HAVE_INSTRUCTIONS = 0x0100
34
+
35
+ # 8-byte header + 7 × 4-byte stream sizes.
36
+ HEADER_SIZE = 36
37
+
38
+ attr_reader :num_glyphs, :index_format
39
+
40
+ # @param transformed_glyf [String] bytes of the transformed glyf table
41
+ # @param num_glyphs [Integer] from maxp
42
+ # @param index_format [Integer] 0 (short) or 1 (long), from head
43
+ def initialize(transformed_glyf:, num_glyphs:, index_format:)
44
+ @data = transformed_glyf
45
+ @num_glyphs = num_glyphs
46
+ @index_format = index_format
47
+ end
48
+
49
+ # Reconstruct glyf and loca tables.
50
+ #
51
+ # @return [Hash{Symbol => String}] `{ glyf:, loca: }`
52
+ def reconstruct
53
+ header = parse_header
54
+ streams = read_streams(header)
55
+
56
+ glyf = String.new(encoding: Encoding::BINARY)
57
+ offsets = [0]
58
+
59
+ num_glyphs.times do |glyph_id|
60
+ glyph = decode_glyph(glyph_id, streams)
61
+ glyf << glyph
62
+ offsets << glyf.bytesize
63
+ end
64
+
65
+ { glyf:, loca: build_loca(offsets) }
66
+ end
67
+
68
+ private
69
+
70
+ # Parsed header. Holds the 4 uint16 fields plus the 7 uint32 stream
71
+ # sizes. Stored as a Hash so the keys document themselves.
72
+ def parse_header
73
+ version, option_flags, ng, idx_fmt = @data[0, 8].unpack("S>S>S>S>")
74
+ sizes = @data[8, 28].unpack("L>7")
75
+
76
+ if ng != @num_glyphs
77
+ raise InvalidFontError,
78
+ "WOFF2 glyf numGlyphs mismatch: header says #{ng}, expected #{@num_glyphs}"
79
+ end
80
+ if idx_fmt != @index_format
81
+ raise InvalidFontError,
82
+ "WOFF2 glyf indexFormat mismatch: header says #{idx_fmt}, expected #{@index_format}"
83
+ end
84
+
85
+ {
86
+ version:,
87
+ option_flags:,
88
+ has_overlap_bitmap: (option_flags & 0x01).nonzero?,
89
+ stream_sizes: sizes,
90
+ }
91
+ end
92
+
93
+ # Slice the 7 streams out of the data block and set up per-stream
94
+ # StringIO cursors. The bboxStream is split into bboxBitmap (fixed
95
+ # size per numGlyphs) and bboxStream (variable, conditionally read
96
+ # per glyph). The optional overlapSimpleBitmap is read at the end.
97
+ def read_streams(header)
98
+ pos = HEADER_SIZE
99
+ bitmap_size = ((@num_glyphs + 31) >> 5) << 2
100
+ overlap_size = header[:has_overlap_bitmap] ? ((@num_glyphs + 7) >> 3) : 0
101
+
102
+ names = %i[n_contour n_points flags glyph composite bbox instructions]
103
+ streams = {}
104
+ names.each_with_index do |name, i|
105
+ size = header[:stream_sizes][i]
106
+ streams[name] = StringIO.new(@data[pos, size])
107
+ pos += size
108
+ end
109
+
110
+ # bboxStream begins with bboxBitmap (4 × floor((numGlyphs+31)/32)).
111
+ bbox_blob = streams[:bbox].string
112
+ streams[:bbox_bitmap] = bbox_blob[0, bitmap_size] || ""
113
+ streams[:bbox_entries] = StringIO.new(bbox_blob[bitmap_size..] || "")
114
+
115
+ # overlapSimpleBitmap follows all streams when optionFlags bit 0 set.
116
+ streams[:overlap_bitmap] = header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
117
+
118
+ # nContour is read positionally (int16 per glyph).
119
+ nc_data = streams[:n_contour].string
120
+ streams[:n_contour_values] = nc_data.unpack("s>*")
121
+
122
+ streams
123
+ end
124
+
125
+ def decode_glyph(glyph_id, streams)
126
+ num_contours = streams[:n_contour_values][glyph_id]
127
+
128
+ case num_contours
129
+ when 0
130
+ # Empty glyph: just an entry in loca (loca[n] == loca[n-1]).
131
+ ""
132
+ when -1
133
+ decode_composite(glyph_id, streams)
134
+ else
135
+ decode_simple(glyph_id, num_contours, streams)
136
+ end
137
+ end
138
+
139
+ # Decode a simple glyph per spec section 5.1 "Decoding of Simple Glyphs".
140
+ def decode_simple(glyph_id, num_contours, streams)
141
+ # Step 1: read num_contours 255UInt16 values from nPointsStream to
142
+ # build endPtsOfContours[].
143
+ end_pts = []
144
+ cumulative = -1
145
+ num_contours.times do
146
+ pts_of_contour = read_255_uint16(streams[:n_points])
147
+ cumulative += pts_of_contour
148
+ end_pts << cumulative
149
+ end
150
+ total_points = end_pts.last + 1
151
+
152
+ # Step 2: read total_points flag bytes from flagStream (triplet flags).
153
+ triplet_flags = streams[:flags].read(total_points).bytes
154
+
155
+ # Step 3: read coordinate triplets from glyphStream (1-4 bytes per
156
+ # point depending on flag). Decode to (dx, dy) deltas.
157
+ xs = []
158
+ ys = []
159
+ on_curve_flags = []
160
+ x = 0
161
+ y = 0
162
+ triplet_flags.each do |flag|
163
+ dx, dy, oc = TripletCodec.decode(flag, read_triplet_payload(flag, streams[:glyph]))
164
+ x += dx
165
+ y += dy
166
+ xs << x
167
+ ys << y
168
+ on_curve_flags << (oc ? FLAG_ON_CURVE : 0)
169
+ end
170
+
171
+ # Step 4-5: read instructionLength from glyphStream, bytes from instructionStream.
172
+ inst_len = read_255_uint16(streams[:glyph])
173
+ instructions = streams[:instructions].read(inst_len)
174
+
175
+ # Bbox: emit explicit bbox if bboxBitmap bit is set for this glyph,
176
+ # otherwise recalculate from coordinates.
177
+ bbox = if bbox_bit_set?(streams[:bbox_bitmap], glyph_id)
178
+ read_bbox(streams[:bbox_entries])
179
+ else
180
+ calc_bounds(xs, ys) || [0, 0, 0, 0]
181
+ end
182
+
183
+ # Re-encode in standard TrueType format.
184
+ build_simple_glyph_bytes(num_contours:, bbox:, end_pts:,
185
+ on_curve_flags:, xs:, ys:, instructions:,
186
+ overlap: overlap_bit_set?(streams[:overlap_bitmap], glyph_id))
187
+ end
188
+
189
+ def decode_composite(_glyph_id, streams)
190
+ # Composite components are stored verbatim in compositeStream. Walk
191
+ # the component records to find their total byte length, then copy.
192
+ # Per spec section 5.1 "Decoding of Composite Glyphs": each component
193
+ # has a uint16 flags word, then 4-14 argument bytes.
194
+ composite_io = streams[:composite]
195
+ component_start = composite_io.pos
196
+ have_instructions = false
197
+ loop do
198
+ break if composite_io.eof?
199
+
200
+ flags = read_uint16_io(composite_io)
201
+ composite_io.read(2) # glyphIndex
202
+ composite_io.read((flags & ARG_1_AND_2_ARE_WORDS).nonzero? ? 4 : 2)
203
+ composite_io.read(composite_transform_size(flags))
204
+ have_instructions = (flags & WE_HAVE_INSTRUCTIONS).nonzero?
205
+ break if (flags & MORE_COMPONENTS).zero?
206
+ end
207
+ component_end = composite_io.pos
208
+ component_bytes = composite_io.string[component_start...component_end]
209
+
210
+ instructions = String.new(encoding: Encoding::BINARY)
211
+ if have_instructions
212
+ inst_len = read_255_uint16(streams[:glyph])
213
+ instructions = streams[:instructions].read(inst_len)
214
+ end
215
+
216
+ # Composite glyphs always have explicit bbox in bboxStream.
217
+ bbox = read_bbox(streams[:bbox_entries])
218
+
219
+ # num_contours = -1 (int16), bbox 4 × int16, components, optional instructions
220
+ out = String.new(encoding: Encoding::BINARY)
221
+ out << [-1].pack("s>")
222
+ out << bbox.pack("s>*")
223
+ out << component_bytes
224
+ # If the last component had WE_HAVE_INSTRUCTIONS, the standard
225
+ # TrueType composite format expects a uint16 instructionLength
226
+ # followed by the instruction bytes.
227
+ if have_instructions
228
+ out << [instructions.bytesize].pack("n")
229
+ out << instructions
230
+ end
231
+ out
232
+ end
233
+
234
+ def composite_transform_size(flags)
235
+ if (flags & WE_HAVE_A_SCALE).nonzero? then 2
236
+ elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE).nonzero? then 4
237
+ elsif (flags & WE_HAVE_A_TWO_BY_TWO).nonzero? then 8
238
+ else
239
+ 0
240
+ end
241
+ end
242
+
243
+ # Read 1-4 payload bytes for a triplet based on the flag's index bits.
244
+ def read_triplet_payload(flag, io)
245
+ idx = flag & 0x7F
246
+ n_bytes = if idx < 84 then 1
247
+ elsif idx < 120 then 2
248
+ elsif idx < 124 then 3
249
+ else 4
250
+ end
251
+ bytes = io.read(n_bytes)
252
+ bytes ? bytes.bytes : Array.new(n_bytes, 0)
253
+ end
254
+
255
+ def read_255_uint16(io)
256
+ return 0 if io.eof?
257
+
258
+ code = io.read(1)&.unpack1("C")
259
+ return 0 unless code
260
+
261
+ case code
262
+ when 0..252 then code
263
+ when 253
264
+ b = io.read(1)&.unpack1("C") || 0
265
+ 253 + b
266
+ when 254
267
+ io.read(2)&.unpack1("n") || 0
268
+
269
+ when 255
270
+ v = io.read(2)&.unpack1("n") || 0
271
+ v + 506
272
+ end
273
+ end
274
+
275
+ def read_uint16_io(io)
276
+ io.read(2)&.unpack1("n") || 0
277
+ end
278
+
279
+ def bbox_bit_set?(bbox_bitmap, glyph_id)
280
+ return false if bbox_bitmap.nil? || bbox_bitmap.empty?
281
+
282
+ byte_idx = glyph_id >> 3
283
+ bit_idx = glyph_id & 7
284
+ (bbox_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
285
+ end
286
+
287
+ def overlap_bit_set?(overlap_bitmap, glyph_id)
288
+ return false unless overlap_bitmap && !overlap_bitmap.empty?
289
+
290
+ byte_idx = glyph_id >> 3
291
+ bit_idx = glyph_id & 7
292
+ (overlap_bitmap.getbyte(byte_idx) & (0x80 >> bit_idx)).nonzero?
293
+ end
294
+
295
+ def read_bbox(io)
296
+ bytes = io.read(8)
297
+ return [0, 0, 0, 0] unless bytes&.bytesize == 8
298
+
299
+ bytes.unpack("s>*")
300
+ end
301
+
302
+ def calc_bounds(xs, ys)
303
+ return nil if xs.empty?
304
+
305
+ [xs.min, ys.min, xs.max, ys.max]
306
+ end
307
+
308
+ # Build a standard TrueType simple-glyph record from per-point arrays.
309
+ # Re-encodes coordinates using the standard TrueType flag scheme based
310
+ # on per-point delta magnitudes (per OFF section 5.3.3 spec rules).
311
+ def build_simple_glyph_bytes(num_contours:, bbox:, end_pts:, on_curve_flags:,
312
+ xs:, ys:, instructions:, overlap:)
313
+ out = String.new(encoding: Encoding::BINARY)
314
+ out << [num_contours].pack("s>")
315
+ out << bbox.pack("s>*")
316
+ out << end_pts.pack("n*")
317
+ out << [instructions.bytesize].pack("n")
318
+ out << instructions
319
+
320
+ prev_x = prev_y = 0
321
+ tt_flags = []
322
+ x_bytes = String.new(encoding: Encoding::BINARY)
323
+ y_bytes = String.new(encoding: Encoding::BINARY)
324
+
325
+ xs.each_with_index do |x, i|
326
+ y = ys[i]
327
+ dx = x - prev_x
328
+ dy = y - prev_y
329
+ prev_x = x
330
+ prev_y = y
331
+
332
+ base = on_curve_flags[i]
333
+ base |= FLAG_OVERLAP_SIMPLE if overlap && i.zero?
334
+
335
+ flag = encode_axis_bits(base, x_bytes, short_bit: FLAG_X_SHORT,
336
+ same_bit: FLAG_X_SAME_OR_POS,
337
+ value: dx)
338
+ flag = encode_axis_bits(flag, y_bytes, short_bit: FLAG_Y_SHORT,
339
+ same_bit: FLAG_Y_SAME_OR_POS,
340
+ value: dy)
341
+ tt_flags << flag
342
+ end
343
+
344
+ out << encode_flags_rle(tt_flags)
345
+ out << x_bytes
346
+ out << y_bytes
347
+ out
348
+ end
349
+
350
+ # Encode one axis delta into standard TrueType byte layout. Returns
351
+ # the updated flag value (X bits if short_bit is FLAG_X_SHORT, Y bits
352
+ # if short_bit is FLAG_Y_SHORT).
353
+ def encode_axis_bits(base, out, short_bit:, same_bit:, value:)
354
+ if value.zero?
355
+ base | same_bit
356
+ elsif value.abs <= 255
357
+ flag = base | short_bit
358
+ flag |= same_bit if value.positive?
359
+ out << (value.abs & 0xFF)
360
+ flag
361
+ else
362
+ out << [value & 0xFFFF].pack("n")
363
+ base
364
+ end
365
+ end
366
+
367
+ # Run-length encode the flags array per OFF spec: when two or more
368
+ # consecutive flags are identical, emit REPEAT_FLAG followed by count.
369
+ def encode_flags_rle(flags)
370
+ out = String.new(encoding: Encoding::BINARY)
371
+ i = 0
372
+ while i < flags.size
373
+ flag = flags[i]
374
+ count = 1
375
+ while i + count < flags.size && flags[i + count] == flag && count < 255
376
+ count += 1
377
+ end
378
+ if count > 1
379
+ out << (flag | FLAG_REPEAT).chr(Encoding::BINARY)
380
+ out << count.chr(Encoding::BINARY)
381
+ i += count
382
+ else
383
+ out << flag.chr(Encoding::BINARY)
384
+ i += 1
385
+ end
386
+ end
387
+ out
388
+ end
389
+
390
+ # Build loca table from accumulated glyph offsets per spec section 5.3.
391
+ def build_loca(offsets)
392
+ if @index_format.zero?
393
+ offsets.map { |o| o / 2 }.pack("n*")
394
+ else
395
+ offsets.pack("N*")
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end
@@ -41,17 +41,21 @@ module Fontisan
41
41
  # Simple glyph records start with int16 numberOfContours + 4 int16 bbox.
42
42
  SIMPLE_HEADER_SIZE = 10
43
43
 
44
- attr_reader :num_glyphs, :index_format
44
+ attr_reader :num_glyphs, :source_index_format
45
45
 
46
46
  # @param glyf_data [String] raw glyf table bytes
47
47
  # @param loca_data [String] raw loca table bytes
48
48
  # @param num_glyphs [Integer] from maxp
49
- # @param index_format [Integer] 0 (short) or 1 (long), from head
49
+ # @param index_format [Integer] 0 (short) or 1 (long), from head.
50
+ # Used to parse the source loca; the WOFF2 transformed glyf
51
+ # always emits indexFormat=0 (Chrome OTS silently rejects any
52
+ # other value, even though WOFF2 spec section 5.1 allows the
53
+ # source's indexToLocFormat).
50
54
  def initialize(glyf_data:, loca_data:, num_glyphs:, index_format:)
51
55
  @glyf_data = glyf_data
52
56
  @loca_data = loca_data
53
57
  @num_glyphs = num_glyphs
54
- @index_format = index_format
58
+ @source_index_format = index_format
55
59
  end
56
60
 
57
61
  # Encode the glyf/loca transform, returning the transformed glyf bytes.
@@ -251,7 +255,10 @@ module Fontisan
251
255
  out << [0].pack("S>") # version
252
256
  out << [(s.has_overlap? ? 1 : 0)].pack("S>") # optionFlags
253
257
  out << [@num_glyphs].pack("S>")
254
- out << [@index_format].pack("S>")
258
+ # Always emit indexFormat=0 per Chrome OTS requirement. WOFF2 spec
259
+ # section 5.1 allows the source's indexToLocFormat, but Chrome's
260
+ # OTS silently rejects any value other than 0.
261
+ out << [0].pack("S>")
255
262
  out << [s.n_contour.bytesize].pack("L>")
256
263
  out << [s.n_points.bytesize].pack("L>")
257
264
  out << [s.flags.bytesize].pack("L>")
@@ -271,7 +278,7 @@ module Fontisan
271
278
  end
272
279
 
273
280
  def parse_loca_offsets
274
- if @index_format.zero?
281
+ if @source_index_format.zero?
275
282
  @loca_data.unpack("n*").map { |v| v * 2 }
276
283
  else
277
284
  @loca_data.unpack("N*")
@@ -4,10 +4,11 @@
4
4
 
5
5
  module Fontisan
6
6
  module Woff2
7
+ autoload :CollectionEncoder, "fontisan/woff2/collection_encoder"
7
8
  autoload :Directory, "fontisan/woff2/directory"
8
9
  autoload :EncoderRules, "fontisan/woff2/encoder_rules"
10
+ autoload :GlyfLocaReconstruct, "fontisan/woff2/glyf_loca_reconstruct"
9
11
  autoload :GlyfLocaTransform, "fontisan/woff2/glyf_loca_transform"
10
- autoload :GlyfTransformer, "fontisan/woff2/glyf_transformer"
11
12
  autoload :HmtxTransformer, "fontisan/woff2/hmtx_transformer"
12
13
  autoload :TableTransformer, "fontisan/woff2/table_transformer"
13
14
  autoload :TripletCodec, "fontisan/woff2/triplet_codec"
@@ -538,59 +538,42 @@ module Fontisan
538
538
  # @param decompressed_tables [Hash<String, String>] Decompressed tables
539
539
  # @return [void] Modifies decompressed_tables in place
540
540
  def self.apply_transformations!(table_entries, decompressed_tables)
541
- # Find entries that need transformation
542
541
  glyf_entry = table_entries.find { |e| e.tag == "glyf" }
543
542
  hmtx_entry = table_entries.find { |e| e.tag == "hmtx" }
544
543
 
545
- # Get required metadata for transformations
546
544
  maxp_data = decompressed_tables["maxp"]
547
545
  hhea_data = decompressed_tables["hhea"]
546
+ head_data = decompressed_tables["head"]
548
547
 
549
- return unless maxp_data && hhea_data
548
+ return unless maxp_data && hhea_data && head_data
550
549
 
551
- # Parse num_glyphs from maxp table
552
550
  # maxp format: version(4) + numGlyphs(2) + ...
553
551
  num_glyphs = maxp_data[4, 2].unpack1("n")
554
-
555
- # Parse numberOfHMetrics from hhea table
556
552
  # hhea format: ... + numberOfHMetrics(2) at offset 34
557
553
  number_of_h_metrics = hhea_data[34, 2].unpack1("n")
558
-
559
- # Check if this is a variable font by checking for fvar table
560
- variable_font = table_entries.any? { |e| e.tag == "fvar" }
561
-
562
- # Transform glyf/loca if needed
563
- # transform_length is only set when table is actually transformed
564
- # Check that transform_length exists and is greater than 0
565
- if glyf_entry&.instance_variable_defined?(:@transform_length) &&
566
- glyf_entry.transform_length&.positive?
567
- transformed_glyf = decompressed_tables["glyf"]
568
-
569
- if transformed_glyf
570
- result = Woff2::GlyfTransformer.reconstruct(
571
- transformed_glyf,
572
- num_glyphs,
573
- variable_font: variable_font,
574
- )
575
- decompressed_tables["glyf"] = result[:glyf]
576
- decompressed_tables["loca"] = result[:loca]
577
- end
554
+ # head format: ... + indexToLocFormat(2) at offset 50
555
+ index_format = head_data[50, 2].unpack1("n")
556
+
557
+ # Reconstruct glyf/loca per spec section 5.1/5.3 when glyf is
558
+ # transformed. Per spec section 4.1, transformLength is present IFF
559
+ # the table is transformed, so a non-nil value indicates transform.
560
+ if glyf_entry&.transform_length && decompressed_tables["glyf"]
561
+ result = Woff2::GlyfLocaReconstruct.new(
562
+ transformed_glyf: decompressed_tables["glyf"],
563
+ num_glyphs:,
564
+ index_format:,
565
+ ).reconstruct
566
+ decompressed_tables["glyf"] = result[:glyf]
567
+ decompressed_tables["loca"] = result[:loca]
578
568
  end
579
569
 
580
- # Transform hmtx if needed
581
- # transform_length is only set when table is actually transformed
582
- # Check that transform_length exists and is greater than 0
583
- if hmtx_entry&.instance_variable_defined?(:@transform_length) &&
584
- hmtx_entry.transform_length&.positive?
585
- transformed_hmtx = decompressed_tables["hmtx"]
586
-
587
- if transformed_hmtx
588
- decompressed_tables["hmtx"] = Woff2::HmtxTransformer.reconstruct(
589
- transformed_hmtx,
590
- num_glyphs,
591
- number_of_h_metrics,
592
- )
593
- end
570
+ # Reconstruct hmtx per spec section 5.4 when hmtx is transformed.
571
+ if hmtx_entry&.transform_length && decompressed_tables["hmtx"]
572
+ decompressed_tables["hmtx"] = Woff2::HmtxTransformer.reconstruct(
573
+ decompressed_tables["hmtx"],
574
+ num_glyphs,
575
+ number_of_h_metrics,
576
+ )
594
577
  end
595
578
  end
596
579
 
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.13
4
+ version: 0.4.14
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-05 00:00:00.000000000 Z
11
+ date: 2026-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -718,10 +718,11 @@ files:
718
718
  - lib/fontisan/version.rb
719
719
  - lib/fontisan/version.rb.orig
720
720
  - lib/fontisan/woff2.rb
721
+ - lib/fontisan/woff2/collection_encoder.rb
721
722
  - lib/fontisan/woff2/directory.rb
722
723
  - lib/fontisan/woff2/encoder_rules.rb
724
+ - lib/fontisan/woff2/glyf_loca_reconstruct.rb
723
725
  - lib/fontisan/woff2/glyf_loca_transform.rb
724
- - lib/fontisan/woff2/glyf_transformer.rb
725
726
  - lib/fontisan/woff2/header.rb
726
727
  - lib/fontisan/woff2/hmtx_transformer.rb
727
728
  - lib/fontisan/woff2/table_transformer.rb