fontisan 0.2.5 → 0.2.6
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 +36 -20
- data/README.adoc +184 -857
- data/lib/fontisan/cli.rb +27 -7
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +7 -2
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -63,7 +63,7 @@ module Fontisan
|
|
|
63
63
|
when Constants::TTC_TAG
|
|
64
64
|
load_from_collection(io, path, font_index, mode: resolved_mode,
|
|
65
65
|
lazy: resolved_lazy)
|
|
66
|
-
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
|
|
66
|
+
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
|
|
67
67
|
TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
68
68
|
when "OTTO"
|
|
69
69
|
OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
@@ -71,6 +71,8 @@ module Fontisan
|
|
|
71
71
|
WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
72
72
|
when "wOF2"
|
|
73
73
|
Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
74
|
+
when Constants::DFONT_RESOURCE_HEADER
|
|
75
|
+
extract_and_load_dfont(io, path, font_index, resolved_mode, resolved_lazy)
|
|
74
76
|
else
|
|
75
77
|
raise InvalidFontError,
|
|
76
78
|
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
|
|
@@ -92,13 +94,25 @@ module Fontisan
|
|
|
92
94
|
|
|
93
95
|
File.open(path, "rb") do |io|
|
|
94
96
|
signature = io.read(4)
|
|
95
|
-
|
|
97
|
+
io.rewind
|
|
98
|
+
|
|
99
|
+
# Check for TTC/OTC signature
|
|
100
|
+
return true if signature == Constants::TTC_TAG
|
|
101
|
+
|
|
102
|
+
# Check for multi-font dfont (suitcase) - only if it's actually a dfont
|
|
103
|
+
if signature == Constants::DFONT_RESOURCE_HEADER
|
|
104
|
+
require_relative "parsers/dfont_parser"
|
|
105
|
+
# Verify it's a valid dfont and has multiple fonts
|
|
106
|
+
return Parsers::DfontParser.dfont?(io) && Parsers::DfontParser.sfnt_count(io) > 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
false
|
|
96
110
|
end
|
|
97
111
|
end
|
|
98
112
|
|
|
99
113
|
# Load a collection object without extracting fonts
|
|
100
114
|
#
|
|
101
|
-
# Returns the collection object (TrueTypeCollection or
|
|
115
|
+
# Returns the collection object (TrueTypeCollection, OpenTypeCollection, or DfontCollection)
|
|
102
116
|
# without extracting individual fonts. Useful for inspecting collection
|
|
103
117
|
# metadata and structure.
|
|
104
118
|
#
|
|
@@ -112,6 +126,10 @@ module Fontisan
|
|
|
112
126
|
# - OTC typically contains OpenType fonts (CFF/CFF2 outlines)
|
|
113
127
|
# - Mixed collections are possible (both TTF and OTF in same collection)
|
|
114
128
|
#
|
|
129
|
+
# dfont (Data Fork Font) is an Apple-specific format that contains Mac
|
|
130
|
+
# font suitcase resources. It can contain multiple SFNT fonts (TrueType
|
|
131
|
+
# or OpenType).
|
|
132
|
+
#
|
|
115
133
|
# Each collection can contain multiple SFNT-format font files, with table
|
|
116
134
|
# deduplication to save space. Individual fonts within a collection are
|
|
117
135
|
# stored at different offsets within the file, each with their own table
|
|
@@ -128,13 +146,16 @@ module Fontisan
|
|
|
128
146
|
# 4. If ANY font is OpenType (CFF), returns OpenTypeCollection
|
|
129
147
|
# 5. Only returns TrueTypeCollection if ALL fonts are TrueType
|
|
130
148
|
#
|
|
149
|
+
# For dfont files, returns DfontCollection.
|
|
150
|
+
#
|
|
131
151
|
# This approach correctly handles:
|
|
132
152
|
# - Homogeneous collections (all TTF or all OTF)
|
|
133
153
|
# - Mixed collections (both TTF and OTF fonts) - uses OpenTypeCollection
|
|
134
154
|
# - Large collections with many fonts (like NotoSerifCJK.ttc with 35 fonts)
|
|
155
|
+
# - dfont suitcases (Apple-specific)
|
|
135
156
|
#
|
|
136
157
|
# @param path [String] Path to the collection file
|
|
137
|
-
# @return [TrueTypeCollection, OpenTypeCollection] The collection object
|
|
158
|
+
# @return [TrueTypeCollection, OpenTypeCollection, DfontCollection] The collection object
|
|
138
159
|
# @raise [Errno::ENOENT] if file does not exist
|
|
139
160
|
# @raise [InvalidFontError] if file is not a collection or type cannot be determined
|
|
140
161
|
#
|
|
@@ -146,10 +167,18 @@ module Fontisan
|
|
|
146
167
|
|
|
147
168
|
File.open(path, "rb") do |io|
|
|
148
169
|
signature = io.read(4)
|
|
170
|
+
io.rewind
|
|
171
|
+
|
|
172
|
+
# Check for dfont
|
|
173
|
+
if signature == Constants::DFONT_RESOURCE_HEADER || dfont_signature?(io)
|
|
174
|
+
require_relative "dfont_collection"
|
|
175
|
+
return DfontCollection.from_file(path)
|
|
176
|
+
end
|
|
149
177
|
|
|
178
|
+
# Check for TTC/OTC
|
|
150
179
|
unless signature == Constants::TTC_TAG
|
|
151
180
|
raise InvalidFontError,
|
|
152
|
-
"File is not a collection (TTC/OTC). Use FontLoader.load instead."
|
|
181
|
+
"File is not a collection (TTC/OTC/dfont). Use FontLoader.load instead."
|
|
153
182
|
end
|
|
154
183
|
|
|
155
184
|
# Read version and num_fonts
|
|
@@ -291,6 +320,50 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
291
320
|
end
|
|
292
321
|
end
|
|
293
322
|
|
|
323
|
+
# Extract and load font from dfont resource fork
|
|
324
|
+
#
|
|
325
|
+
# @param io [IO] Open file handle
|
|
326
|
+
# @param path [String] Path to dfont file
|
|
327
|
+
# @param font_index [Integer] Font index in suitcase
|
|
328
|
+
# @param mode [Symbol] Loading mode
|
|
329
|
+
# @param lazy [Boolean] Lazy loading flag
|
|
330
|
+
# @return [TrueTypeFont, OpenTypeFont] Loaded font
|
|
331
|
+
# @api private
|
|
332
|
+
def self.extract_and_load_dfont(io, path, font_index, mode, lazy)
|
|
333
|
+
require_relative "parsers/dfont_parser"
|
|
334
|
+
|
|
335
|
+
# Extract SFNT data from resource fork
|
|
336
|
+
sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: font_index)
|
|
337
|
+
|
|
338
|
+
# Create StringIO with SFNT data
|
|
339
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
340
|
+
|
|
341
|
+
# Detect SFNT signature
|
|
342
|
+
signature = sfnt_io.read(4)
|
|
343
|
+
sfnt_io.rewind
|
|
344
|
+
|
|
345
|
+
# Read and setup font based on signature
|
|
346
|
+
case signature
|
|
347
|
+
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
|
|
348
|
+
font = TrueTypeFont.read(sfnt_io)
|
|
349
|
+
font.initialize_storage
|
|
350
|
+
font.loading_mode = mode
|
|
351
|
+
font.lazy_load_enabled = lazy
|
|
352
|
+
font.read_table_data(sfnt_io) unless lazy
|
|
353
|
+
font
|
|
354
|
+
when "OTTO"
|
|
355
|
+
font = OpenTypeFont.read(sfnt_io)
|
|
356
|
+
font.initialize_storage
|
|
357
|
+
font.loading_mode = mode
|
|
358
|
+
font.lazy_load_enabled = lazy
|
|
359
|
+
font.read_table_data(sfnt_io) unless lazy
|
|
360
|
+
font
|
|
361
|
+
else
|
|
362
|
+
raise InvalidFontError,
|
|
363
|
+
"Invalid SFNT data in dfont resource (signature: #{signature.inspect})"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
294
367
|
# Pack uint32 value to big-endian bytes
|
|
295
368
|
#
|
|
296
369
|
# @param value [Integer] The uint32 value
|
|
@@ -301,6 +374,18 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
301
374
|
end
|
|
302
375
|
|
|
303
376
|
private_class_method :load_from_collection, :pack_uint32, :env_mode,
|
|
304
|
-
:env_lazy
|
|
377
|
+
:env_lazy, :extract_and_load_dfont
|
|
378
|
+
|
|
379
|
+
# Check if file has dfont signature
|
|
380
|
+
#
|
|
381
|
+
# @param io [IO] Open file handle
|
|
382
|
+
# @return [Boolean] true if dfont
|
|
383
|
+
# @api private
|
|
384
|
+
def self.dfont_signature?(io)
|
|
385
|
+
require_relative "parsers/dfont_parser"
|
|
386
|
+
Parsers::DfontParser.dfont?(io)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private_class_method :dfont_signature?
|
|
305
390
|
end
|
|
306
391
|
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Parsers
|
|
8
|
+
# Parser for Apple dfont (Data Fork Font) resource fork format.
|
|
9
|
+
#
|
|
10
|
+
# dfont files store resource fork data in the data fork, containing
|
|
11
|
+
# TrueType or OpenType SFNT data embedded in a resource fork structure.
|
|
12
|
+
#
|
|
13
|
+
# @example Extract SFNT from dfont
|
|
14
|
+
# File.open("font.dfont", "rb") do |io|
|
|
15
|
+
# sfnt_data = DfontParser.extract_sfnt(io)
|
|
16
|
+
# # sfnt_data is raw SFNT binary
|
|
17
|
+
# end
|
|
18
|
+
class DfontParser
|
|
19
|
+
# Resource fork header structure (16 bytes)
|
|
20
|
+
class ResourceHeader < BinData::Record
|
|
21
|
+
endian :big
|
|
22
|
+
uint32 :resource_data_offset # Offset to resource data section
|
|
23
|
+
uint32 :resource_map_offset # Offset to resource map
|
|
24
|
+
uint32 :resource_data_length # Length of resource data
|
|
25
|
+
uint32 :resource_map_length # Length of resource map
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resource type entry in type list
|
|
29
|
+
class ResourceType < BinData::Record
|
|
30
|
+
endian :big
|
|
31
|
+
string :type_code, length: 4 # Resource type (e.g., 'sfnt')
|
|
32
|
+
uint16 :resource_count_minus_1 # Number of resources - 1
|
|
33
|
+
uint16 :reference_list_offset # Offset to reference list
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Resource reference in reference list
|
|
37
|
+
class ResourceReference < BinData::Record
|
|
38
|
+
endian :big
|
|
39
|
+
uint16 :resource_id # Resource ID
|
|
40
|
+
int16 :name_offset # Offset to name in name list (-1 if none)
|
|
41
|
+
uint8 :attributes # Resource attributes
|
|
42
|
+
bit24 :data_offset # Offset to data (relative to resource data section)
|
|
43
|
+
uint32 :reserved # Reserved (handle to resource in memory)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extract SFNT data from dfont file
|
|
47
|
+
#
|
|
48
|
+
# @param io [IO] Open file handle
|
|
49
|
+
# @param index [Integer] Font index for multi-font suitcases (default: 0)
|
|
50
|
+
# @return [String] Raw SFNT binary data
|
|
51
|
+
# @raise [InvalidFontError] if not valid dfont or index out of range
|
|
52
|
+
def self.extract_sfnt(io, index: 0)
|
|
53
|
+
header = parse_header(io)
|
|
54
|
+
sfnt_resources = find_sfnt_resources(io, header)
|
|
55
|
+
|
|
56
|
+
if sfnt_resources.empty?
|
|
57
|
+
raise InvalidFontError, "No sfnt resources found in dfont file"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if index >= sfnt_resources.length
|
|
61
|
+
raise InvalidFontError,
|
|
62
|
+
"Font index #{index} out of range (dfont has #{sfnt_resources.length} fonts)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
extract_resource_data(io, header, sfnt_resources[index])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Count number of sfnt resources (fonts) in dfont
|
|
69
|
+
#
|
|
70
|
+
# @param io [IO] Open file handle
|
|
71
|
+
# @return [Integer] Number of fonts
|
|
72
|
+
def self.sfnt_count(io)
|
|
73
|
+
header = parse_header(io)
|
|
74
|
+
sfnt_resources = find_sfnt_resources(io, header)
|
|
75
|
+
sfnt_resources.length
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if file is valid dfont resource fork
|
|
79
|
+
#
|
|
80
|
+
# @param io [IO] Open file handle
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def self.dfont?(io)
|
|
83
|
+
io.rewind
|
|
84
|
+
header_bytes = io.read(16)
|
|
85
|
+
io.rewind
|
|
86
|
+
|
|
87
|
+
return false if header_bytes.nil? || header_bytes.length < 16
|
|
88
|
+
|
|
89
|
+
# Basic sanity check on resource fork structure
|
|
90
|
+
data_offset = header_bytes[0..3].unpack1("N")
|
|
91
|
+
map_offset = header_bytes[4..7].unpack1("N")
|
|
92
|
+
|
|
93
|
+
data_offset.positive? && map_offset > data_offset
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse resource fork header
|
|
97
|
+
#
|
|
98
|
+
# @param io [IO] Open file handle
|
|
99
|
+
# @return [ResourceHeader] Parsed header
|
|
100
|
+
# @raise [InvalidFontError] if header invalid
|
|
101
|
+
# @api private
|
|
102
|
+
def self.parse_header(io)
|
|
103
|
+
io.rewind
|
|
104
|
+
ResourceHeader.read(io)
|
|
105
|
+
rescue BinData::ValidityError => e
|
|
106
|
+
raise InvalidFontError, "Invalid dfont resource header: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Find all sfnt resources in resource map
|
|
110
|
+
#
|
|
111
|
+
# @param io [IO] Open file handle
|
|
112
|
+
# @param header [ResourceHeader] Parsed header
|
|
113
|
+
# @return [Array<Hash>] Array of resource info hashes with :id and :offset
|
|
114
|
+
# @api private
|
|
115
|
+
def self.find_sfnt_resources(io, header)
|
|
116
|
+
# Seek to resource map
|
|
117
|
+
io.seek(header.resource_map_offset)
|
|
118
|
+
|
|
119
|
+
# Skip resource map header (22 bytes reserved + 4 bytes attributes + 2 bytes type list offset + 2 bytes name list offset)
|
|
120
|
+
# The actual layout is:
|
|
121
|
+
# - Bytes 0-15: Copy of resource header (16 bytes)
|
|
122
|
+
# - Bytes 16-19: Reserved for handle to next resource map (4 bytes)
|
|
123
|
+
# - Bytes 20-21: Reserved for file reference number (2 bytes)
|
|
124
|
+
# - Bytes 22-23: Resource file attributes (2 bytes)
|
|
125
|
+
# - Bytes 24-25: Offset to type list (2 bytes)
|
|
126
|
+
# - Bytes 26-27: Offset to name list (2 bytes)
|
|
127
|
+
io.seek(header.resource_map_offset + 24)
|
|
128
|
+
|
|
129
|
+
# Read type list offset (relative to start of resource map)
|
|
130
|
+
type_list_offset = io.read(2).unpack1("n")
|
|
131
|
+
|
|
132
|
+
# Seek to type list
|
|
133
|
+
io.seek(header.resource_map_offset + type_list_offset)
|
|
134
|
+
|
|
135
|
+
# Read number of types minus 1
|
|
136
|
+
type_count_minus_1 = io.read(2).unpack1("n")
|
|
137
|
+
type_count = type_count_minus_1 + 1
|
|
138
|
+
|
|
139
|
+
# Find 'sfnt' type in type list
|
|
140
|
+
sfnt_type = nil
|
|
141
|
+
type_count.times do
|
|
142
|
+
type_entry = ResourceType.read(io)
|
|
143
|
+
|
|
144
|
+
if type_entry.type_code == "sfnt"
|
|
145
|
+
sfnt_type = type_entry
|
|
146
|
+
break
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return [] unless sfnt_type
|
|
151
|
+
|
|
152
|
+
# Read reference list for sfnt resources
|
|
153
|
+
reference_list_offset = header.resource_map_offset + type_list_offset + sfnt_type.reference_list_offset
|
|
154
|
+
io.seek(reference_list_offset)
|
|
155
|
+
|
|
156
|
+
resource_count = sfnt_type.resource_count_minus_1 + 1
|
|
157
|
+
resources = []
|
|
158
|
+
|
|
159
|
+
resource_count.times do
|
|
160
|
+
ref = ResourceReference.read(io)
|
|
161
|
+
resources << { id: ref.resource_id, offset: ref.data_offset }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
resources
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Extract resource data at specific offset
|
|
168
|
+
#
|
|
169
|
+
# @param io [IO] Open file handle
|
|
170
|
+
# @param header [ResourceHeader] Parsed header
|
|
171
|
+
# @param resource_info [Hash] Resource info with :offset
|
|
172
|
+
# @return [String] Raw SFNT binary data
|
|
173
|
+
# @api private
|
|
174
|
+
def self.extract_resource_data(io, header, resource_info)
|
|
175
|
+
# Calculate absolute offset to resource data
|
|
176
|
+
# The offset in the reference is relative to the start of the resource data section
|
|
177
|
+
data_offset = header.resource_data_offset + resource_info[:offset]
|
|
178
|
+
|
|
179
|
+
io.seek(data_offset)
|
|
180
|
+
|
|
181
|
+
# Read data length (first 4 bytes of resource data)
|
|
182
|
+
data_length = io.read(4).unpack1("N")
|
|
183
|
+
|
|
184
|
+
# Read the actual data
|
|
185
|
+
io.read(data_length)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private_class_method :parse_header, :find_sfnt_resources,
|
|
189
|
+
:extract_resource_data
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -69,12 +69,6 @@ module Fontisan
|
|
|
69
69
|
# Whether lazy loading is enabled
|
|
70
70
|
attr_accessor :lazy_load_enabled
|
|
71
71
|
|
|
72
|
-
# Page cache for lazy loading (maps page_start_offset => page_data)
|
|
73
|
-
attr_accessor :page_cache
|
|
74
|
-
|
|
75
|
-
# Page size for lazy loading alignment (typical filesystem page size)
|
|
76
|
-
PAGE_SIZE = 4096
|
|
77
|
-
|
|
78
72
|
# Read TrueType Font from a file
|
|
79
73
|
#
|
|
80
74
|
# @param path [String] Path to the TTF file
|
|
@@ -101,8 +95,9 @@ module Fontisan
|
|
|
101
95
|
font.lazy_load_enabled = lazy
|
|
102
96
|
|
|
103
97
|
if lazy
|
|
104
|
-
#
|
|
105
|
-
|
|
98
|
+
# Reuse existing IO handle by duplicating it to prevent double file open
|
|
99
|
+
# The dup ensures the handle stays open after this block closes
|
|
100
|
+
font.io_source = io.dup
|
|
106
101
|
font.setup_finalizer
|
|
107
102
|
else
|
|
108
103
|
# Read tables upfront
|
|
@@ -141,7 +136,6 @@ module Fontisan
|
|
|
141
136
|
@loading_mode = LoadingModes::FULL
|
|
142
137
|
@lazy_load_enabled = false
|
|
143
138
|
@io_source = nil
|
|
144
|
-
@page_cache = {}
|
|
145
139
|
end
|
|
146
140
|
|
|
147
141
|
# Read table data for all tables
|
|
@@ -450,8 +444,8 @@ module Fontisan
|
|
|
450
444
|
|
|
451
445
|
# Load a single table's data on demand
|
|
452
446
|
#
|
|
453
|
-
# Uses
|
|
454
|
-
# performance is
|
|
447
|
+
# Uses direct seek-and-read for minimal overhead. This ensures lazy loading
|
|
448
|
+
# performance is comparable to eager loading when accessing all tables.
|
|
455
449
|
#
|
|
456
450
|
# @param tag [String] The table tag to load
|
|
457
451
|
# @return [void]
|
|
@@ -461,42 +455,10 @@ module Fontisan
|
|
|
461
455
|
entry = find_table_entry(tag)
|
|
462
456
|
return nil unless entry
|
|
463
457
|
|
|
464
|
-
#
|
|
465
|
-
|
|
466
|
-
table_end = entry.offset + entry.table_length
|
|
467
|
-
|
|
468
|
-
# Calculate page boundaries
|
|
469
|
-
page_start = (table_start / PAGE_SIZE) * PAGE_SIZE
|
|
470
|
-
page_end = ((table_end + PAGE_SIZE - 1) / PAGE_SIZE) * PAGE_SIZE
|
|
471
|
-
|
|
472
|
-
# Read all required pages (or use cached pages)
|
|
473
|
-
table_data_parts = []
|
|
474
|
-
current_page = page_start
|
|
475
|
-
|
|
476
|
-
while current_page < page_end
|
|
477
|
-
page_data = @page_cache[current_page]
|
|
478
|
-
|
|
479
|
-
unless page_data
|
|
480
|
-
# Read page from disk and cache it
|
|
481
|
-
@io_source.seek(current_page)
|
|
482
|
-
page_data = @io_source.read(PAGE_SIZE) || ""
|
|
483
|
-
@page_cache[current_page] = page_data
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Calculate which part of this page we need
|
|
487
|
-
chunk_start = [table_start - current_page, 0].max
|
|
488
|
-
chunk_end = [table_end - current_page, PAGE_SIZE].min
|
|
489
|
-
|
|
490
|
-
if chunk_end > chunk_start
|
|
491
|
-
table_data_parts << page_data[chunk_start...chunk_end]
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
current_page += PAGE_SIZE
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
# Combine parts and store
|
|
458
|
+
# Direct seek and read - same as eager loading but on-demand
|
|
459
|
+
@io_source.seek(entry.offset)
|
|
498
460
|
tag_key = tag.dup.force_encoding("UTF-8")
|
|
499
|
-
@table_data[tag_key] =
|
|
461
|
+
@table_data[tag_key] = @io_source.read(entry.table_length)
|
|
500
462
|
end
|
|
501
463
|
|
|
502
464
|
# Parse a table from raw data (Fontisan extension)
|