fontisan 0.2.8 → 0.2.10
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 +17 -101
- data/CHANGELOG.md +116 -0
- data/README.adoc +25 -13
- data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
- data/docs/COLLECTION_VALIDATION.adoc +143 -0
- data/docs/COLOR_FONTS.adoc +127 -0
- data/docs/DOCUMENTATION_SUMMARY.md +141 -0
- data/docs/FONT_HINTING.adoc +9 -1
- data/docs/VALIDATION.adoc +254 -0
- data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
- data/lib/fontisan/open_type_font.rb +18 -424
- data/lib/fontisan/sfnt_font.rb +690 -0
- data/lib/fontisan/sfnt_table.rb +264 -0
- data/lib/fontisan/tables/cmap_table.rb +231 -0
- data/lib/fontisan/tables/glyf_table.rb +255 -0
- data/lib/fontisan/tables/head_table.rb +111 -0
- data/lib/fontisan/tables/hhea_table.rb +255 -0
- data/lib/fontisan/tables/hmtx_table.rb +191 -0
- data/lib/fontisan/tables/loca_table.rb +212 -0
- data/lib/fontisan/tables/maxp_table.rb +258 -0
- data/lib/fontisan/tables/name_table.rb +176 -0
- data/lib/fontisan/tables/os2_table.rb +329 -0
- data/lib/fontisan/tables/post_table.rb +183 -0
- data/lib/fontisan/true_type_font.rb +12 -463
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff_font.rb +45 -29
- metadata +21 -2
|
@@ -1,42 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "constants"
|
|
5
|
-
require_relative "loading_modes"
|
|
6
|
-
require_relative "utilities/checksum_calculator"
|
|
3
|
+
require_relative "sfnt_font"
|
|
7
4
|
|
|
8
5
|
module Fontisan
|
|
9
|
-
#
|
|
10
|
-
class OffsetTable < BinData::Record
|
|
11
|
-
endian :big
|
|
12
|
-
uint32 :sfnt_version
|
|
13
|
-
uint16 :num_tables
|
|
14
|
-
uint16 :search_range
|
|
15
|
-
uint16 :entry_selector
|
|
16
|
-
uint16 :range_shift
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# TTF Table Directory Entry structure
|
|
20
|
-
class TableDirectory < BinData::Record
|
|
21
|
-
endian :big
|
|
22
|
-
string :tag, length: 4
|
|
23
|
-
uint32 :checksum
|
|
24
|
-
uint32 :offset
|
|
25
|
-
uint32 :table_length
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# TrueType Font domain object using BinData
|
|
29
|
-
#
|
|
30
|
-
# Represents a complete TrueType Font file using BinData's declarative
|
|
31
|
-
# DSL for binary structure definition. The structure definition IS the
|
|
32
|
-
# documentation, and BinData handles all low-level reading/writing.
|
|
6
|
+
# TrueType Font domain object
|
|
33
7
|
#
|
|
34
|
-
#
|
|
8
|
+
# Represents a TrueType Font file (glyf outlines). Inherits all shared
|
|
9
|
+
# SFNT functionality from SfntFont and adds TrueType-specific behavior.
|
|
35
10
|
#
|
|
36
11
|
# @example Reading and analyzing a font
|
|
37
12
|
# ttf = TrueTypeFont.from_file("font.ttf")
|
|
38
13
|
# puts ttf.header.num_tables # => 14
|
|
39
|
-
# name_table = ttf.table("name")
|
|
14
|
+
# name_table = ttf.table("name")
|
|
40
15
|
# puts name_table.english_name(Tables::Name::FAMILY)
|
|
41
16
|
#
|
|
42
17
|
# @example Loading with metadata mode
|
|
@@ -46,34 +21,15 @@ module Fontisan
|
|
|
46
21
|
#
|
|
47
22
|
# @example Writing a font
|
|
48
23
|
# ttf.to_file("output.ttf")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
array :tables, type: :table_directory, initial_length: lambda {
|
|
54
|
-
header.num_tables
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# Table data is stored separately since it's at variable offsets
|
|
58
|
-
attr_accessor :table_data
|
|
59
|
-
|
|
60
|
-
# Parsed table instances cache (Fontisan extension)
|
|
61
|
-
attr_accessor :parsed_tables
|
|
62
|
-
|
|
63
|
-
# Loading mode for this font (:metadata or :full)
|
|
64
|
-
attr_accessor :loading_mode
|
|
65
|
-
|
|
66
|
-
# IO source for lazy loading
|
|
67
|
-
attr_accessor :io_source
|
|
68
|
-
|
|
69
|
-
# Whether lazy loading is enabled
|
|
70
|
-
attr_accessor :lazy_load_enabled
|
|
71
|
-
|
|
24
|
+
#
|
|
25
|
+
# @example Reading from TTC collection
|
|
26
|
+
# ttf = TrueTypeFont.from_ttc(io, offset)
|
|
27
|
+
class TrueTypeFont < SfntFont
|
|
72
28
|
# Read TrueType Font from a file
|
|
73
29
|
#
|
|
74
30
|
# @param path [String] Path to the TTF file
|
|
75
31
|
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
76
|
-
# @param lazy [Boolean] If true, load tables on demand (default: false
|
|
32
|
+
# @param lazy [Boolean] If true, load tables on demand (default: false)
|
|
77
33
|
# @return [TrueTypeFont] A new instance
|
|
78
34
|
# @raise [ArgumentError] if path is nil or empty, or if mode is invalid
|
|
79
35
|
# @raise [Errno::ENOENT] if file does not exist
|
|
@@ -127,153 +83,6 @@ module Fontisan
|
|
|
127
83
|
font
|
|
128
84
|
end
|
|
129
85
|
|
|
130
|
-
# Initialize storage hashes (Fontisan extension)
|
|
131
|
-
#
|
|
132
|
-
# @return [void]
|
|
133
|
-
def initialize_storage
|
|
134
|
-
@table_data = {}
|
|
135
|
-
@parsed_tables = {}
|
|
136
|
-
@loading_mode = LoadingModes::FULL
|
|
137
|
-
@lazy_load_enabled = false
|
|
138
|
-
@io_source = nil
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Read table data for all tables
|
|
142
|
-
#
|
|
143
|
-
# In metadata mode, only reads metadata tables. In full mode, reads all tables.
|
|
144
|
-
# In lazy load mode, doesn't read data upfront.
|
|
145
|
-
#
|
|
146
|
-
# @param io [IO] Open file handle
|
|
147
|
-
# @return [void]
|
|
148
|
-
def read_table_data(io)
|
|
149
|
-
@table_data = {}
|
|
150
|
-
|
|
151
|
-
if @lazy_load_enabled
|
|
152
|
-
# Don't read data, just keep IO reference
|
|
153
|
-
@io_source = io
|
|
154
|
-
return
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
if @loading_mode == LoadingModes::METADATA
|
|
158
|
-
# Only read metadata tables for performance
|
|
159
|
-
# Use page-aware batched reading to maximize filesystem prefetching
|
|
160
|
-
read_metadata_tables_batched(io)
|
|
161
|
-
else
|
|
162
|
-
# Read all tables
|
|
163
|
-
tables.each do |entry|
|
|
164
|
-
io.seek(entry.offset)
|
|
165
|
-
# Force UTF-8 encoding on tag for hash key consistency
|
|
166
|
-
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
167
|
-
@table_data[tag_key] = io.read(entry.table_length)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Read metadata tables using page-aware batching
|
|
173
|
-
#
|
|
174
|
-
# Groups adjacent tables within page boundaries and reads them together
|
|
175
|
-
# to maximize filesystem prefetching and minimize random seeks.
|
|
176
|
-
#
|
|
177
|
-
# @param io [IO] Open file handle
|
|
178
|
-
# @return [void]
|
|
179
|
-
def read_metadata_tables_batched(io)
|
|
180
|
-
# Typical filesystem page size (4KB is common, but 8KB gives better prefetch window)
|
|
181
|
-
page_threshold = 8192
|
|
182
|
-
|
|
183
|
-
# Get metadata tables sorted by offset for sequential access
|
|
184
|
-
metadata_entries = tables.select { |entry| LoadingModes::METADATA_TABLES_SET.include?(entry.tag) }
|
|
185
|
-
metadata_entries.sort_by!(&:offset)
|
|
186
|
-
|
|
187
|
-
return if metadata_entries.empty?
|
|
188
|
-
|
|
189
|
-
# Group adjacent tables within page threshold for batched reading
|
|
190
|
-
i = 0
|
|
191
|
-
while i < metadata_entries.size
|
|
192
|
-
batch_start = metadata_entries[i]
|
|
193
|
-
batch_end = batch_start
|
|
194
|
-
batch_entries = [batch_start]
|
|
195
|
-
|
|
196
|
-
# Extend batch while next table is within page threshold
|
|
197
|
-
j = i + 1
|
|
198
|
-
while j < metadata_entries.size
|
|
199
|
-
next_entry = metadata_entries[j]
|
|
200
|
-
gap = next_entry.offset - (batch_end.offset + batch_end.table_length)
|
|
201
|
-
|
|
202
|
-
# If gap is small (within page threshold), include in batch
|
|
203
|
-
if gap <= page_threshold
|
|
204
|
-
batch_end = next_entry
|
|
205
|
-
batch_entries << next_entry
|
|
206
|
-
j += 1
|
|
207
|
-
else
|
|
208
|
-
break
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Read batch
|
|
213
|
-
if batch_entries.size == 1
|
|
214
|
-
# Single table, read normally
|
|
215
|
-
io.seek(batch_start.offset)
|
|
216
|
-
tag_key = batch_start.tag.dup.force_encoding("UTF-8")
|
|
217
|
-
@table_data[tag_key] = io.read(batch_start.table_length)
|
|
218
|
-
else
|
|
219
|
-
# Multiple tables, read contiguous segment
|
|
220
|
-
batch_offset = batch_start.offset
|
|
221
|
-
batch_length = (batch_end.offset + batch_end.table_length) - batch_start.offset
|
|
222
|
-
|
|
223
|
-
io.seek(batch_offset)
|
|
224
|
-
batch_data = io.read(batch_length)
|
|
225
|
-
|
|
226
|
-
# Extract individual tables from batch
|
|
227
|
-
batch_entries.each do |entry|
|
|
228
|
-
relative_offset = entry.offset - batch_offset
|
|
229
|
-
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
230
|
-
@table_data[tag_key] =
|
|
231
|
-
batch_data[relative_offset, entry.table_length]
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
i = j
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Write TrueType Font to a file
|
|
240
|
-
#
|
|
241
|
-
# Writes the complete TTF structure to disk, including proper checksum
|
|
242
|
-
# calculation and table alignment.
|
|
243
|
-
#
|
|
244
|
-
# @param path [String] Path where the TTF file will be written
|
|
245
|
-
# @return [Integer] Number of bytes written
|
|
246
|
-
# @raise [IOError] if writing fails
|
|
247
|
-
def to_file(path)
|
|
248
|
-
File.open(path, "wb") do |io|
|
|
249
|
-
# Write header and tables (directory)
|
|
250
|
-
write_structure(io)
|
|
251
|
-
|
|
252
|
-
# Write table data with updated offsets
|
|
253
|
-
write_table_data_with_offsets(io)
|
|
254
|
-
|
|
255
|
-
io.pos
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Update checksum adjustment in head table
|
|
259
|
-
update_checksum_adjustment_in_file(path) if head_table
|
|
260
|
-
|
|
261
|
-
File.size(path)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Validate format correctness
|
|
265
|
-
#
|
|
266
|
-
# @return [Boolean] true if the TTF format is valid, false otherwise
|
|
267
|
-
def valid?
|
|
268
|
-
return false unless header
|
|
269
|
-
return false unless tables.respond_to?(:length)
|
|
270
|
-
return false unless @table_data.is_a?(Hash)
|
|
271
|
-
return false if tables.length != header.num_tables
|
|
272
|
-
return false unless head_table
|
|
273
|
-
|
|
274
|
-
true
|
|
275
|
-
end
|
|
276
|
-
|
|
277
86
|
# Check if font is TrueType flavored
|
|
278
87
|
#
|
|
279
88
|
# @return [Boolean] true for TrueType fonts
|
|
@@ -288,194 +97,11 @@ module Fontisan
|
|
|
288
97
|
false
|
|
289
98
|
end
|
|
290
99
|
|
|
291
|
-
# Check if font has a specific table
|
|
292
|
-
#
|
|
293
|
-
# @param tag [String] The table tag to check for
|
|
294
|
-
# @return [Boolean] true if table exists, false otherwise
|
|
295
|
-
def has_table?(tag)
|
|
296
|
-
tables.any? { |entry| entry.tag == tag }
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# Check if a table is available in the current loading mode
|
|
300
|
-
#
|
|
301
|
-
# @param tag [String] The table tag to check
|
|
302
|
-
# @return [Boolean] true if table is available in current mode
|
|
303
|
-
def table_available?(tag)
|
|
304
|
-
return false unless has_table?(tag)
|
|
305
|
-
|
|
306
|
-
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Find a table entry by tag
|
|
310
|
-
#
|
|
311
|
-
# @param tag [String] The table tag to find
|
|
312
|
-
# @return [TableDirectory, nil] The table entry or nil
|
|
313
|
-
def find_table_entry(tag)
|
|
314
|
-
tables.find { |entry| entry.tag == tag }
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Get the head table entry
|
|
318
|
-
#
|
|
319
|
-
# @return [TableDirectory, nil] The head table entry or nil
|
|
320
|
-
def head_table
|
|
321
|
-
find_table_entry(Constants::HEAD_TAG)
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# Get list of all table tags (Fontisan extension)
|
|
325
|
-
#
|
|
326
|
-
# @return [Array<String>] Array of table tag strings
|
|
327
|
-
def table_names
|
|
328
|
-
tables.map(&:tag)
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Get parsed table instance (Fontisan extension)
|
|
332
|
-
#
|
|
333
|
-
# This method parses the raw table data into a structured table object
|
|
334
|
-
# and caches the result for subsequent calls. Enforces mode restrictions.
|
|
335
|
-
#
|
|
336
|
-
# @param tag [String] The table tag to retrieve
|
|
337
|
-
# @return [Tables::*, nil] Parsed table object or nil if not found
|
|
338
|
-
# @raise [ArgumentError] if table is not available in current loading mode
|
|
339
|
-
def table(tag)
|
|
340
|
-
# Check mode restrictions
|
|
341
|
-
unless table_available?(tag)
|
|
342
|
-
if has_table?(tag)
|
|
343
|
-
raise ArgumentError,
|
|
344
|
-
"Table '#{tag}' is not available in #{@loading_mode} mode. " \
|
|
345
|
-
"Available tables: #{LoadingModes.tables_for(@loading_mode).inspect}"
|
|
346
|
-
else
|
|
347
|
-
return nil
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
return @parsed_tables[tag] if @parsed_tables.key?(tag)
|
|
352
|
-
|
|
353
|
-
# Lazy load table data if enabled
|
|
354
|
-
if @lazy_load_enabled && !@table_data.key?(tag)
|
|
355
|
-
load_table_data(tag)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
@parsed_tables[tag] ||= parse_table(tag)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Get units per em from head table (Fontisan extension)
|
|
362
|
-
#
|
|
363
|
-
# @return [Integer, nil] Units per em value
|
|
364
|
-
def units_per_em
|
|
365
|
-
head = table(Constants::HEAD_TAG)
|
|
366
|
-
head&.units_per_em
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Convenience methods for accessing common name table fields
|
|
370
|
-
# These are particularly useful in minimal mode
|
|
371
|
-
|
|
372
|
-
# Get font family name
|
|
373
|
-
#
|
|
374
|
-
# @return [String, nil] Family name or nil if not found
|
|
375
|
-
def family_name
|
|
376
|
-
name_table = table(Constants::NAME_TAG)
|
|
377
|
-
name_table&.english_name(Tables::Name::FAMILY)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
# Get font subfamily name (e.g., Regular, Bold, Italic)
|
|
381
|
-
#
|
|
382
|
-
# @return [String, nil] Subfamily name or nil if not found
|
|
383
|
-
def subfamily_name
|
|
384
|
-
name_table = table(Constants::NAME_TAG)
|
|
385
|
-
name_table&.english_name(Tables::Name::SUBFAMILY)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Get full font name
|
|
389
|
-
#
|
|
390
|
-
# @return [String, nil] Full name or nil if not found
|
|
391
|
-
def full_name
|
|
392
|
-
name_table = table(Constants::NAME_TAG)
|
|
393
|
-
name_table&.english_name(Tables::Name::FULL_NAME)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Get PostScript name
|
|
397
|
-
#
|
|
398
|
-
# @return [String, nil] PostScript name or nil if not found
|
|
399
|
-
def post_script_name
|
|
400
|
-
name_table = table(Constants::NAME_TAG)
|
|
401
|
-
name_table&.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
# Get preferred family name
|
|
405
|
-
#
|
|
406
|
-
# @return [String, nil] Preferred family name or nil if not found
|
|
407
|
-
def preferred_family_name
|
|
408
|
-
name_table = table(Constants::NAME_TAG)
|
|
409
|
-
name_table&.english_name(Tables::Name::PREFERRED_FAMILY)
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
# Get preferred subfamily name
|
|
413
|
-
#
|
|
414
|
-
# @return [String, nil] Preferred subfamily name or nil if not found
|
|
415
|
-
def preferred_subfamily_name
|
|
416
|
-
name_table = table(Constants::NAME_TAG)
|
|
417
|
-
name_table&.english_name(Tables::Name::PREFERRED_SUBFAMILY)
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
# Close the IO source (for lazy loading)
|
|
421
|
-
#
|
|
422
|
-
# @return [void]
|
|
423
|
-
def close
|
|
424
|
-
@io_source&.close
|
|
425
|
-
@io_source = nil
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
# Setup finalizer for cleanup
|
|
429
|
-
#
|
|
430
|
-
# @return [void]
|
|
431
|
-
def setup_finalizer
|
|
432
|
-
ObjectSpace.define_finalizer(self, self.class.finalize(@io_source))
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
# Finalizer proc for closing IO
|
|
436
|
-
#
|
|
437
|
-
# @param io [IO] The IO object to close
|
|
438
|
-
# @return [Proc] The finalizer proc
|
|
439
|
-
def self.finalize(io)
|
|
440
|
-
proc { io&.close }
|
|
441
|
-
end
|
|
442
|
-
|
|
443
100
|
private
|
|
444
101
|
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
# Uses direct seek-and-read for minimal overhead. This ensures lazy loading
|
|
448
|
-
# performance is comparable to eager loading when accessing all tables.
|
|
102
|
+
# Map table tag to parser class
|
|
449
103
|
#
|
|
450
|
-
#
|
|
451
|
-
# @return [void]
|
|
452
|
-
def load_table_data(tag)
|
|
453
|
-
return unless @io_source
|
|
454
|
-
|
|
455
|
-
entry = find_table_entry(tag)
|
|
456
|
-
return nil unless entry
|
|
457
|
-
|
|
458
|
-
# Direct seek and read - same as eager loading but on-demand
|
|
459
|
-
@io_source.seek(entry.offset)
|
|
460
|
-
tag_key = tag.dup.force_encoding("UTF-8")
|
|
461
|
-
@table_data[tag_key] = @io_source.read(entry.table_length)
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
# Parse a table from raw data (Fontisan extension)
|
|
465
|
-
#
|
|
466
|
-
# @param tag [String] The table tag to parse
|
|
467
|
-
# @return [Tables::*, nil] Parsed table object or nil
|
|
468
|
-
def parse_table(tag)
|
|
469
|
-
raw_data = @table_data[tag]
|
|
470
|
-
return nil unless raw_data
|
|
471
|
-
|
|
472
|
-
table_class = table_class_for(tag)
|
|
473
|
-
return nil unless table_class
|
|
474
|
-
|
|
475
|
-
table_class.read(raw_data)
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
# Map table tag to parser class (Fontisan extension)
|
|
104
|
+
# TrueType-specific mapping includes glyf/loca tables.
|
|
479
105
|
#
|
|
480
106
|
# @param tag [String] The table tag
|
|
481
107
|
# @return [Class, nil] Table parser class or nil
|
|
@@ -502,82 +128,5 @@ module Fontisan
|
|
|
502
128
|
"sbix" => Tables::Sbix,
|
|
503
129
|
}[tag]
|
|
504
130
|
end
|
|
505
|
-
|
|
506
|
-
# Write the structure (header + table directory) to IO
|
|
507
|
-
#
|
|
508
|
-
# @param io [IO] Open file handle
|
|
509
|
-
# @return [void]
|
|
510
|
-
def write_structure(io)
|
|
511
|
-
# Write header
|
|
512
|
-
header.write(io)
|
|
513
|
-
|
|
514
|
-
# Write table directory with placeholder offsets
|
|
515
|
-
tables.each do |entry|
|
|
516
|
-
io.write(entry.tag)
|
|
517
|
-
io.write([entry.checksum].pack("N"))
|
|
518
|
-
io.write([0].pack("N")) # Placeholder offset
|
|
519
|
-
io.write([entry.table_length].pack("N"))
|
|
520
|
-
end
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
# Write table data and update offsets in directory
|
|
524
|
-
#
|
|
525
|
-
# @param io [IO] Open file handle
|
|
526
|
-
# @return [void]
|
|
527
|
-
def write_table_data_with_offsets(io)
|
|
528
|
-
tables.each_with_index do |entry, index|
|
|
529
|
-
# Record current position
|
|
530
|
-
current_position = io.pos
|
|
531
|
-
|
|
532
|
-
# Write table data
|
|
533
|
-
data = @table_data[entry.tag]
|
|
534
|
-
raise IOError, "Missing table data for tag '#{entry.tag}'" if data.nil?
|
|
535
|
-
|
|
536
|
-
io.write(data)
|
|
537
|
-
|
|
538
|
-
# Add padding to align to 4-byte boundary
|
|
539
|
-
padding = (Constants::TABLE_ALIGNMENT - (io.pos % Constants::TABLE_ALIGNMENT)) % Constants::TABLE_ALIGNMENT
|
|
540
|
-
io.write("\x00" * padding) if padding.positive?
|
|
541
|
-
|
|
542
|
-
# Zero out checksumAdjustment field in head table
|
|
543
|
-
if entry.tag == Constants::HEAD_TAG
|
|
544
|
-
current_pos = io.pos
|
|
545
|
-
io.seek(current_position + 8)
|
|
546
|
-
io.write([0].pack("N"))
|
|
547
|
-
io.seek(current_pos)
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# Update offset in table directory
|
|
551
|
-
# Table directory starts at byte 12, each entry is 16 bytes
|
|
552
|
-
# Offset field is at byte 8 within each entry
|
|
553
|
-
directory_offset_position = 12 + (index * 16) + 8
|
|
554
|
-
current_pos = io.pos
|
|
555
|
-
io.seek(directory_offset_position)
|
|
556
|
-
io.write([current_position].pack("N")) # Offset is now known
|
|
557
|
-
io.seek(current_pos)
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# Update checksumAdjustment field in head table
|
|
562
|
-
#
|
|
563
|
-
# @param path [String] Path to the TTF file
|
|
564
|
-
# @return [void]
|
|
565
|
-
def update_checksum_adjustment_in_file(path)
|
|
566
|
-
File.open(path, "r+b") do |io|
|
|
567
|
-
# Calculate checksum directly from IO to avoid Windows Tempfile issues
|
|
568
|
-
checksum = Utilities::ChecksumCalculator.calculate_checksum_from_io(io)
|
|
569
|
-
|
|
570
|
-
# Calculate adjustment
|
|
571
|
-
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
572
|
-
|
|
573
|
-
# Find head table position
|
|
574
|
-
head_entry = head_table
|
|
575
|
-
return unless head_entry
|
|
576
|
-
|
|
577
|
-
# Write adjustment to head table (offset 8 within head table)
|
|
578
|
-
io.seek(head_entry.offset + 8)
|
|
579
|
-
io.write([adjustment].pack("N"))
|
|
580
|
-
end
|
|
581
|
-
end
|
|
582
131
|
end
|
|
583
132
|
end
|
data/lib/fontisan/version.rb
CHANGED
data/lib/fontisan/woff_font.rb
CHANGED
|
@@ -438,12 +438,13 @@ module Fontisan
|
|
|
438
438
|
io.write("\x00" * padding) if padding.positive?
|
|
439
439
|
end
|
|
440
440
|
|
|
441
|
+
# Update checksum adjustment in head table BEFORE closing file
|
|
442
|
+
# This avoids Windows file locking issues when Tempfiles are used
|
|
443
|
+
update_checksum_adjustment_in_io(io)
|
|
444
|
+
|
|
441
445
|
io.pos
|
|
442
446
|
end
|
|
443
447
|
|
|
444
|
-
# Update checksum adjustment in head table
|
|
445
|
-
update_checksum_adjustment_in_file(output_path)
|
|
446
|
-
|
|
447
448
|
File.size(output_path)
|
|
448
449
|
end
|
|
449
450
|
|
|
@@ -458,43 +459,58 @@ module Fontisan
|
|
|
458
459
|
[search_range, entry_selector, range_shift]
|
|
459
460
|
end
|
|
460
461
|
|
|
461
|
-
# Update checksumAdjustment field in head table
|
|
462
|
+
# Update checksumAdjustment field in head table using an open IO object
|
|
462
463
|
#
|
|
463
|
-
# @param
|
|
464
|
+
# @param io [IO] Open IO object positioned at start of file
|
|
464
465
|
# @return [void]
|
|
465
|
-
def
|
|
466
|
-
#
|
|
466
|
+
def update_checksum_adjustment_in_io(io)
|
|
467
|
+
# Save current position
|
|
468
|
+
current_pos = io.pos
|
|
469
|
+
|
|
470
|
+
# Find head table position in the file
|
|
467
471
|
head_offset = nil
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
break
|
|
482
|
-
end
|
|
472
|
+
io.seek(4) # Skip sfnt_version
|
|
473
|
+
num_tables = io.read(2).unpack1("n")
|
|
474
|
+
io.seek(12) # Start of table directory
|
|
475
|
+
|
|
476
|
+
num_tables.times do
|
|
477
|
+
tag = io.read(4)
|
|
478
|
+
io.read(4) # checksum
|
|
479
|
+
offset = io.read(4).unpack1("N")
|
|
480
|
+
io.read(4) # length
|
|
481
|
+
|
|
482
|
+
if tag == Constants::HEAD_TAG
|
|
483
|
+
head_offset = offset
|
|
484
|
+
break
|
|
483
485
|
end
|
|
484
486
|
end
|
|
485
487
|
|
|
486
488
|
return unless head_offset
|
|
487
489
|
|
|
490
|
+
# Rewind to calculate checksum from the beginning
|
|
491
|
+
io.rewind
|
|
492
|
+
|
|
488
493
|
# Calculate checksum directly from IO to avoid Windows Tempfile issues
|
|
489
|
-
|
|
490
|
-
checksum = Utilities::ChecksumCalculator.calculate_checksum_from_io(io)
|
|
494
|
+
checksum = Utilities::ChecksumCalculator.calculate_checksum_from_io(io)
|
|
491
495
|
|
|
492
|
-
|
|
493
|
-
|
|
496
|
+
# Calculate adjustment
|
|
497
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
494
498
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
499
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
500
|
+
io.seek(head_offset + 8)
|
|
501
|
+
io.write([adjustment].pack("N"))
|
|
502
|
+
|
|
503
|
+
# Restore original position
|
|
504
|
+
io.seek(current_pos)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Update checksumAdjustment field in head table
|
|
508
|
+
#
|
|
509
|
+
# @param path [String] Path to the font file
|
|
510
|
+
# @return [void]
|
|
511
|
+
def update_checksum_adjustment_in_file(path)
|
|
512
|
+
File.open(path, "r+b") do |io|
|
|
513
|
+
update_checksum_adjustment_in_io(io)
|
|
498
514
|
end
|
|
499
515
|
end
|
|
500
516
|
end
|