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.
@@ -1,716 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "stringio"
4
-
5
- module Fontisan
6
- module Woff2
7
- # Reconstructs glyf and loca tables from WOFF2 transformed format
8
- #
9
- # WOFF2 glyf table transformation splits glyph data into separate streams
10
- # for better compression. This transformer reconstructs the standard
11
- # `glyf` and `loca` table formats from the transformed data.
12
- #
13
- # Transformation format (Section 5 of WOFF2 spec):
14
- # - Separate streams for nContour, nPoints, flags, x-coords, y-coords
15
- # - Variable-length integer encoding (255UInt16)
16
- # - Composite glyph components stored separately
17
- #
18
- # See: https://www.w3.org/TR/WOFF2/#glyf_table_format
19
- #
20
- # @example Reconstructing tables
21
- # result = GlyfTransformer.reconstruct(transformed_data, num_glyphs)
22
- # glyf_data = result[:glyf]
23
- # loca_data = result[:loca]
24
- class GlyfTransformer
25
- # Glyph flags
26
- ON_CURVE_POINT = 0x01
27
- X_SHORT_VECTOR = 0x02
28
- Y_SHORT_VECTOR = 0x04
29
- REPEAT_FLAG = 0x08
30
- X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR = 0x10
31
- Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR = 0x20
32
-
33
- # Composite glyph flags
34
- ARG_1_AND_2_ARE_WORDS = 0x0001
35
- ARGS_ARE_XY_VALUES = 0x0002
36
- ROUND_XY_TO_GRID = 0x0004
37
- WE_HAVE_A_SCALE = 0x0008
38
- MORE_COMPONENTS = 0x0020
39
- WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
40
- WE_HAVE_A_TWO_BY_TWO = 0x0080
41
- WE_HAVE_INSTRUCTIONS = 0x0100
42
- USE_MY_METRICS = 0x0200
43
- OVERLAP_COMPOUND = 0x0400
44
- HAVE_VARIATIONS = 0x1000 # Variable font variation data follows
45
-
46
- # Reconstruct glyf and loca tables from transformed data
47
- #
48
- # @param transformed_data [String] The transformed glyf table data
49
- # @param num_glyphs [Integer] Number of glyphs from maxp table
50
- # @param variable_font [Boolean] Whether this is a variable font with variation data
51
- # @return [Hash] { glyf: String, loca: String }
52
- # @raise [InvalidFontError] If data is corrupted or invalid
53
- def self.reconstruct(transformed_data, num_glyphs, variable_font: false)
54
- io = StringIO.new(transformed_data)
55
-
56
- # Check minimum size for header
57
- if io.size < 8
58
- raise InvalidFontError,
59
- "Transformed glyf data too small: #{io.size} bytes"
60
- end
61
-
62
- # Read header
63
- read_uint32(io)
64
- num_glyphs_in_data = read_uint16(io)
65
- index_format = read_uint16(io)
66
-
67
- if num_glyphs_in_data != num_glyphs
68
- raise InvalidFontError,
69
- "Glyph count mismatch: expected #{num_glyphs}, got #{num_glyphs_in_data}"
70
- end
71
-
72
- # Read nContour stream
73
- n_contour_data = read_stream_safely(io, "nContour",
74
- variable_font: variable_font)
75
-
76
- # Read nPoints stream
77
- n_points_data = read_stream_safely(io, "nPoints",
78
- variable_font: variable_font)
79
-
80
- # Read flag stream
81
- flag_data = read_stream_safely(io, "flag", variable_font: variable_font)
82
-
83
- # Read glyph stream (coordinates, instructions, composite data)
84
- glyph_data = read_stream_safely(io, "glyph",
85
- variable_font: variable_font)
86
-
87
- # Read composite stream
88
- composite_data = read_stream_safely(io, "composite",
89
- variable_font: variable_font)
90
-
91
- # Read bbox stream
92
- bbox_data = read_stream_safely(io, "bbox", variable_font: variable_font)
93
-
94
- # Read instruction stream
95
- instruction_data = read_stream_safely(io, "instruction",
96
- variable_font: variable_font)
97
-
98
- # Parse streams
99
- n_contours = parse_n_contour_stream(StringIO.new(n_contour_data),
100
- num_glyphs)
101
-
102
- # Reconstruct glyphs
103
- glyphs = reconstruct_glyphs(
104
- n_contours,
105
- StringIO.new(n_points_data),
106
- StringIO.new(flag_data),
107
- StringIO.new(glyph_data),
108
- StringIO.new(composite_data),
109
- StringIO.new(bbox_data),
110
- StringIO.new(instruction_data),
111
- variable_font: variable_font,
112
- )
113
-
114
- # Build glyf and loca tables
115
- build_tables(glyphs, index_format)
116
- end
117
-
118
- # Safely read a stream with bounds checking
119
- #
120
- # @param io [StringIO] Input stream
121
- # @param stream_name [String] Name of stream for error messages
122
- # @param variable_font [Boolean] Whether this is a variable font (allows incomplete streams)
123
- # @return [String] Stream data (empty if not available)
124
- def self.read_stream_safely(io, _stream_name, variable_font: false)
125
- remaining = io.size - io.pos
126
- if remaining < 4
127
- # Not enough data for stream size - return empty stream
128
- return ""
129
- end
130
-
131
- # Read stream size safely
132
- size_bytes = io.read(4)
133
- return "" unless size_bytes && size_bytes.bytesize == 4
134
-
135
- stream_size = size_bytes.unpack1("N")
136
- remaining = io.size - io.pos
137
-
138
- if remaining < stream_size
139
- # Stream size extends beyond available data
140
- # Read what we can
141
- io.read(remaining) || ""
142
- # For variable fonts, we may have incomplete streams - just return what we have
143
-
144
- else
145
- io.read(stream_size) || ""
146
- end
147
- end
148
-
149
- # Read variable-length 255UInt16 integer
150
- #
151
- # Format from WOFF2 spec:
152
- # - value < 253: one byte
153
- # - value == 253: 253 + next uint16
154
- # - value == 254: 253 * 2 + next uint16
155
- # - value == 255: 253 * 3 + next uint16
156
- #
157
- # @param io [StringIO] Input stream
158
- # @return [Integer] Decoded value, or 0 if not enough data
159
- def self.read_255_uint16(io)
160
- return 0 if io.eof? || (io.size - io.pos) < 1
161
-
162
- code_byte = io.read(1)
163
- return 0 unless code_byte && code_byte.bytesize == 1
164
-
165
- code = code_byte.unpack1("C")
166
-
167
- case code
168
- when 255
169
- return 0 if io.eof? || (io.size - io.pos) < 2
170
-
171
- value_bytes = io.read(2)
172
- return 0 unless value_bytes && value_bytes.bytesize == 2
173
-
174
- 759 + value_bytes.unpack1("n") # 253 * 3 + value
175
- when 254
176
- return 0 if io.eof? || (io.size - io.pos) < 2
177
-
178
- value_bytes = io.read(2)
179
- return 0 unless value_bytes && value_bytes.bytesize == 2
180
-
181
- 506 + value_bytes.unpack1("n") # 253 * 2 + value
182
- when 253
183
- return 0 if io.eof? || (io.size - io.pos) < 2
184
-
185
- value_bytes = io.read(2)
186
- return 0 unless value_bytes && value_bytes.bytesize == 2
187
-
188
- 253 + value_bytes.unpack1("n")
189
- else
190
- code
191
- end
192
- end
193
-
194
- # Parse nContour stream
195
- #
196
- # @param io [StringIO] Input stream
197
- # @param num_glyphs [Integer] Number of glyphs
198
- # @return [Array<Integer>] Number of contours per glyph (-1 for composite)
199
- def self.parse_n_contour_stream(io, num_glyphs)
200
- n_contours = []
201
- num_glyphs.times do
202
- # For variable fonts, stream may be incomplete
203
- break if io.eof? || (io.size - io.pos) < 2
204
-
205
- value = read_int16(io)
206
- n_contours << value
207
- end
208
-
209
- # Pad with zeros if we have fewer contours than glyphs
210
- while n_contours.size < num_glyphs
211
- n_contours << 0
212
- end
213
-
214
- n_contours
215
- end
216
-
217
- # Reconstruct all glyphs
218
- #
219
- # @param n_contours [Array<Integer>] Contour counts
220
- # @param n_points_io [StringIO] Points stream
221
- # @param flag_io [StringIO] Flag stream
222
- # @param glyph_io [StringIO] Glyph data stream
223
- # @param composite_io [StringIO] Composite glyph stream
224
- # @param bbox_io [StringIO] Bounding box stream
225
- # @param instruction_io [StringIO] Instruction stream
226
- # @param variable_font [Boolean] Whether this is a variable font
227
- # @return [Array<String>] Reconstructed glyph data
228
- def self.reconstruct_glyphs(n_contours, n_points_io, flag_io, glyph_io,
229
- composite_io, bbox_io, instruction_io, variable_font: false)
230
- glyphs = []
231
-
232
- n_contours.each do |num_contours|
233
- if num_contours.zero?
234
- # Empty glyph
235
- glyphs << ""
236
- elsif num_contours.positive?
237
- # Simple glyph
238
- glyphs << reconstruct_simple_glyph(
239
- num_contours, n_points_io, flag_io,
240
- glyph_io, bbox_io, instruction_io
241
- )
242
- elsif num_contours == -1
243
- # Composite glyph
244
- glyphs << reconstruct_composite_glyph(
245
- composite_io, bbox_io, instruction_io, variable_font: variable_font
246
- )
247
- else
248
- raise InvalidFontError, "Invalid nContours value: #{num_contours}"
249
- end
250
- end
251
-
252
- glyphs
253
- end
254
-
255
- # Reconstruct a simple glyph
256
- #
257
- # @param num_contours [Integer] Number of contours
258
- # @param n_points_io [StringIO] Points stream
259
- # @param flag_io [StringIO] Flag stream
260
- # @param glyph_io [StringIO] Glyph data stream
261
- # @param bbox_io [StringIO] Bounding box stream
262
- # @param instruction_io [StringIO] Instruction stream
263
- # @return [String] Glyph data in standard format
264
- def self.reconstruct_simple_glyph(num_contours, n_points_io, flag_io,
265
- glyph_io, bbox_io, instruction_io)
266
- # Read end points of contours
267
- end_pts_of_contours = []
268
- max_points_per_glyph = 100000 # Sanity limit
269
-
270
- num_contours.times do
271
- if end_pts_of_contours.empty?
272
- val = read_255_uint16(n_points_io)
273
- end_pts_of_contours << val
274
- else
275
- delta = read_255_uint16(n_points_io)
276
- next_end_pt = end_pts_of_contours.last + delta + 1
277
-
278
- # Sanity check to prevent explosion from corrupted data
279
- if delta > 10000 || next_end_pt > max_points_per_glyph
280
- # Data appears corrupted, stop reading
281
- break
282
- end
283
-
284
- end_pts_of_contours << next_end_pt
285
- end
286
- end
287
-
288
- # Handle case where stream was corrupted
289
- if end_pts_of_contours.empty?
290
- # Return minimal empty glyph
291
- return ["\x00\x00"].pack("n") * 5 # Empty glyph: num_contours=0, bbox=0
292
- end
293
-
294
- total_points = [end_pts_of_contours.last + 1, max_points_per_glyph].min
295
-
296
- # Read flags
297
- flags = read_flags(flag_io, total_points)
298
-
299
- # Read coordinates
300
- x_coordinates = read_coordinates(glyph_io, flags, X_SHORT_VECTOR,
301
- X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR)
302
- y_coordinates = read_coordinates(glyph_io, flags, Y_SHORT_VECTOR,
303
- Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR)
304
-
305
- # Read bounding box safely
306
- bbox_remaining = bbox_io.size - bbox_io.pos
307
- if bbox_remaining < 8
308
- # Not enough data, use default bounding box
309
- x_min = y_min = x_max = y_max = 0
310
- else
311
- bbox_bytes = bbox_io.read(8)
312
- if bbox_bytes && bbox_bytes.bytesize == 8
313
- x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
314
- # Convert to signed
315
- x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
316
- y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
317
- x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
318
- y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
319
- else
320
- x_min = y_min = x_max = y_max = 0
321
- end
322
- end
323
-
324
- # Read instructions safely
325
- instruction_length = 0
326
- instructions = ""
327
-
328
- inst_remaining = instruction_io.size - instruction_io.pos
329
- if inst_remaining >= 2
330
- inst_length_data = read_255_uint16(instruction_io)
331
- if inst_length_data
332
- instruction_length = inst_length_data
333
- if instruction_length.positive?
334
- inst_remaining = instruction_io.size - instruction_io.pos
335
- instructions = if inst_remaining >= instruction_length
336
- instruction_io.read(instruction_length) || ""
337
- else
338
- # Read what we can
339
- instruction_io.read(inst_remaining) || ""
340
- end
341
- end
342
- end
343
- end
344
-
345
- # Build glyph data in standard format
346
- build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
347
- end_pts_of_contours, instructions, flags,
348
- x_coordinates, y_coordinates)
349
- end
350
-
351
- # Reconstruct a composite glyph
352
- #
353
- # @param composite_io [StringIO] Composite stream
354
- # @param bbox_io [StringIO] Bounding box stream
355
- # @param instruction_io [StringIO] Instruction stream
356
- # @param variable_font [Boolean] Whether this is a variable font
357
- # @return [String] Glyph data in standard format
358
- def self.reconstruct_composite_glyph(composite_io, bbox_io,
359
- instruction_io, variable_font: false)
360
- # Track available bytes to prevent EOF errors
361
- composite_size = composite_io.size - composite_io.pos
362
-
363
- # Validate minimum size (at least flags + glyph_index + args)
364
- return "" if composite_size < 8
365
-
366
- # Read bounding box safely
367
- bbox_remaining = bbox_io.size - bbox_io.pos
368
- if bbox_remaining < 8
369
- # Not enough data for bounding box, return empty glyph
370
- return ""
371
- end
372
-
373
- bbox_bytes = bbox_io.read(8)
374
- unless bbox_bytes && bbox_bytes.bytesize == 8
375
- return ""
376
- end
377
-
378
- x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
379
- # Convert to signed
380
- x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
381
- y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
382
- x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
383
- y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
384
-
385
- # Read composite data
386
- composite_data = +""
387
- has_instructions = false
388
- has_variations = false
389
-
390
- loop do
391
- # Check if we have enough bytes for flags and glyph_index
392
- remaining = composite_io.size - composite_io.pos
393
- break if composite_io.eof? || remaining < 4
394
-
395
- # Read flags and glyph_index safely
396
- component_header = composite_io.read(4)
397
- break unless component_header && component_header.bytesize == 4
398
-
399
- flags, glyph_index = component_header.unpack("n2")
400
-
401
- # Write flags and index
402
- composite_data << [flags].pack("n")
403
- composite_data << [glyph_index].pack("n")
404
-
405
- # Read arguments (depend on flags)
406
- remaining = composite_io.size - composite_io.pos
407
- if (flags & ARG_1_AND_2_ARE_WORDS).zero?
408
- break if composite_io.eof? || remaining < 2
409
-
410
- arg_bytes = composite_io.read(2)
411
- break unless arg_bytes && arg_bytes.bytesize == 2
412
-
413
- arg1, arg2 = arg_bytes.unpack("c2")
414
- composite_data << [arg1, arg2].pack("c2")
415
- else
416
- break if composite_io.eof? || remaining < 4
417
-
418
- arg_bytes = composite_io.read(4)
419
- break unless arg_bytes && arg_bytes.bytesize == 4
420
-
421
- arg1, arg2 = arg_bytes.unpack("n2")
422
- # Convert to signed
423
- arg1 = arg1 > 0x7FFF ? arg1 - 0x10000 : arg1
424
- arg2 = arg2 > 0x7FFF ? arg2 - 0x10000 : arg2
425
- composite_data << [arg1, arg2].pack("n2")
426
- end
427
-
428
- # Read transformation matrix (depends on flags) with bounds checking
429
- if (flags & WE_HAVE_A_SCALE) != 0
430
- remaining = composite_io.size - composite_io.pos
431
- break if composite_io.eof? || remaining < 2
432
-
433
- scale_bytes = composite_io.read(2)
434
- break unless scale_bytes && scale_bytes.bytesize == 2
435
-
436
- scale = scale_bytes.unpack1("n")
437
- composite_data << [scale].pack("n")
438
- elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
439
- remaining = composite_io.size - composite_io.pos
440
- break if composite_io.eof? || remaining < 4
441
-
442
- scale_bytes = composite_io.read(4)
443
- break unless scale_bytes && scale_bytes.bytesize == 4
444
-
445
- x_scale, y_scale = scale_bytes.unpack("n2")
446
- composite_data << [x_scale, y_scale].pack("n2")
447
- elsif (flags & WE_HAVE_A_TWO_BY_TWO) != 0
448
- remaining = composite_io.size - composite_io.pos
449
- break if composite_io.eof? || remaining < 8
450
-
451
- matrix_bytes = composite_io.read(8)
452
- break unless matrix_bytes && matrix_bytes.bytesize == 8
453
-
454
- x_scale, scale01, scale10, y_scale = matrix_bytes.unpack("n4")
455
- composite_data << [x_scale, scale01, scale10, y_scale].pack("n4")
456
- end
457
-
458
- # Check for variable font variation data
459
- # Only parse if this is a variable font and the flag is set
460
- if variable_font && (flags & HAVE_VARIATIONS) != 0
461
- has_variations = true
462
- # Read tuple variation count and data
463
- remaining = composite_io.size - composite_io.pos
464
- if !composite_io.eof? && remaining >= 2
465
- # Read tuple count safely
466
- tuple_bytes = composite_io.read(2)
467
- if tuple_bytes && tuple_bytes.bytesize == 2
468
- tuple_count = tuple_bytes.unpack1("n")
469
- composite_data << [tuple_count].pack("n")
470
-
471
- # Each tuple has variation data - read and preserve it
472
- tuple_count.times do
473
- remaining = composite_io.size - composite_io.pos
474
- break if composite_io.eof? || remaining < 4
475
-
476
- # Read variation data (2 int16 values per tuple)
477
- var_bytes = composite_io.read(4)
478
- break unless var_bytes && var_bytes.bytesize == 4
479
-
480
- var1, var2 = var_bytes.unpack("n2")
481
- # Convert to signed if needed
482
- var1 = var1 > 0x7FFF ? var1 - 0x10000 : var1
483
- var2 = var2 > 0x7FFF ? var2 - 0x10000 : var2
484
- composite_data << [var1, var2].pack("n2")
485
- end
486
- end
487
- end
488
- end
489
-
490
- has_instructions = (flags & WE_HAVE_INSTRUCTIONS) != 0
491
-
492
- break if (flags & MORE_COMPONENTS).zero?
493
- end
494
-
495
- # Add instructions if present
496
- instructions = +""
497
- if has_instructions
498
- # Read instruction length safely
499
- remaining = instruction_io.size - instruction_io.pos
500
- if !instruction_io.eof? && remaining >= 2
501
- length_bytes = instruction_io.read(2)
502
- if length_bytes && length_bytes.bytesize == 2
503
- instruction_length = length_bytes.unpack1("n")
504
- if instruction_length.positive?
505
- remaining = instruction_io.size - instruction_io.pos
506
- instructions = if remaining >= instruction_length
507
- instruction_io.read(instruction_length) || ""
508
- else
509
- # Read what we can
510
- instruction_io.read(remaining) || ""
511
- end
512
- end
513
- end
514
- end
515
- end
516
-
517
- # Build composite glyph data
518
- data = +""
519
- data << [-1].pack("n") # numberOfContours = -1
520
- data << [x_min, y_min, x_max, y_max].pack("n4")
521
- data << composite_data
522
- data << [instructions.bytesize].pack("n") if has_instructions
523
- data << instructions if has_instructions
524
-
525
- data
526
- end
527
-
528
- # Read flags with repeat handling
529
- #
530
- # @param io [StringIO] Flag stream
531
- # @param count [Integer] Number of flags to read
532
- # @return [Array<Integer>] Flag values
533
- def self.read_flags(io, count)
534
- flags = []
535
-
536
- while flags.size < count
537
- # Safety check to prevent infinite loops with corrupted streams
538
- if flags.size > 200000
539
- # Stream appears corrupted, pad with zeros
540
- while flags.size < count
541
- flags << 0
542
- end
543
- break
544
- end
545
-
546
- # EOF protection for variable fonts
547
- break if io.eof? || (io.size - io.pos) < 1
548
-
549
- flag = read_uint8(io)
550
- flags << flag
551
-
552
- if (flag & REPEAT_FLAG) != 0
553
- break if io.eof? || (io.size - io.pos) < 1
554
-
555
- repeat_count = read_uint8(io)
556
- # Safety check on repeat count
557
- repeat_count = [repeat_count, 100].min
558
- repeat_count.times { flags << flag }
559
- end
560
- end
561
-
562
- flags
563
- end
564
-
565
- # Read coordinates
566
- #
567
- # @param io [StringIO] Glyph stream
568
- # @param flags [Array<Integer>] Flag values
569
- # @param short_flag [Integer] Flag bit for short vector
570
- # @param same_or_positive_flag [Integer] Flag bit for same/positive
571
- # @return [Array<Integer>] Coordinate values
572
- def self.read_coordinates(io, flags, short_flag, same_or_positive_flag)
573
- coords = []
574
- value = 0
575
-
576
- flags.each do |flag|
577
- # EOF protection
578
- if (flag & short_flag) != 0
579
- break if io.eof? || (io.size - io.pos) < 1
580
-
581
- # Short vector (one byte)
582
- delta = read_uint8(io)
583
- delta = -delta if (flag & same_or_positive_flag).zero?
584
- elsif (flag & same_or_positive_flag) != 0
585
- # Same as previous (delta = 0)
586
- delta = 0
587
- else
588
- break if io.eof? || (io.size - io.pos) < 2
589
-
590
- # Long vector (two bytes, signed)
591
- delta = read_int16(io)
592
- end
593
-
594
- value += delta
595
- coords << value
596
- end
597
-
598
- # Pad with last value if needed
599
- last_val = coords.last || 0
600
- while coords.size < flags.size
601
- coords << last_val
602
- end
603
-
604
- coords
605
- end
606
-
607
- # Build simple glyph data in standard format
608
- #
609
- # @return [String] Glyph data
610
- def self.build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
611
- end_pts, instructions, flags, x_coords, y_coords)
612
- data = +""
613
- data << [num_contours].pack("n")
614
- data << [x_min, y_min, x_max, y_max].pack("n4")
615
-
616
- end_pts.each { |pt| data << [pt].pack("n") }
617
-
618
- data << [instructions.bytesize].pack("n")
619
- data << instructions
620
-
621
- flags.each { |flag| data << [flag].pack("C") }
622
-
623
- # Write x-coordinates
624
- prev_x = 0
625
- x_coords.each do |x|
626
- delta = x - prev_x
627
- prev_x = x
628
-
629
- data << if delta.abs <= 255
630
- [delta.abs].pack("C")
631
- else
632
- [delta].pack("n")
633
- end
634
- end
635
-
636
- # Write y-coordinates
637
- prev_y = 0
638
- y_coords.each do |y|
639
- delta = y - prev_y
640
- prev_y = y
641
-
642
- data << if delta.abs <= 255
643
- [delta.abs].pack("C")
644
- else
645
- [delta].pack("n")
646
- end
647
- end
648
-
649
- data
650
- end
651
-
652
- # Build glyf and loca tables
653
- #
654
- # @param glyphs [Array<String>] Glyph data
655
- # @param index_format [Integer] Loca format (0 = short, 1 = long)
656
- # @return [Hash] { glyf: String, loca: String }
657
- def self.build_tables(glyphs, index_format)
658
- glyf_data = +""
659
- loca_offsets = [0]
660
-
661
- glyphs.each do |glyph|
662
- glyf_data << glyph
663
-
664
- # Add padding to 4-byte boundary
665
- padding = (4 - (glyph.bytesize % 4)) % 4
666
- glyf_data << ("\x00" * padding)
667
-
668
- loca_offsets << glyf_data.bytesize
669
- end
670
-
671
- # Build loca table
672
- loca_data = +""
673
- if index_format.zero?
674
- # Short format (divide offsets by 2)
675
- loca_offsets.each do |offset|
676
- loca_data << [offset / 2].pack("n")
677
- end
678
- else
679
- # Long format
680
- loca_offsets.each do |offset|
681
- loca_data << [offset].pack("N")
682
- end
683
- end
684
-
685
- { glyf: glyf_data, loca: loca_data }
686
- end
687
-
688
- # Helper methods for reading binary data
689
-
690
- def self.read_uint8(io)
691
- io.read(1)&.unpack1("C") || raise(EOFError, "Unexpected end of stream")
692
- end
693
-
694
- def self.read_int8(io)
695
- io.read(1)&.unpack1("c") || raise(EOFError, "Unexpected end of stream")
696
- end
697
-
698
- def self.read_uint16(io)
699
- io.read(2)&.unpack1("n") || raise(EOFError, "Unexpected end of stream")
700
- end
701
-
702
- def self.read_int16(io)
703
- value = read_uint16(io)
704
- value > 0x7FFF ? value - 0x10000 : value
705
- end
706
-
707
- def self.read_uint32(io)
708
- io.read(4)&.unpack1("N") || raise(EOFError, "Unexpected end of stream")
709
- end
710
-
711
- def self.read_f2dot14(io)
712
- read_uint16(io)
713
- end
714
- end
715
- end
716
- end