fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require "brotli"
|
|
5
|
+
require_relative "constants"
|
|
6
|
+
require_relative "utilities/checksum_calculator"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
# WOFF2 Header structure
|
|
10
|
+
#
|
|
11
|
+
# WOFF2 header is more compact than WOFF, using variable-length integers
|
|
12
|
+
# for some fields and omitting redundant information.
|
|
13
|
+
class Woff2Header < BinData::Record
|
|
14
|
+
endian :big
|
|
15
|
+
uint32 :signature # 0x774F4632 'wOF2'
|
|
16
|
+
uint32 :flavor # sfnt version (0x00010000 for TTF, 'OTTO' for CFF)
|
|
17
|
+
uint32 :woff2_length # Total size of WOFF2 file
|
|
18
|
+
uint16 :num_tables # Number of entries in directory
|
|
19
|
+
uint16 :reserved # Reserved, must be zero
|
|
20
|
+
uint32 :total_sfnt_size # Total size needed for uncompressed font
|
|
21
|
+
uint32 :total_compressed_size # Total size of compressed data block
|
|
22
|
+
uint16 :major_version # Major version of WOFF file
|
|
23
|
+
uint16 :minor_version # Minor version of WOFF file
|
|
24
|
+
uint32 :meta_offset # Offset to metadata block
|
|
25
|
+
uint32 :meta_length # Length of compressed metadata block
|
|
26
|
+
uint32 :meta_orig_length # Length of uncompressed metadata block
|
|
27
|
+
uint32 :priv_offset # Offset to private data block
|
|
28
|
+
uint32 :priv_length # Length of private data block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# WOFF2 Table Directory Entry structure
|
|
32
|
+
#
|
|
33
|
+
# WOFF2 table directory entries are more complex than WOFF,
|
|
34
|
+
# with transformation flags and variable-length sizes.
|
|
35
|
+
class Woff2TableDirectoryEntry
|
|
36
|
+
attr_accessor :tag, :flags, :transform_version, :orig_length,
|
|
37
|
+
:transform_length, :offset
|
|
38
|
+
|
|
39
|
+
# Transformation version flags
|
|
40
|
+
TRANSFORM_NONE = 0
|
|
41
|
+
TRANSFORM_GLYF_LOCA = 0
|
|
42
|
+
TRANSFORM_LOCA = 1
|
|
43
|
+
TRANSFORM_HMTX = 2
|
|
44
|
+
|
|
45
|
+
# Known table tags with assigned indices (0-62)
|
|
46
|
+
KNOWN_TAGS = [
|
|
47
|
+
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
|
|
48
|
+
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
|
49
|
+
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
|
|
50
|
+
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
|
51
|
+
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
|
|
52
|
+
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
|
53
|
+
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
|
|
54
|
+
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def initialize
|
|
58
|
+
@flags = 0
|
|
59
|
+
@transform_version = TRANSFORM_NONE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if table is transformed
|
|
63
|
+
def transformed?
|
|
64
|
+
(@flags & 0x3F) != 0x3F && KNOWN_TAGS[tag_index]&.start_with?(/glyf|loca|hmtx/)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get transform version for this table
|
|
68
|
+
def transform_version
|
|
69
|
+
return TRANSFORM_NONE unless transformed?
|
|
70
|
+
|
|
71
|
+
case tag
|
|
72
|
+
when "glyf", "loca"
|
|
73
|
+
TRANSFORM_GLYF_LOCA
|
|
74
|
+
when "hmtx"
|
|
75
|
+
TRANSFORM_HMTX
|
|
76
|
+
else
|
|
77
|
+
TRANSFORM_NONE
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def tag_index
|
|
84
|
+
@flags & 0x3F
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Web Open Font Format 2.0 (WOFF2) font domain object
|
|
89
|
+
#
|
|
90
|
+
# Represents a WOFF2 font file that uses Brotli compression and table
|
|
91
|
+
# transformations. WOFF2 is significantly more complex than WOFF.
|
|
92
|
+
#
|
|
93
|
+
# According to the WOFF2 specification (https://www.w3.org/TR/WOFF2/):
|
|
94
|
+
# - Tables can be transformed (glyf, loca, hmtx have special formats)
|
|
95
|
+
# - All compressed data in a single Brotli stream
|
|
96
|
+
# - Variable-length integer encoding (UIntBase128, 255UInt16)
|
|
97
|
+
# - More efficient compression than WOFF
|
|
98
|
+
#
|
|
99
|
+
# @example Reading a WOFF2 font
|
|
100
|
+
# woff2 = Woff2Font.from_file("font.woff2")
|
|
101
|
+
# puts woff2.header.num_tables
|
|
102
|
+
# name_table = woff2.table("name")
|
|
103
|
+
# puts name_table.english_name(Tables::Name::FAMILY)
|
|
104
|
+
#
|
|
105
|
+
# @example Converting to TTF/OTF
|
|
106
|
+
# woff2 = Woff2Font.from_file("font.woff2")
|
|
107
|
+
# woff2.to_ttf("output.ttf") # if TrueType flavored
|
|
108
|
+
# woff2.to_otf("output.otf") # if CFF flavored
|
|
109
|
+
class Woff2Font
|
|
110
|
+
attr_accessor :header, :table_entries, :decompressed_tables,
|
|
111
|
+
:parsed_tables, :io_source
|
|
112
|
+
|
|
113
|
+
# WOFF2 signature constant
|
|
114
|
+
WOFF2_SIGNATURE = 0x774F4632 # 'wOF2'
|
|
115
|
+
|
|
116
|
+
# Read WOFF2 font from a file
|
|
117
|
+
#
|
|
118
|
+
# @param path [String] Path to the WOFF2 file
|
|
119
|
+
# @return [Woff2Font] A new instance
|
|
120
|
+
# @raise [ArgumentError] if path is nil or empty
|
|
121
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
122
|
+
# @raise [InvalidFontError] if file format is invalid
|
|
123
|
+
def self.from_file(path)
|
|
124
|
+
if path.nil? || path.to_s.empty?
|
|
125
|
+
raise ArgumentError, "path cannot be nil or empty"
|
|
126
|
+
end
|
|
127
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
128
|
+
|
|
129
|
+
File.open(path, "rb") do |io|
|
|
130
|
+
font = new
|
|
131
|
+
font.read_from_io(io)
|
|
132
|
+
font.validate_signature!
|
|
133
|
+
font.initialize_storage
|
|
134
|
+
font.decompress_and_parse_tables(io)
|
|
135
|
+
font.io_source = io
|
|
136
|
+
font
|
|
137
|
+
end
|
|
138
|
+
rescue BinData::ValidityError, EOFError => e
|
|
139
|
+
raise InvalidFontError, "Invalid WOFF2 file: #{e.message}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def initialize
|
|
143
|
+
@header = nil
|
|
144
|
+
@table_entries = []
|
|
145
|
+
@decompressed_tables = {}
|
|
146
|
+
@parsed_tables = {}
|
|
147
|
+
@io_source = nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Read header and table directory from IO
|
|
151
|
+
#
|
|
152
|
+
# @param io [IO] Open file handle
|
|
153
|
+
# @return [void]
|
|
154
|
+
def read_from_io(io)
|
|
155
|
+
@header = Woff2Header.read(io)
|
|
156
|
+
read_table_directory(io)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Initialize storage hashes
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
def initialize_storage
|
|
163
|
+
@decompressed_tables ||= {}
|
|
164
|
+
@initialize_storage ||= {}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate WOFF2 signature
|
|
168
|
+
#
|
|
169
|
+
# @raise [InvalidFontError] if signature is invalid
|
|
170
|
+
# @return [void]
|
|
171
|
+
def validate_signature!
|
|
172
|
+
signature_value = header.signature.to_i
|
|
173
|
+
unless signature_value == WOFF2_SIGNATURE
|
|
174
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
175
|
+
"Invalid WOFF2 signature: expected 0x#{WOFF2_SIGNATURE.to_s(16)}, " \
|
|
176
|
+
"got 0x#{signature_value.to_s(16)}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if font is TrueType flavored
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean] true if TrueType, false if CFF
|
|
183
|
+
def truetype?
|
|
184
|
+
[Constants::SFNT_VERSION_TRUETYPE, 0x00010000].include?(header.flavor)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check if font is CFF flavored (OpenType with CFF outlines)
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean] true if CFF, false if TrueType
|
|
190
|
+
def cff?
|
|
191
|
+
[Constants::SFNT_VERSION_OTTO, 0x4F54544F].include?(header.flavor) # 'OTTO'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Get decompressed table data
|
|
195
|
+
#
|
|
196
|
+
# Provides unified interface compatible with WoffFont
|
|
197
|
+
#
|
|
198
|
+
# @param tag [String] The table tag
|
|
199
|
+
# @return [String, nil] Decompressed table data or nil if not found
|
|
200
|
+
def table_data(tag)
|
|
201
|
+
@decompressed_tables[tag]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Check if font has a specific table
|
|
205
|
+
#
|
|
206
|
+
# @param tag [String] The table tag to check for
|
|
207
|
+
# @return [Boolean] true if table exists, false otherwise
|
|
208
|
+
def has_table?(tag)
|
|
209
|
+
table_entries.any? { |entry| entry.tag == tag }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Find a table entry by tag
|
|
213
|
+
#
|
|
214
|
+
# @param tag [String] The table tag to find
|
|
215
|
+
# @return [Woff2TableDirectoryEntry, nil] The table entry or nil
|
|
216
|
+
def find_table_entry(tag)
|
|
217
|
+
table_entries.find { |entry| entry.tag == tag }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get list of all table tags
|
|
221
|
+
#
|
|
222
|
+
# @return [Array<String>] Array of table tag strings
|
|
223
|
+
def table_names
|
|
224
|
+
table_entries.map(&:tag)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Get parsed table instance
|
|
228
|
+
#
|
|
229
|
+
# This method decompresses and parses the raw table data into a
|
|
230
|
+
# structured table object and caches the result for subsequent calls.
|
|
231
|
+
#
|
|
232
|
+
# @param tag [String] The table tag to retrieve
|
|
233
|
+
# @return [Tables::*, nil] Parsed table object or nil if not found
|
|
234
|
+
def table(tag)
|
|
235
|
+
@parsed_tables[tag] ||= parse_table(tag)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get units per em from head table
|
|
239
|
+
#
|
|
240
|
+
# @return [Integer, nil] Units per em value
|
|
241
|
+
def units_per_em
|
|
242
|
+
head = table(Constants::HEAD_TAG)
|
|
243
|
+
head&.units_per_em
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Get WOFF2 metadata if present
|
|
247
|
+
#
|
|
248
|
+
# @return [String, nil] Decompressed metadata XML or nil
|
|
249
|
+
def metadata
|
|
250
|
+
return nil if header.meta_length.zero?
|
|
251
|
+
return @metadata if defined?(@metadata)
|
|
252
|
+
|
|
253
|
+
File.open(io_source.path, "rb") do |io|
|
|
254
|
+
io.seek(header.meta_offset)
|
|
255
|
+
compressed_meta = io.read(header.meta_length)
|
|
256
|
+
@metadata = Brotli.inflate(compressed_meta)
|
|
257
|
+
|
|
258
|
+
# Verify decompressed size
|
|
259
|
+
if @metadata.bytesize != header.meta_orig_length
|
|
260
|
+
raise InvalidFontError,
|
|
261
|
+
"Metadata size mismatch: expected #{header.meta_orig_length}, got #{@metadata.bytesize}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@metadata
|
|
265
|
+
end
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
warn "Failed to decompress WOFF2 metadata: #{e.message}"
|
|
268
|
+
@metadata = nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Convert WOFF2 to TTF format
|
|
272
|
+
#
|
|
273
|
+
# Decompresses and reconstructs tables, then builds a standard TTF file
|
|
274
|
+
#
|
|
275
|
+
# @param output_path [String] Path where TTF file will be written
|
|
276
|
+
# @return [Integer] Number of bytes written
|
|
277
|
+
# @raise [InvalidFontError] if font is not TrueType flavored
|
|
278
|
+
def to_ttf(output_path)
|
|
279
|
+
unless truetype?
|
|
280
|
+
raise InvalidFontError,
|
|
281
|
+
"Cannot convert to TTF: font is CFF flavored (use to_otf)"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
build_sfnt_font(output_path, Constants::SFNT_VERSION_TRUETYPE)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Convert WOFF2 to OTF format
|
|
288
|
+
#
|
|
289
|
+
# Decompresses and reconstructs tables, then builds a standard OTF file
|
|
290
|
+
#
|
|
291
|
+
# @param output_path [String] Path where OTF file will be written
|
|
292
|
+
# @return [Integer] Number of bytes written
|
|
293
|
+
# @raise [InvalidFontError] if font is not CFF flavored
|
|
294
|
+
def to_otf(output_path)
|
|
295
|
+
unless cff?
|
|
296
|
+
raise InvalidFontError,
|
|
297
|
+
"Cannot convert to OTF: font is TrueType flavored (use to_ttf)"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
build_sfnt_font(output_path, Constants::SFNT_VERSION_OTTO)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Validate format correctness
|
|
304
|
+
#
|
|
305
|
+
# @return [Boolean] true if the WOFF2 format is valid, false otherwise
|
|
306
|
+
def valid?
|
|
307
|
+
return false unless header
|
|
308
|
+
return false unless header.signature == WOFF2_SIGNATURE
|
|
309
|
+
return false unless table_entries.respond_to?(:length)
|
|
310
|
+
return false if table_entries.length != header.num_tables
|
|
311
|
+
return false unless has_table?(Constants::HEAD_TAG)
|
|
312
|
+
|
|
313
|
+
true
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
private
|
|
317
|
+
|
|
318
|
+
# Read variable-length UIntBase128 integer
|
|
319
|
+
#
|
|
320
|
+
# WOFF2 uses a variable-length encoding for table sizes:
|
|
321
|
+
# - If high bit is 0, it's a single byte value
|
|
322
|
+
# - If high bit is 1, continue reading bytes
|
|
323
|
+
# - Maximum 5 bytes for a 32-bit value
|
|
324
|
+
#
|
|
325
|
+
# @param io [IO] Open file handle
|
|
326
|
+
# @return [Integer] The decoded integer value
|
|
327
|
+
def read_uint_base128(io)
|
|
328
|
+
result = 0
|
|
329
|
+
5.times do
|
|
330
|
+
byte = io.read(1).unpack1("C")
|
|
331
|
+
return nil unless byte
|
|
332
|
+
|
|
333
|
+
# Continue if high bit is set
|
|
334
|
+
if (byte & 0x80).zero?
|
|
335
|
+
return (result << 7) | byte
|
|
336
|
+
else
|
|
337
|
+
result = (result << 7) | (byte & 0x7F)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# If we're here, the encoding is invalid
|
|
342
|
+
raise InvalidFontError, "Invalid UIntBase128 encoding"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Read 255UInt16 variable-length integer
|
|
346
|
+
#
|
|
347
|
+
# Used in transformed glyf table:
|
|
348
|
+
# - If value < 253, it's the value itself (1 byte)
|
|
349
|
+
# - If value == 253, read next byte + 253 (2 bytes)
|
|
350
|
+
# - If value == 254, read next 2 bytes as big-endian (3 bytes)
|
|
351
|
+
# - If value == 255, read next 2 bytes + 506 (3 bytes special)
|
|
352
|
+
#
|
|
353
|
+
# @param io [IO] Open file handle
|
|
354
|
+
# @return [Integer] The decoded integer value
|
|
355
|
+
def read_255_uint16(io)
|
|
356
|
+
first = io.read(1).unpack1("C")
|
|
357
|
+
return nil unless first
|
|
358
|
+
|
|
359
|
+
case first
|
|
360
|
+
when 0..252
|
|
361
|
+
first
|
|
362
|
+
when 253
|
|
363
|
+
second = io.read(1).unpack1("C")
|
|
364
|
+
253 + second
|
|
365
|
+
when 254
|
|
366
|
+
io.read(2).unpack1("n")
|
|
367
|
+
when 255
|
|
368
|
+
value = io.read(2).unpack1("n")
|
|
369
|
+
value + 506
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Read WOFF2 table directory
|
|
374
|
+
#
|
|
375
|
+
# The table directory in WOFF2 is more compact than WOFF,
|
|
376
|
+
# using variable-length integers and known table indices.
|
|
377
|
+
#
|
|
378
|
+
# @param io [IO] Open file handle
|
|
379
|
+
# @return [void]
|
|
380
|
+
def read_table_directory(io)
|
|
381
|
+
@table_entries = []
|
|
382
|
+
|
|
383
|
+
header.num_tables.times do
|
|
384
|
+
entry = Woff2TableDirectoryEntry.new
|
|
385
|
+
|
|
386
|
+
# Read flags byte
|
|
387
|
+
flags = io.read(1).unpack1("C")
|
|
388
|
+
entry.flags = flags
|
|
389
|
+
|
|
390
|
+
# Determine tag
|
|
391
|
+
tag_index = flags & 0x3F
|
|
392
|
+
if tag_index == 0x3F
|
|
393
|
+
# Custom tag (4 bytes)
|
|
394
|
+
entry.tag = io.read(4).force_encoding("UTF-8")
|
|
395
|
+
else
|
|
396
|
+
# Known tag from table
|
|
397
|
+
entry.tag = Woff2TableDirectoryEntry::KNOWN_TAGS[tag_index]
|
|
398
|
+
unless entry.tag
|
|
399
|
+
raise InvalidFontError, "Invalid table tag index: #{tag_index}"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Read orig_length (UIntBase128)
|
|
404
|
+
entry.orig_length = read_uint_base128(io)
|
|
405
|
+
|
|
406
|
+
# For transformed tables, read transform_length
|
|
407
|
+
transform_version = (flags >> 6) & 0x03
|
|
408
|
+
if transform_version != 0 && ["glyf", "loca",
|
|
409
|
+
"hmtx"].include?(entry.tag)
|
|
410
|
+
entry.transform_length = read_uint_base128(io)
|
|
411
|
+
entry.transform_version = transform_version
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
@table_entries << entry
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Decompress table data block and reconstruct tables
|
|
419
|
+
#
|
|
420
|
+
# WOFF2 stores all table data in a single Brotli-compressed block.
|
|
421
|
+
# After decompression, we need to:
|
|
422
|
+
# 1. Split into individual tables
|
|
423
|
+
# 2. Reconstruct transformed tables (glyf, loca, hmtx)
|
|
424
|
+
#
|
|
425
|
+
# @param io [IO] Open file handle
|
|
426
|
+
# @return [void]
|
|
427
|
+
def decompress_and_parse_tables(io)
|
|
428
|
+
# Position after table directory
|
|
429
|
+
# The compressed data starts immediately after the table directory
|
|
430
|
+
compressed_offset = header.to_binary_s.bytesize +
|
|
431
|
+
calculate_table_directory_size
|
|
432
|
+
|
|
433
|
+
io.seek(compressed_offset)
|
|
434
|
+
compressed_data = io.read(header.total_compressed_size)
|
|
435
|
+
|
|
436
|
+
# Decompress entire data block with Brotli
|
|
437
|
+
decompressed_data = Brotli.inflate(compressed_data)
|
|
438
|
+
|
|
439
|
+
# Split decompressed data into individual tables
|
|
440
|
+
offset = 0
|
|
441
|
+
table_entries.each do |entry|
|
|
442
|
+
table_size = entry.transform_length || entry.orig_length
|
|
443
|
+
|
|
444
|
+
table_data = decompressed_data[offset, table_size]
|
|
445
|
+
offset += table_size
|
|
446
|
+
|
|
447
|
+
# Reconstruct transformed tables
|
|
448
|
+
if entry.transform_version && entry.transform_version != Woff2TableDirectoryEntry::TRANSFORM_NONE
|
|
449
|
+
table_data = reconstruct_transformed_table(entry, table_data)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
@decompressed_tables[entry.tag] = table_data
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Calculate size of table directory
|
|
457
|
+
#
|
|
458
|
+
# Variable-length encoding makes this non-trivial
|
|
459
|
+
#
|
|
460
|
+
# @return [Integer] Size in bytes
|
|
461
|
+
def calculate_table_directory_size
|
|
462
|
+
size = 0
|
|
463
|
+
table_entries.each do |entry|
|
|
464
|
+
size += 1 # flags byte
|
|
465
|
+
|
|
466
|
+
# Tag (4 bytes if custom, 0 if known)
|
|
467
|
+
tag_index = entry.flags & 0x3F
|
|
468
|
+
size += 4 if tag_index == 0x3F
|
|
469
|
+
|
|
470
|
+
# orig_length (UIntBase128) - estimate
|
|
471
|
+
size += uint_base128_size(entry.orig_length)
|
|
472
|
+
|
|
473
|
+
# transform_length if present
|
|
474
|
+
if entry.transform_version && entry.transform_version != Woff2TableDirectoryEntry::TRANSFORM_NONE
|
|
475
|
+
size += uint_base128_size(entry.transform_length)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
size
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Estimate size of UIntBase128 encoded value
|
|
482
|
+
#
|
|
483
|
+
# @param value [Integer] The value to encode
|
|
484
|
+
# @return [Integer] Estimated size in bytes
|
|
485
|
+
def uint_base128_size(value)
|
|
486
|
+
return 1 if value < 128
|
|
487
|
+
|
|
488
|
+
bytes = 0
|
|
489
|
+
v = value
|
|
490
|
+
while v.positive?
|
|
491
|
+
bytes += 1
|
|
492
|
+
v >>= 7
|
|
493
|
+
end
|
|
494
|
+
[bytes, 5].min # Max 5 bytes
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Reconstruct transformed table from WOFF2 format
|
|
498
|
+
#
|
|
499
|
+
# WOFF2 can transform certain tables for better compression:
|
|
500
|
+
# - glyf/loca: Complex transformation with multiple streams
|
|
501
|
+
# - hmtx: Can omit redundant data
|
|
502
|
+
#
|
|
503
|
+
# @param entry [Woff2TableDirectoryEntry] Table entry
|
|
504
|
+
# @param data [String] Transformed table data
|
|
505
|
+
# @return [String] Reconstructed standard table data
|
|
506
|
+
def reconstruct_transformed_table(entry, data)
|
|
507
|
+
case entry.tag
|
|
508
|
+
when "glyf", "loca"
|
|
509
|
+
reconstruct_glyf_loca(entry, data)
|
|
510
|
+
when "hmtx"
|
|
511
|
+
reconstruct_hmtx(entry, data)
|
|
512
|
+
else
|
|
513
|
+
# Unknown transformation, return as-is
|
|
514
|
+
data
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Reconstruct glyf/loca tables from WOFF2 transformed format
|
|
519
|
+
#
|
|
520
|
+
# This is the most complex WOFF2 transformation. The transformed
|
|
521
|
+
# glyf table contains multiple streams that need to be reconstructed.
|
|
522
|
+
#
|
|
523
|
+
# @param entry [Woff2TableDirectoryEntry] Table entry
|
|
524
|
+
# @param data [String] Transformed data
|
|
525
|
+
# @return [String] Reconstructed glyf or loca table data
|
|
526
|
+
def reconstruct_glyf_loca(_entry, _data)
|
|
527
|
+
# TODO: Implement full glyf/loca reconstruction
|
|
528
|
+
# This is extremely complex and requires:
|
|
529
|
+
# 1. Parse glyph streams (nContour, nPoints, flags, coords, etc.)
|
|
530
|
+
# 2. Reconstruct standard glyf format
|
|
531
|
+
# 3. Build loca table with proper offsets
|
|
532
|
+
#
|
|
533
|
+
# For now, return empty data to prevent crashes
|
|
534
|
+
# This will need proper implementation for production use
|
|
535
|
+
warn "WOFF2 transformed glyf/loca reconstruction not yet implemented"
|
|
536
|
+
""
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Reconstruct hmtx table from WOFF2 transformed format
|
|
540
|
+
#
|
|
541
|
+
# WOFF2 can store hmtx in a more compact format by:
|
|
542
|
+
# - Omitting redundant advance widths
|
|
543
|
+
# - Using flags to indicate presence of LSB array
|
|
544
|
+
#
|
|
545
|
+
# @param entry [Woff2TableDirectoryEntry] Table entry
|
|
546
|
+
# @param data [String] Transformed data
|
|
547
|
+
# @return [String] Reconstructed hmtx table data
|
|
548
|
+
def reconstruct_hmtx(_entry, data)
|
|
549
|
+
# TODO: Implement hmtx reconstruction
|
|
550
|
+
# This requires:
|
|
551
|
+
# 1. Parse flags
|
|
552
|
+
# 2. Reconstruct advance width array
|
|
553
|
+
# 3. Reconstruct LSB array (if present) or derive from glyf
|
|
554
|
+
#
|
|
555
|
+
# For now, return as-is
|
|
556
|
+
warn "WOFF2 transformed hmtx reconstruction not yet implemented"
|
|
557
|
+
data
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Parse a table from decompressed data
|
|
561
|
+
#
|
|
562
|
+
# @param tag [String] The table tag to parse
|
|
563
|
+
# @return [Tables::*, nil] Parsed table object or nil
|
|
564
|
+
def parse_table(tag)
|
|
565
|
+
raw_data = table_data(tag)
|
|
566
|
+
return nil unless raw_data
|
|
567
|
+
|
|
568
|
+
table_class = table_class_for(tag)
|
|
569
|
+
return nil unless table_class
|
|
570
|
+
|
|
571
|
+
table_class.read(raw_data)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Map table tag to parser class
|
|
575
|
+
#
|
|
576
|
+
# @param tag [String] The table tag
|
|
577
|
+
# @return [Class, nil] Table parser class or nil
|
|
578
|
+
def table_class_for(tag)
|
|
579
|
+
{
|
|
580
|
+
Constants::HEAD_TAG => Tables::Head,
|
|
581
|
+
Constants::HHEA_TAG => Tables::Hhea,
|
|
582
|
+
Constants::HMTX_TAG => Tables::Hmtx,
|
|
583
|
+
Constants::MAXP_TAG => Tables::Maxp,
|
|
584
|
+
Constants::NAME_TAG => Tables::Name,
|
|
585
|
+
Constants::OS2_TAG => Tables::Os2,
|
|
586
|
+
Constants::POST_TAG => Tables::Post,
|
|
587
|
+
Constants::CMAP_TAG => Tables::Cmap,
|
|
588
|
+
Constants::FVAR_TAG => Tables::Fvar,
|
|
589
|
+
Constants::GSUB_TAG => Tables::Gsub,
|
|
590
|
+
Constants::GPOS_TAG => Tables::Gpos,
|
|
591
|
+
}[tag]
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Build an SFNT font file (TTF or OTF) from decompressed WOFF2 data
|
|
595
|
+
#
|
|
596
|
+
# @param output_path [String] Path where font will be written
|
|
597
|
+
# @param sfnt_version [Integer] SFNT version
|
|
598
|
+
# @return [Integer] Number of bytes written
|
|
599
|
+
def build_sfnt_font(output_path, sfnt_version)
|
|
600
|
+
File.open(output_path, "wb") do |io|
|
|
601
|
+
# Calculate offset table fields
|
|
602
|
+
num_tables = table_entries.length
|
|
603
|
+
search_range, entry_selector, range_shift = calculate_offset_table_fields(num_tables)
|
|
604
|
+
|
|
605
|
+
# Write offset table
|
|
606
|
+
io.write([sfnt_version].pack("N"))
|
|
607
|
+
io.write([num_tables].pack("n"))
|
|
608
|
+
io.write([search_range].pack("n"))
|
|
609
|
+
io.write([entry_selector].pack("n"))
|
|
610
|
+
io.write([range_shift].pack("n"))
|
|
611
|
+
|
|
612
|
+
# Calculate table offsets
|
|
613
|
+
offset = 12 + (num_tables * 16) # Header + directory
|
|
614
|
+
table_records = []
|
|
615
|
+
|
|
616
|
+
table_entries.each do |entry|
|
|
617
|
+
tag = entry.tag
|
|
618
|
+
data = @decompressed_tables[tag]
|
|
619
|
+
next unless data
|
|
620
|
+
|
|
621
|
+
length = data.bytesize
|
|
622
|
+
|
|
623
|
+
# Calculate checksum
|
|
624
|
+
checksum = Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
625
|
+
|
|
626
|
+
table_records << {
|
|
627
|
+
tag: tag,
|
|
628
|
+
checksum: checksum,
|
|
629
|
+
offset: offset,
|
|
630
|
+
length: length,
|
|
631
|
+
data: data,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# Update offset for next table (with padding)
|
|
635
|
+
offset += length
|
|
636
|
+
padding = (Constants::TABLE_ALIGNMENT - (length % Constants::TABLE_ALIGNMENT)) %
|
|
637
|
+
Constants::TABLE_ALIGNMENT
|
|
638
|
+
offset += padding
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Write table directory
|
|
642
|
+
table_records.each do |record|
|
|
643
|
+
io.write(record[:tag].ljust(4, "\x00"))
|
|
644
|
+
io.write([record[:checksum]].pack("N"))
|
|
645
|
+
io.write([record[:offset]].pack("N"))
|
|
646
|
+
io.write([record[:length]].pack("N"))
|
|
647
|
+
|
|
648
|
+
# Write table data
|
|
649
|
+
io.write(record[:data])
|
|
650
|
+
|
|
651
|
+
# Add padding
|
|
652
|
+
padding = (Constants::TABLE_ALIGNMENT - (record[:length] % Constants::TABLE_ALIGNMENT)) %
|
|
653
|
+
Constants::TABLE_ALIGNMENT
|
|
654
|
+
io.write("\x00" * padding) if padding.positive?
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
io.pos
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Update checksum adjustment in head table
|
|
661
|
+
update_checksum_adjustment_in_file(output_path)
|
|
662
|
+
|
|
663
|
+
File.size(output_path)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Calculate offset table fields
|
|
667
|
+
#
|
|
668
|
+
# @param num_tables [Integer] Number of tables
|
|
669
|
+
# @return [Array<Integer>] [searchRange, entrySelector, rangeShift]
|
|
670
|
+
def calculate_offset_table_fields(num_tables)
|
|
671
|
+
entry_selector = (Math.log(num_tables) / Math.log(2)).floor
|
|
672
|
+
search_range = (2**entry_selector) * 16
|
|
673
|
+
range_shift = num_tables * 16 - search_range
|
|
674
|
+
[search_range, entry_selector, range_shift]
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# Update checksumAdjustment field in head table
|
|
678
|
+
#
|
|
679
|
+
# @param path [String] Path to the font file
|
|
680
|
+
# @return [void]
|
|
681
|
+
def update_checksum_adjustment_in_file(path)
|
|
682
|
+
# Calculate file checksum
|
|
683
|
+
checksum = Utilities::ChecksumCalculator.calculate_file_checksum(path)
|
|
684
|
+
|
|
685
|
+
# Calculate adjustment
|
|
686
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
687
|
+
|
|
688
|
+
# Find head table position in output file
|
|
689
|
+
File.open(path, "rb") do |io|
|
|
690
|
+
io.seek(4) # Skip sfnt_version
|
|
691
|
+
num_tables = io.read(2).unpack1("n")
|
|
692
|
+
io.seek(12) # Start of table directory
|
|
693
|
+
|
|
694
|
+
num_tables.times do
|
|
695
|
+
tag = io.read(4)
|
|
696
|
+
io.read(4) # checksum
|
|
697
|
+
offset = io.read(4).unpack1("N")
|
|
698
|
+
io.read(4) # length
|
|
699
|
+
|
|
700
|
+
if tag == Constants::HEAD_TAG
|
|
701
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
702
|
+
File.open(path, "r+b") do |write_io|
|
|
703
|
+
write_io.seek(offset + 8)
|
|
704
|
+
write_io.write([adjustment].pack("N"))
|
|
705
|
+
end
|
|
706
|
+
break
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
end
|