fontisan 0.1.0 → 0.2.1
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 +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- 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 +364 -4
- data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
- data/lib/fontisan/font_writer.rb +302 -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 +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- 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/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -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 +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -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 +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -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/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -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/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -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 +231 -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 +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -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/validation/variable_font_validator.rb +218 -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 +375 -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/instance_writer.rb +341 -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/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require "brotli"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require_relative "constants"
|
|
7
|
+
require_relative "loading_modes"
|
|
8
|
+
require_relative "utilities/checksum_calculator"
|
|
9
|
+
require_relative "woff2/header"
|
|
10
|
+
require_relative "woff2/glyf_transformer"
|
|
11
|
+
require_relative "woff2/hmtx_transformer"
|
|
12
|
+
require_relative "true_type_font"
|
|
13
|
+
require_relative "open_type_font"
|
|
14
|
+
require_relative "error"
|
|
15
|
+
|
|
16
|
+
module Fontisan
|
|
17
|
+
# WOFF2 Table Directory Entry structure
|
|
18
|
+
#
|
|
19
|
+
# WOFF2 table directory entries are more complex than WOFF,
|
|
20
|
+
# with transformation flags and variable-length sizes.
|
|
21
|
+
class Woff2TableDirectoryEntry
|
|
22
|
+
attr_accessor :tag, :flags, :transform_version, :orig_length,
|
|
23
|
+
:transform_length, :offset
|
|
24
|
+
|
|
25
|
+
# Transformation version flags
|
|
26
|
+
TRANSFORM_NONE = 0
|
|
27
|
+
TRANSFORM_GLYF_LOCA = 0
|
|
28
|
+
TRANSFORM_LOCA = 1
|
|
29
|
+
TRANSFORM_HMTX = 2
|
|
30
|
+
|
|
31
|
+
# Known table tags with assigned indices (0-62)
|
|
32
|
+
KNOWN_TAGS = [
|
|
33
|
+
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
|
|
34
|
+
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
|
35
|
+
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
|
|
36
|
+
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
|
37
|
+
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
|
|
38
|
+
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
|
39
|
+
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
|
|
40
|
+
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@flags = 0
|
|
45
|
+
# Don't initialize transform_version - leave it nil
|
|
46
|
+
# It will be set during parsing if table is transformed
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if table is transformed
|
|
50
|
+
def transformed?
|
|
51
|
+
(@flags & 0x3F) != 0x3F && KNOWN_TAGS[tag_index]&.start_with?(/glyf|loca|hmtx/)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get transform version for this table
|
|
55
|
+
def transform_version
|
|
56
|
+
return TRANSFORM_NONE unless transformed?
|
|
57
|
+
|
|
58
|
+
case tag
|
|
59
|
+
when "glyf", "loca"
|
|
60
|
+
TRANSFORM_GLYF_LOCA
|
|
61
|
+
when "hmtx"
|
|
62
|
+
TRANSFORM_HMTX
|
|
63
|
+
else
|
|
64
|
+
TRANSFORM_NONE
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def tag_index
|
|
71
|
+
@flags & 0x3F
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Web Open Font Format 2.0 (WOFF2) font loader
|
|
76
|
+
#
|
|
77
|
+
# This class manages WOFF2 font files and provides access to
|
|
78
|
+
# decompressed tables and transformed data.
|
|
79
|
+
#
|
|
80
|
+
# @example Reading a WOFF2 font
|
|
81
|
+
# font = Woff2Font.from_file("font.woff2")
|
|
82
|
+
# puts font.header.flavor
|
|
83
|
+
# puts font.table_names
|
|
84
|
+
class Woff2Font
|
|
85
|
+
# Simple struct for storing file path
|
|
86
|
+
IOSource = Struct.new(:path)
|
|
87
|
+
|
|
88
|
+
attr_accessor :header, :table_entries, :decompressed_tables, :parsed_tables, :io_source
|
|
89
|
+
attr_accessor :underlying_font # Allow both reading and setting for table delegation
|
|
90
|
+
|
|
91
|
+
def initialize
|
|
92
|
+
@header = nil
|
|
93
|
+
@table_entries = []
|
|
94
|
+
@decompressed_tables = {}
|
|
95
|
+
@parsed_tables = {}
|
|
96
|
+
@io_source = nil
|
|
97
|
+
@underlying_font = nil # Store the actual TrueTypeFont/OpenTypeFont
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Initialize storage hashes
|
|
101
|
+
def initialize_storage
|
|
102
|
+
@decompressed_tables ||= {}
|
|
103
|
+
@initialize_storage ||= {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if font has TrueType flavor
|
|
107
|
+
def truetype?
|
|
108
|
+
return false unless @header
|
|
109
|
+
|
|
110
|
+
[Constants::SFNT_VERSION_TRUETYPE, 0x00010000].include?(@header.flavor)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if font has CFF flavor
|
|
114
|
+
def cff?
|
|
115
|
+
return false unless @header
|
|
116
|
+
|
|
117
|
+
[Constants::SFNT_VERSION_OTTO, 0x4F54544F].include?(@header.flavor)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if font is a variable font
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] true if font has fvar table (variable font)
|
|
123
|
+
def variable_font?
|
|
124
|
+
has_table?("fvar")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validate WOFF2 signature
|
|
128
|
+
def validate_signature!
|
|
129
|
+
unless @header && @header.signature == Woff2::Woff2Header::SIGNATURE
|
|
130
|
+
raise InvalidFontError, "Invalid WOFF2 signature"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if font is valid
|
|
135
|
+
def valid?
|
|
136
|
+
return false unless @header
|
|
137
|
+
return false unless @header.signature == Woff2::Woff2Header::SIGNATURE
|
|
138
|
+
return false unless @header.num_tables == @table_entries.length
|
|
139
|
+
return false unless has_table?("head")
|
|
140
|
+
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check if table exists
|
|
145
|
+
def has_table?(tag)
|
|
146
|
+
@table_entries.any? { |entry| entry.tag == tag }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Find table entry by tag
|
|
150
|
+
def find_table_entry(tag)
|
|
151
|
+
@table_entries.find { |entry| entry.tag == tag }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get list of table tags
|
|
155
|
+
def table_names
|
|
156
|
+
@table_entries.map(&:tag)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get decompressed table data
|
|
160
|
+
def table_data(tag)
|
|
161
|
+
# First try underlying font's table data if available
|
|
162
|
+
if @underlying_font && @underlying_font.respond_to?(:table_data)
|
|
163
|
+
underlying_data = @underlying_font.table_data[tag]
|
|
164
|
+
return underlying_data if underlying_data
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Fallback to decompressed_tables
|
|
168
|
+
@decompressed_tables[tag]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get parsed table object
|
|
172
|
+
def table(tag)
|
|
173
|
+
# Delegate to underlying font if available
|
|
174
|
+
return @underlying_font.table(tag) if @underlying_font
|
|
175
|
+
|
|
176
|
+
# Fallback to parsed_tables hash
|
|
177
|
+
# Normalize tag to UTF-8 string for hash lookup
|
|
178
|
+
# Use dup to create mutable copy since force_encoding modifies in place
|
|
179
|
+
tag_key = tag.to_s.dup.force_encoding("UTF-8")
|
|
180
|
+
@parsed_tables[tag_key]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Convert to TTF
|
|
184
|
+
def to_ttf(output_path)
|
|
185
|
+
unless truetype?
|
|
186
|
+
raise InvalidFontError, "Cannot convert to TTF: font is not TrueType flavored"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Build SFNT and create TrueTypeFont
|
|
190
|
+
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries, @decompressed_tables)
|
|
191
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
192
|
+
|
|
193
|
+
# Create actual TrueTypeFont and save for table delegation
|
|
194
|
+
@underlying_font = TrueTypeFont.read(sfnt_io)
|
|
195
|
+
@underlying_font.initialize_storage
|
|
196
|
+
@underlying_font.read_table_data(sfnt_io)
|
|
197
|
+
|
|
198
|
+
FontWriter.write_to_file(@underlying_font.tables, output_path)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Convert to OTF
|
|
202
|
+
def to_otf(output_path)
|
|
203
|
+
unless cff?
|
|
204
|
+
raise InvalidFontError, "Cannot convert to OTF: font is not CFF flavored"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Build SFNT and create OpenTypeFont
|
|
208
|
+
sfnt_data = self.class.build_sfnt_in_memory(@header, @table_entries, @decompressed_tables)
|
|
209
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
210
|
+
|
|
211
|
+
# Create actual OpenTypeFont and save for table delegation
|
|
212
|
+
@underlying_font = OpenTypeFont.read(sfnt_io)
|
|
213
|
+
@underlying_font.initialize_storage
|
|
214
|
+
@underlying_font.read_table_data(sfnt_io)
|
|
215
|
+
|
|
216
|
+
FontWriter.write_to_file(@underlying_font.tables, output_path)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Get metadata (if present)
|
|
220
|
+
def metadata
|
|
221
|
+
return nil unless @header&.meta_length&.positive?
|
|
222
|
+
return nil unless @io_source
|
|
223
|
+
|
|
224
|
+
begin
|
|
225
|
+
File.open(@io_source.path, "rb") do |io|
|
|
226
|
+
io.seek(@header.meta_offset)
|
|
227
|
+
compressed_meta = io.read(@header.meta_length)
|
|
228
|
+
Brotli.inflate(compressed_meta)
|
|
229
|
+
end
|
|
230
|
+
rescue StandardError => e
|
|
231
|
+
warn "Failed to decompress metadata: #{e.message}"
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Convenience methods for accessing common name table fields
|
|
237
|
+
|
|
238
|
+
# Get font family name
|
|
239
|
+
def family_name
|
|
240
|
+
name_table = table("name")
|
|
241
|
+
name_table&.english_name(Tables::Name::FAMILY)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Get font subfamily name
|
|
245
|
+
def subfamily_name
|
|
246
|
+
name_table = table("name")
|
|
247
|
+
name_table&.english_name(Tables::Name::SUBFAMILY)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get full font name
|
|
251
|
+
def full_name
|
|
252
|
+
name_table = table("name")
|
|
253
|
+
name_table&.english_name(Tables::Name::FULL_NAME)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Get PostScript name
|
|
257
|
+
def post_script_name
|
|
258
|
+
name_table = table("name")
|
|
259
|
+
name_table&.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Get preferred family name
|
|
263
|
+
def preferred_family_name
|
|
264
|
+
name_table = table("name")
|
|
265
|
+
name_table&.english_name(Tables::Name::PREFERRED_FAMILY)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Get preferred subfamily name
|
|
269
|
+
def preferred_subfamily_name
|
|
270
|
+
name_table = table("name")
|
|
271
|
+
name_table&.english_name(Tables::Name::PREFERRED_SUBFAMILY)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Get units per em
|
|
275
|
+
def units_per_em
|
|
276
|
+
head = table("head")
|
|
277
|
+
head&.units_per_em
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Read WOFF2 font from a file and return Woff2Font instance
|
|
281
|
+
#
|
|
282
|
+
# @param path [String] Path to the WOFF2 file
|
|
283
|
+
# @param mode [Symbol] Loading mode (:metadata or :full)
|
|
284
|
+
# @param lazy [Boolean] If true, load tables on demand
|
|
285
|
+
# @return [Woff2Font] The WOFF2 font object
|
|
286
|
+
# @raise [ArgumentError] if path is nil or empty
|
|
287
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
288
|
+
# @raise [InvalidFontError] if file format is invalid
|
|
289
|
+
def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
|
|
290
|
+
if path.nil? || path.to_s.empty?
|
|
291
|
+
raise ArgumentError, "path cannot be nil or empty"
|
|
292
|
+
end
|
|
293
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
294
|
+
|
|
295
|
+
woff2 = new
|
|
296
|
+
woff2.io_source = IOSource.new(path)
|
|
297
|
+
|
|
298
|
+
File.open(path, "rb") do |io|
|
|
299
|
+
# Read header to determine font flavor
|
|
300
|
+
woff2.header = Woff2::Woff2Header.read(io)
|
|
301
|
+
|
|
302
|
+
# Validate signature
|
|
303
|
+
unless woff2.header.signature == Woff2::Woff2Header::SIGNATURE
|
|
304
|
+
raise InvalidFontError,
|
|
305
|
+
"Invalid WOFF2 signature: expected 0x#{Woff2::Woff2Header::SIGNATURE.to_s(16)}, " \
|
|
306
|
+
"got 0x#{woff2.header.signature.to_i.to_s(16)}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Read table directory
|
|
310
|
+
woff2.table_entries = read_table_directory_from_io(io, woff2.header)
|
|
311
|
+
|
|
312
|
+
# Decompress table data
|
|
313
|
+
woff2.decompressed_tables = decompress_tables(io, woff2.header, woff2.table_entries)
|
|
314
|
+
|
|
315
|
+
# Apply table transformations if present
|
|
316
|
+
apply_transformations!(woff2.table_entries, woff2.decompressed_tables)
|
|
317
|
+
|
|
318
|
+
# Build SFNT structure in memory
|
|
319
|
+
sfnt_data = build_sfnt_in_memory(woff2.header, woff2.table_entries, woff2.decompressed_tables)
|
|
320
|
+
|
|
321
|
+
# Create StringIO for reading
|
|
322
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
323
|
+
sfnt_io.rewind
|
|
324
|
+
|
|
325
|
+
# Parse tables based on font type
|
|
326
|
+
if woff2.truetype?
|
|
327
|
+
font = TrueTypeFont.read(sfnt_io)
|
|
328
|
+
font.initialize_storage
|
|
329
|
+
font.loading_mode = mode
|
|
330
|
+
font.lazy_load_enabled = lazy
|
|
331
|
+
|
|
332
|
+
# Create fresh StringIO for table data reading
|
|
333
|
+
table_io = StringIO.new(sfnt_data)
|
|
334
|
+
font.read_table_data(table_io)
|
|
335
|
+
|
|
336
|
+
# Store underlying font for table access delegation
|
|
337
|
+
woff2.underlying_font = font
|
|
338
|
+
woff2.parsed_tables = font.parsed_tables
|
|
339
|
+
elsif woff2.cff?
|
|
340
|
+
font = OpenTypeFont.read(sfnt_io)
|
|
341
|
+
font.initialize_storage
|
|
342
|
+
font.loading_mode = mode
|
|
343
|
+
font.lazy_load_enabled = lazy
|
|
344
|
+
|
|
345
|
+
# Create fresh StringIO for table data reading
|
|
346
|
+
table_io = StringIO.new(sfnt_data)
|
|
347
|
+
font.read_table_data(table_io)
|
|
348
|
+
|
|
349
|
+
# Store underlying font for table access delegation
|
|
350
|
+
woff2.underlying_font = font
|
|
351
|
+
woff2.parsed_tables = font.parsed_tables
|
|
352
|
+
else
|
|
353
|
+
raise InvalidFontError,
|
|
354
|
+
"Unknown WOFF2 flavor: 0x#{woff2.header.flavor.to_s(16)}"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
woff2
|
|
359
|
+
rescue BinData::ValidityError, EOFError => e
|
|
360
|
+
raise InvalidFontError, "Invalid WOFF2 file: #{e.message}"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Read table directory from IO
|
|
364
|
+
#
|
|
365
|
+
# @param io [IO] Open file handle
|
|
366
|
+
# @param header [Woff2::Woff2Header] WOFF2 header
|
|
367
|
+
# @return [Array<Woff2TableDirectoryEntry>] Table entries
|
|
368
|
+
def self.read_table_directory_from_io(io, header)
|
|
369
|
+
table_entries = []
|
|
370
|
+
|
|
371
|
+
header.num_tables.times do
|
|
372
|
+
entry = Woff2TableDirectoryEntry.new
|
|
373
|
+
|
|
374
|
+
# Read flags byte with nil check
|
|
375
|
+
flags_data = io.read(1)
|
|
376
|
+
raise EOFError, "Unexpected EOF while reading table directory flags" if flags_data.nil?
|
|
377
|
+
|
|
378
|
+
flags = flags_data.unpack1("C")
|
|
379
|
+
entry.flags = flags
|
|
380
|
+
|
|
381
|
+
# Determine tag
|
|
382
|
+
tag_index = flags & 0x3F
|
|
383
|
+
if tag_index == 0x3F
|
|
384
|
+
# Custom tag (4 bytes)
|
|
385
|
+
tag_data = io.read(4)
|
|
386
|
+
raise EOFError, "Unexpected EOF while reading custom tag" if tag_data.nil? || tag_data.bytesize < 4
|
|
387
|
+
entry.tag = tag_data.force_encoding("UTF-8")
|
|
388
|
+
else
|
|
389
|
+
# Known tag from table
|
|
390
|
+
entry.tag = Woff2TableDirectoryEntry::KNOWN_TAGS[tag_index]
|
|
391
|
+
unless entry.tag
|
|
392
|
+
raise InvalidFontError, "Invalid table tag index: #{tag_index}"
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Read orig_length (UIntBase128)
|
|
397
|
+
entry.orig_length = read_uint_base128_from_io(io)
|
|
398
|
+
|
|
399
|
+
# Determine if transformLength should be read
|
|
400
|
+
# According to WOFF2 spec section 4.2:
|
|
401
|
+
# - glyf/loca with version 0: TRANSFORMED (transformLength present)
|
|
402
|
+
# - hmtx with non-zero version: TRANSFORMED (transformLength present)
|
|
403
|
+
# - all other tables: transformation version is 0 (no transformLength)
|
|
404
|
+
transform_version = (flags >> 6) & 0x03
|
|
405
|
+
has_transform_length = if ["glyf", "loca"].include?(entry.tag) && transform_version.zero?
|
|
406
|
+
true
|
|
407
|
+
elsif entry.tag == "hmtx" && transform_version != 0
|
|
408
|
+
true
|
|
409
|
+
else
|
|
410
|
+
false
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
if has_transform_length
|
|
414
|
+
entry.transform_length = read_uint_base128_from_io(io)
|
|
415
|
+
entry.transform_version = transform_version
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
table_entries << entry
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
table_entries
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Read variable-length UIntBase128 integer from IO
|
|
425
|
+
#
|
|
426
|
+
# @param io [IO] Open file handle
|
|
427
|
+
# @return [Integer] The decoded integer value
|
|
428
|
+
def self.read_uint_base128_from_io(io)
|
|
429
|
+
result = 0
|
|
430
|
+
5.times do
|
|
431
|
+
byte_data = io.read(1)
|
|
432
|
+
raise EOFError, "Unexpected EOF while reading UIntBase128" if byte_data.nil?
|
|
433
|
+
|
|
434
|
+
byte = byte_data.unpack1("C")
|
|
435
|
+
|
|
436
|
+
# Continue if high bit is set
|
|
437
|
+
if (byte & 0x80).zero?
|
|
438
|
+
return (result << 7) | byte
|
|
439
|
+
else
|
|
440
|
+
result = (result << 7) | (byte & 0x7F)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# If we're here, the encoding is invalid
|
|
445
|
+
raise InvalidFontError, "Invalid UIntBase128 encoding"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Decompress tables from WOFF2 compressed data block
|
|
449
|
+
#
|
|
450
|
+
# @param io [IO] Open file handle
|
|
451
|
+
# @param header [Woff2::Woff2Header] WOFF2 header
|
|
452
|
+
# @param table_entries [Array<Woff2TableDirectoryEntry>] Table entries
|
|
453
|
+
# @return [Hash<String, String>] Map of tag to decompressed data
|
|
454
|
+
def self.decompress_tables(io, header, table_entries)
|
|
455
|
+
# IO stream is already positioned at compressed data after reading table directory
|
|
456
|
+
# No need to seek - just read from current position
|
|
457
|
+
compressed_data = io.read(header.total_compressed_size)
|
|
458
|
+
|
|
459
|
+
# Decompress entire data block with Brotli
|
|
460
|
+
decompressed_data = Brotli.inflate(compressed_data)
|
|
461
|
+
|
|
462
|
+
# Split decompressed data into individual tables
|
|
463
|
+
decompressed_tables = {}
|
|
464
|
+
offset = 0
|
|
465
|
+
|
|
466
|
+
table_entries.each do |entry|
|
|
467
|
+
table_size = entry.transform_length || entry.orig_length
|
|
468
|
+
table_data = decompressed_data[offset, table_size]
|
|
469
|
+
offset += table_size
|
|
470
|
+
|
|
471
|
+
decompressed_tables[entry.tag] = table_data
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
decompressed_tables
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Apply table transformations for glyf/loca/hmtx tables
|
|
478
|
+
#
|
|
479
|
+
# @param table_entries [Array<Woff2TableDirectoryEntry>] Table entries
|
|
480
|
+
# @param decompressed_tables [Hash<String, String>] Decompressed tables
|
|
481
|
+
# @return [void] Modifies decompressed_tables in place
|
|
482
|
+
def self.apply_transformations!(table_entries, decompressed_tables)
|
|
483
|
+
# Find entries that need transformation
|
|
484
|
+
glyf_entry = table_entries.find { |e| e.tag == "glyf" }
|
|
485
|
+
hmtx_entry = table_entries.find { |e| e.tag == "hmtx" }
|
|
486
|
+
|
|
487
|
+
# Get required metadata for transformations
|
|
488
|
+
maxp_data = decompressed_tables["maxp"]
|
|
489
|
+
hhea_data = decompressed_tables["hhea"]
|
|
490
|
+
|
|
491
|
+
return unless maxp_data && hhea_data
|
|
492
|
+
|
|
493
|
+
# Parse num_glyphs from maxp table
|
|
494
|
+
# maxp format: version(4) + numGlyphs(2) + ...
|
|
495
|
+
num_glyphs = maxp_data[4, 2].unpack1("n")
|
|
496
|
+
|
|
497
|
+
# Parse numberOfHMetrics from hhea table
|
|
498
|
+
# hhea format: ... + numberOfHMetrics(2) at offset 34
|
|
499
|
+
number_of_h_metrics = hhea_data[34, 2].unpack1("n")
|
|
500
|
+
|
|
501
|
+
# Check if this is a variable font by checking for fvar table
|
|
502
|
+
variable_font = table_entries.any? { |e| e.tag == "fvar" }
|
|
503
|
+
|
|
504
|
+
# Transform glyf/loca if needed
|
|
505
|
+
# transform_length is only set when table is actually transformed
|
|
506
|
+
# Check that transform_length exists and is greater than 0
|
|
507
|
+
if glyf_entry&.instance_variable_defined?(:@transform_length) &&
|
|
508
|
+
glyf_entry.transform_length&.positive?
|
|
509
|
+
transformed_glyf = decompressed_tables["glyf"]
|
|
510
|
+
|
|
511
|
+
if transformed_glyf
|
|
512
|
+
result = Woff2::GlyfTransformer.reconstruct(
|
|
513
|
+
transformed_glyf,
|
|
514
|
+
num_glyphs,
|
|
515
|
+
variable_font: variable_font
|
|
516
|
+
)
|
|
517
|
+
decompressed_tables["glyf"] = result[:glyf]
|
|
518
|
+
decompressed_tables["loca"] = result[:loca]
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Transform hmtx if needed
|
|
523
|
+
# transform_length is only set when table is actually transformed
|
|
524
|
+
# Check that transform_length exists and is greater than 0
|
|
525
|
+
if hmtx_entry&.instance_variable_defined?(:@transform_length) &&
|
|
526
|
+
hmtx_entry.transform_length&.positive?
|
|
527
|
+
transformed_hmtx = decompressed_tables["hmtx"]
|
|
528
|
+
|
|
529
|
+
if transformed_hmtx
|
|
530
|
+
decompressed_tables["hmtx"] = Woff2::HmtxTransformer.reconstruct(
|
|
531
|
+
transformed_hmtx,
|
|
532
|
+
num_glyphs,
|
|
533
|
+
number_of_h_metrics,
|
|
534
|
+
)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Calculate size of table directory
|
|
540
|
+
#
|
|
541
|
+
# @param table_entries [Array<Woff2TableDirectoryEntry>] Table entries
|
|
542
|
+
# @return [Integer] Size in bytes
|
|
543
|
+
def self.calculate_table_directory_size(table_entries)
|
|
544
|
+
size = 0
|
|
545
|
+
table_entries.each do |entry|
|
|
546
|
+
size += 1 # flags byte
|
|
547
|
+
|
|
548
|
+
# Tag (4 bytes if custom, 0 if known)
|
|
549
|
+
tag_index = entry.flags & 0x3F
|
|
550
|
+
size += 4 if tag_index == 0x3F
|
|
551
|
+
|
|
552
|
+
# orig_length (UIntBase128) - estimate
|
|
553
|
+
size += uint_base128_size(entry.orig_length)
|
|
554
|
+
|
|
555
|
+
# transform_length if present
|
|
556
|
+
if entry.transform_version && !entry.transform_version.nil?
|
|
557
|
+
size += uint_base128_size(entry.transform_length)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
size
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Estimate size of UIntBase128 encoded value
|
|
564
|
+
#
|
|
565
|
+
# @param value [Integer] The value to encode
|
|
566
|
+
# @return [Integer] Estimated size in bytes
|
|
567
|
+
def self.uint_base128_size(value)
|
|
568
|
+
return 1 if value < 128
|
|
569
|
+
|
|
570
|
+
bytes = 0
|
|
571
|
+
v = value
|
|
572
|
+
while v.positive?
|
|
573
|
+
bytes += 1
|
|
574
|
+
v >>= 7
|
|
575
|
+
end
|
|
576
|
+
[
|
|
577
|
+
bytes,
|
|
578
|
+
5,
|
|
579
|
+
].min # Max 5 bytes
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Build SFNT binary structure in memory
|
|
583
|
+
#
|
|
584
|
+
# @param header [Woff2::Woff2Header] WOFF2 header
|
|
585
|
+
# @param table_entries [Array<Woff2TableDirectoryEntry>] Table entries
|
|
586
|
+
# @param decompressed_tables [Hash<String, String>] Decompressed table data
|
|
587
|
+
# @return [String] Complete SFNT binary data
|
|
588
|
+
def self.build_sfnt_in_memory(header, table_entries, decompressed_tables)
|
|
589
|
+
sfnt_data = +""
|
|
590
|
+
|
|
591
|
+
# Calculate offset table fields
|
|
592
|
+
num_tables = table_entries.length
|
|
593
|
+
entry_selector = (Math.log(num_tables) / Math.log(2)).floor
|
|
594
|
+
search_range = (2**entry_selector) * 16
|
|
595
|
+
range_shift = num_tables * 16 - search_range
|
|
596
|
+
|
|
597
|
+
# Write offset table
|
|
598
|
+
sfnt_data << [header.flavor].pack("N")
|
|
599
|
+
sfnt_data << [num_tables].pack("n")
|
|
600
|
+
sfnt_data << [search_range].pack("n")
|
|
601
|
+
sfnt_data << [entry_selector].pack("n")
|
|
602
|
+
sfnt_data << [range_shift].pack("n")
|
|
603
|
+
|
|
604
|
+
# Calculate table offsets
|
|
605
|
+
offset = 12 + (num_tables * 16) # Header + directory
|
|
606
|
+
table_records = []
|
|
607
|
+
|
|
608
|
+
table_entries.each do |entry|
|
|
609
|
+
tag = entry.tag
|
|
610
|
+
data = decompressed_tables[tag]
|
|
611
|
+
next unless data
|
|
612
|
+
|
|
613
|
+
length = data.bytesize
|
|
614
|
+
|
|
615
|
+
# Calculate checksum
|
|
616
|
+
checksum = Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
617
|
+
|
|
618
|
+
table_records << {
|
|
619
|
+
tag: tag,
|
|
620
|
+
checksum: checksum,
|
|
621
|
+
offset: offset,
|
|
622
|
+
length: length,
|
|
623
|
+
data: data,
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# Update offset for next table (with padding)
|
|
627
|
+
offset += length
|
|
628
|
+
padding = (Constants::TABLE_ALIGNMENT - (length % Constants::TABLE_ALIGNMENT)) %
|
|
629
|
+
Constants::TABLE_ALIGNMENT
|
|
630
|
+
offset += padding
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Write table directory
|
|
634
|
+
table_records.each do |record|
|
|
635
|
+
sfnt_data << record[:tag].ljust(4, "\x00")
|
|
636
|
+
sfnt_data << [record[:checksum]].pack("N")
|
|
637
|
+
sfnt_data << [record[:offset]].pack("N")
|
|
638
|
+
sfnt_data << [record[:length]].pack("N")
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Write table data with padding
|
|
642
|
+
table_records.each do |record|
|
|
643
|
+
sfnt_data << record[:data]
|
|
644
|
+
|
|
645
|
+
# Add padding
|
|
646
|
+
padding = (Constants::TABLE_ALIGNMENT - (record[:length] % Constants::TABLE_ALIGNMENT)) %
|
|
647
|
+
Constants::TABLE_ALIGNMENT
|
|
648
|
+
sfnt_data << ("\x00" * padding) if padding.positive?
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Update checksumAdjustment in head table
|
|
652
|
+
update_checksum_in_memory(sfnt_data, table_records)
|
|
653
|
+
|
|
654
|
+
sfnt_data
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Update checksumAdjustment field in head table in memory
|
|
658
|
+
#
|
|
659
|
+
# @param sfnt_data [String] The SFNT binary data
|
|
660
|
+
# @param table_records [Array<Hash>] Table records with offsets
|
|
661
|
+
# @return [void]
|
|
662
|
+
def self.update_checksum_in_memory(sfnt_data, table_records)
|
|
663
|
+
# Find head table record
|
|
664
|
+
head_record = table_records.find { |r| r[:tag] == Constants::HEAD_TAG }
|
|
665
|
+
return unless head_record
|
|
666
|
+
|
|
667
|
+
# Zero out checksumAdjustment field first
|
|
668
|
+
head_offset = head_record[:offset]
|
|
669
|
+
sfnt_data[head_offset + 8, 4] = "\x00\x00\x00\x00"
|
|
670
|
+
|
|
671
|
+
# Calculate file checksum
|
|
672
|
+
checksum = 0
|
|
673
|
+
sfnt_data.bytes.each_slice(4) do |bytes|
|
|
674
|
+
word = bytes.pack("C*").ljust(4, "\x00").unpack1("N")
|
|
675
|
+
checksum = (checksum + word) & 0xFFFFFFFF
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Calculate adjustment
|
|
679
|
+
adjustment = (0xB1B0AFBA - checksum) & 0xFFFFFFFF
|
|
680
|
+
|
|
681
|
+
# Write adjustment to head table
|
|
682
|
+
sfnt_data[head_offset + 8, 4] = [adjustment].pack("N")
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
private
|
|
686
|
+
|
|
687
|
+
# Read variable-length UIntBase128 integer from IO
|
|
688
|
+
def read_uint_base128(io)
|
|
689
|
+
self.class.read_uint_base128_from_io(io)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Read 255UInt16 variable-length integer
|
|
693
|
+
def read_255_uint16(io)
|
|
694
|
+
code = io.read(1).unpack1("C")
|
|
695
|
+
|
|
696
|
+
case code
|
|
697
|
+
when 0..252
|
|
698
|
+
code
|
|
699
|
+
when 253
|
|
700
|
+
253 + io.read(1).unpack1("C")
|
|
701
|
+
when 254
|
|
702
|
+
io.read(2).unpack1("n")
|
|
703
|
+
when 255
|
|
704
|
+
io.read(2).unpack1("n") + 506
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Calculate offset table fields
|
|
709
|
+
def calculate_offset_table_fields(num_tables)
|
|
710
|
+
entry_selector = (Math.log(num_tables) / Math.log(2)).floor
|
|
711
|
+
search_range = (2**entry_selector) * 16
|
|
712
|
+
range_shift = num_tables * 16 - search_range
|
|
713
|
+
|
|
714
|
+
[search_range, entry_selector, range_shift]
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
end
|