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,483 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require_relative "constants"
|
|
6
|
+
require_relative "utilities/checksum_calculator"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
# WOFF Header structure
|
|
10
|
+
class WoffHeader < BinData::Record
|
|
11
|
+
endian :big
|
|
12
|
+
uint32 :signature # 0x774F4646 'wOFF'
|
|
13
|
+
uint32 :flavor # sfnt version (0x00010000 for TTF, 'OTTO' for CFF)
|
|
14
|
+
uint32 :woff_length # Total size of WOFF file
|
|
15
|
+
uint16 :num_tables # Number of entries in directory
|
|
16
|
+
uint16 :reserved # Reserved, must be zero
|
|
17
|
+
uint32 :total_sfnt_size # Total size needed for uncompressed font
|
|
18
|
+
uint16 :major_version # Major version of WOFF file
|
|
19
|
+
uint16 :minor_version # Minor version of WOFF file
|
|
20
|
+
uint32 :meta_offset # Offset to metadata block
|
|
21
|
+
uint32 :meta_length # Length of compressed metadata block
|
|
22
|
+
uint32 :meta_orig_length # Length of uncompressed metadata block
|
|
23
|
+
uint32 :priv_offset # Offset to private data block
|
|
24
|
+
uint32 :priv_length # Length of private data block
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# WOFF Table Directory Entry structure
|
|
28
|
+
class WoffTableDirectoryEntry < BinData::Record
|
|
29
|
+
endian :big
|
|
30
|
+
string :tag, length: 4 # Table identifier
|
|
31
|
+
uint32 :offset # Offset to compressed table data
|
|
32
|
+
uint32 :comp_length # Length of compressed data
|
|
33
|
+
uint32 :orig_length # Length of uncompressed data
|
|
34
|
+
uint32 :orig_checksum # Checksum of uncompressed table
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Web Open Font Format (WOFF) font domain object
|
|
38
|
+
#
|
|
39
|
+
# Represents a WOFF font file that uses zlib compression for table data.
|
|
40
|
+
# WOFF is a simple wrapper format for TTF/OTF fonts with compression.
|
|
41
|
+
#
|
|
42
|
+
# According to the WOFF specification (https://www.w3.org/TR/WOFF/):
|
|
43
|
+
# - Tables are individually compressed using zlib
|
|
44
|
+
# - Optional metadata block (compressed XML)
|
|
45
|
+
# - Optional private data block
|
|
46
|
+
#
|
|
47
|
+
# @example Reading a WOFF font
|
|
48
|
+
# woff = WoffFont.from_file("font.woff")
|
|
49
|
+
# puts woff.header.num_tables
|
|
50
|
+
# name_table = woff.table("name")
|
|
51
|
+
# puts name_table.english_name(Tables::Name::FAMILY)
|
|
52
|
+
#
|
|
53
|
+
# @example Converting to TTF/OTF
|
|
54
|
+
# woff = WoffFont.from_file("font.woff")
|
|
55
|
+
# woff.to_ttf("output.ttf") # if TrueType flavored
|
|
56
|
+
# woff.to_otf("output.otf") # if CFF flavored
|
|
57
|
+
class WoffFont < BinData::Record
|
|
58
|
+
endian :big
|
|
59
|
+
|
|
60
|
+
woff_header :header
|
|
61
|
+
array :table_entries, type: :woff_table_directory_entry, initial_length: lambda {
|
|
62
|
+
header.num_tables
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Table data storage (decompressed on demand)
|
|
66
|
+
attr_accessor :decompressed_tables
|
|
67
|
+
attr_accessor :compressed_table_data
|
|
68
|
+
|
|
69
|
+
# Parsed table instances cache
|
|
70
|
+
attr_accessor :parsed_tables
|
|
71
|
+
|
|
72
|
+
# File IO handle for lazy table decompression
|
|
73
|
+
attr_accessor :io_source
|
|
74
|
+
|
|
75
|
+
# WOFF signature constant
|
|
76
|
+
WOFF_SIGNATURE = 0x774F4646 # 'wOFF'
|
|
77
|
+
|
|
78
|
+
# Read WOFF font from a file
|
|
79
|
+
#
|
|
80
|
+
# @param path [String] Path to the WOFF file
|
|
81
|
+
# @return [WoffFont] A new instance
|
|
82
|
+
# @raise [ArgumentError] if path is nil or empty
|
|
83
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
84
|
+
# @raise [InvalidFontError] if file format is invalid
|
|
85
|
+
def self.from_file(path)
|
|
86
|
+
if path.nil? || path.to_s.empty?
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"path cannot be nil or empty"
|
|
89
|
+
end
|
|
90
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
91
|
+
|
|
92
|
+
File.open(path, "rb") do |io|
|
|
93
|
+
font = read(io)
|
|
94
|
+
font.validate_signature!
|
|
95
|
+
font.initialize_storage
|
|
96
|
+
font.io_source = io
|
|
97
|
+
font.read_compressed_table_data(io)
|
|
98
|
+
font
|
|
99
|
+
end
|
|
100
|
+
rescue BinData::ValidityError, EOFError => e
|
|
101
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
102
|
+
"Invalid WOFF file: #{e.message}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Initialize storage hashes
|
|
106
|
+
#
|
|
107
|
+
# @return [void]
|
|
108
|
+
def initialize_storage
|
|
109
|
+
@decompressed_tables = {}
|
|
110
|
+
@compressed_table_data = {}
|
|
111
|
+
@parsed_tables = {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate WOFF signature
|
|
115
|
+
#
|
|
116
|
+
# @raise [InvalidFontError] if signature is invalid
|
|
117
|
+
# @return [void]
|
|
118
|
+
def validate_signature!
|
|
119
|
+
signature_value = header.signature.to_i
|
|
120
|
+
unless signature_value == WOFF_SIGNATURE
|
|
121
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
122
|
+
"Invalid WOFF signature: expected 0x#{WOFF_SIGNATURE.to_s(16)}, " \
|
|
123
|
+
"got 0x#{signature_value.to_s(16)}")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Read compressed table data for all tables
|
|
128
|
+
#
|
|
129
|
+
# Tables are decompressed on-demand for efficiency
|
|
130
|
+
#
|
|
131
|
+
# @param io [IO] Open file handle
|
|
132
|
+
# @return [void]
|
|
133
|
+
def read_compressed_table_data(io)
|
|
134
|
+
@compressed_table_data = {}
|
|
135
|
+
table_entries.each do |entry|
|
|
136
|
+
io.seek(entry.offset)
|
|
137
|
+
# Force UTF-8 encoding on tag for hash key consistency
|
|
138
|
+
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
139
|
+
@compressed_table_data[tag_key] = io.read(entry.comp_length)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if font is TrueType flavored
|
|
144
|
+
#
|
|
145
|
+
# @return [Boolean] true if TrueType, false if CFF
|
|
146
|
+
def truetype?
|
|
147
|
+
[Constants::SFNT_VERSION_TRUETYPE, 0x00010000].include?(header.flavor)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check if font is CFF flavored (OpenType with CFF outlines)
|
|
151
|
+
#
|
|
152
|
+
# @return [Boolean] true if CFF, false if TrueType
|
|
153
|
+
def cff?
|
|
154
|
+
[Constants::SFNT_VERSION_OTTO, 0x4F54544F].include?(header.flavor) # 'OTTO'
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get decompressed table data
|
|
158
|
+
#
|
|
159
|
+
# Decompresses table data on first access and caches result
|
|
160
|
+
#
|
|
161
|
+
# @param tag [String] The table tag
|
|
162
|
+
# @return [String, nil] Decompressed table data or nil if not found
|
|
163
|
+
def table_data(tag)
|
|
164
|
+
return @decompressed_tables[tag] if @decompressed_tables.key?(tag)
|
|
165
|
+
|
|
166
|
+
compressed_data = @compressed_table_data[tag]
|
|
167
|
+
return nil unless compressed_data
|
|
168
|
+
|
|
169
|
+
entry = find_table_entry(tag)
|
|
170
|
+
return nil unless entry
|
|
171
|
+
|
|
172
|
+
# Decompress if compressed (comp_length != orig_length)
|
|
173
|
+
@decompressed_tables[tag] = if entry.comp_length == entry.orig_length
|
|
174
|
+
# Table is not compressed
|
|
175
|
+
compressed_data
|
|
176
|
+
else
|
|
177
|
+
# Decompress using zlib
|
|
178
|
+
Zlib::Inflate.inflate(compressed_data)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Verify decompressed size matches expected
|
|
182
|
+
if @decompressed_tables[tag].bytesize != entry.orig_length
|
|
183
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
184
|
+
"Decompressed table '#{tag}' size mismatch: " \
|
|
185
|
+
"expected #{entry.orig_length}, got #{@decompressed_tables[tag].bytesize}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
@decompressed_tables[tag]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if font has a specific table
|
|
192
|
+
#
|
|
193
|
+
# @param tag [String] The table tag to check for
|
|
194
|
+
# @return [Boolean] true if table exists, false otherwise
|
|
195
|
+
def has_table?(tag)
|
|
196
|
+
table_entries.any? { |entry| entry.tag == tag }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Find a table entry by tag
|
|
200
|
+
#
|
|
201
|
+
# @param tag [String] The table tag to find
|
|
202
|
+
# @return [WoffTableDirectoryEntry, nil] The table entry or nil
|
|
203
|
+
def find_table_entry(tag)
|
|
204
|
+
table_entries.find { |entry| entry.tag == tag }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get list of all table tags
|
|
208
|
+
#
|
|
209
|
+
# @return [Array<String>] Array of table tag strings
|
|
210
|
+
def table_names
|
|
211
|
+
table_entries.map(&:tag)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get parsed table instance
|
|
215
|
+
#
|
|
216
|
+
# This method decompresses and parses the raw table data into a
|
|
217
|
+
# structured table object and caches the result for subsequent calls.
|
|
218
|
+
#
|
|
219
|
+
# @param tag [String] The table tag to retrieve
|
|
220
|
+
# @return [Tables::*, nil] Parsed table object or nil if not found
|
|
221
|
+
def table(tag)
|
|
222
|
+
@parsed_tables[tag] ||= parse_table(tag)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Get units per em from head table
|
|
226
|
+
#
|
|
227
|
+
# @return [Integer, nil] Units per em value
|
|
228
|
+
def units_per_em
|
|
229
|
+
head = table(Constants::HEAD_TAG)
|
|
230
|
+
head&.units_per_em
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Get WOFF metadata if present
|
|
234
|
+
#
|
|
235
|
+
# WOFF metadata is optional compressed XML describing the font
|
|
236
|
+
#
|
|
237
|
+
# @return [String, nil] Decompressed metadata XML or nil
|
|
238
|
+
def metadata
|
|
239
|
+
return nil if header.meta_length.zero?
|
|
240
|
+
return @metadata if defined?(@metadata)
|
|
241
|
+
|
|
242
|
+
File.open(io_source.path, "rb") do |io|
|
|
243
|
+
io.seek(header.meta_offset)
|
|
244
|
+
compressed_meta = io.read(header.meta_length)
|
|
245
|
+
@metadata = Zlib::Inflate.inflate(compressed_meta)
|
|
246
|
+
|
|
247
|
+
# Verify decompressed size
|
|
248
|
+
if @metadata.bytesize != header.meta_orig_length
|
|
249
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
250
|
+
"Metadata size mismatch: expected #{header.meta_orig_length}, " \
|
|
251
|
+
"got #{@metadata.bytesize}")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
@metadata
|
|
255
|
+
end
|
|
256
|
+
rescue StandardError => e
|
|
257
|
+
warn "Failed to decompress WOFF metadata: #{e.message}"
|
|
258
|
+
@metadata = nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get WOFF private data if present
|
|
262
|
+
#
|
|
263
|
+
# WOFF private data is optional application-specific data
|
|
264
|
+
#
|
|
265
|
+
# @return [String, nil] Private data or nil
|
|
266
|
+
def private_data
|
|
267
|
+
return nil if header.priv_length.zero?
|
|
268
|
+
return @private_data if defined?(@private_data)
|
|
269
|
+
|
|
270
|
+
File.open(io_source.path, "rb") do |io|
|
|
271
|
+
io.seek(header.priv_offset)
|
|
272
|
+
@private_data = io.read(header.priv_length)
|
|
273
|
+
end
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
warn "Failed to read WOFF private data: #{e.message}"
|
|
276
|
+
@private_data = nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Convert WOFF to TTF format
|
|
280
|
+
#
|
|
281
|
+
# Decompresses all tables and reconstructs a standard TTF file
|
|
282
|
+
#
|
|
283
|
+
# @param output_path [String] Path where TTF file will be written
|
|
284
|
+
# @return [Integer] Number of bytes written
|
|
285
|
+
# @raise [InvalidFontError] if font is not TrueType flavored
|
|
286
|
+
def to_ttf(output_path)
|
|
287
|
+
unless truetype?
|
|
288
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
289
|
+
"Cannot convert to TTF: font is CFF flavored (use to_otf)")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
build_sfnt_font(output_path, Constants::SFNT_VERSION_TRUETYPE)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Convert WOFF to OTF format
|
|
296
|
+
#
|
|
297
|
+
# Decompresses all tables and reconstructs a standard OTF file
|
|
298
|
+
#
|
|
299
|
+
# @param output_path [String] Path where OTF file will be written
|
|
300
|
+
# @return [Integer] Number of bytes written
|
|
301
|
+
# @raise [InvalidFontError] if font is not CFF flavored
|
|
302
|
+
def to_otf(output_path)
|
|
303
|
+
unless cff?
|
|
304
|
+
Kernel.raise(::Fontisan::InvalidFontError,
|
|
305
|
+
"Cannot convert to OTF: font is TrueType flavored (use to_ttf)")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
build_sfnt_font(output_path, Constants::SFNT_VERSION_OTTO)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Validate format correctness
|
|
312
|
+
#
|
|
313
|
+
# @return [Boolean] true if the WOFF format is valid, false otherwise
|
|
314
|
+
def valid?
|
|
315
|
+
return false unless header
|
|
316
|
+
return false unless header.signature == WOFF_SIGNATURE
|
|
317
|
+
return false unless table_entries.respond_to?(:length)
|
|
318
|
+
return false if table_entries.length != header.num_tables
|
|
319
|
+
return false unless has_table?(Constants::HEAD_TAG)
|
|
320
|
+
|
|
321
|
+
true
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
private
|
|
325
|
+
|
|
326
|
+
# Parse a table from decompressed data
|
|
327
|
+
#
|
|
328
|
+
# @param tag [String] The table tag to parse
|
|
329
|
+
# @return [Tables::*, nil] Parsed table object or nil
|
|
330
|
+
def parse_table(tag)
|
|
331
|
+
raw_data = table_data(tag)
|
|
332
|
+
return nil unless raw_data
|
|
333
|
+
|
|
334
|
+
table_class = table_class_for(tag)
|
|
335
|
+
return nil unless table_class
|
|
336
|
+
|
|
337
|
+
table_class.read(raw_data)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Map table tag to parser class
|
|
341
|
+
#
|
|
342
|
+
# @param tag [String] The table tag
|
|
343
|
+
# @return [Class, nil] Table parser class or nil
|
|
344
|
+
def table_class_for(tag)
|
|
345
|
+
{
|
|
346
|
+
Constants::HEAD_TAG => Tables::Head,
|
|
347
|
+
Constants::HHEA_TAG => Tables::Hhea,
|
|
348
|
+
Constants::HMTX_TAG => Tables::Hmtx,
|
|
349
|
+
Constants::MAXP_TAG => Tables::Maxp,
|
|
350
|
+
Constants::NAME_TAG => Tables::Name,
|
|
351
|
+
Constants::OS2_TAG => Tables::Os2,
|
|
352
|
+
Constants::POST_TAG => Tables::Post,
|
|
353
|
+
Constants::CMAP_TAG => Tables::Cmap,
|
|
354
|
+
Constants::FVAR_TAG => Tables::Fvar,
|
|
355
|
+
Constants::GSUB_TAG => Tables::Gsub,
|
|
356
|
+
Constants::GPOS_TAG => Tables::Gpos,
|
|
357
|
+
}[tag]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Build an SFNT font file (TTF or OTF) from decompressed WOFF data
|
|
361
|
+
#
|
|
362
|
+
# @param output_path [String] Path where font will be written
|
|
363
|
+
# @param sfnt_version [Integer] SFNT version (0x00010000 for TTF, 0x4F54544F for OTF)
|
|
364
|
+
# @return [Integer] Number of bytes written
|
|
365
|
+
def build_sfnt_font(output_path, sfnt_version)
|
|
366
|
+
File.open(output_path, "wb") do |io|
|
|
367
|
+
# Decompress all tables
|
|
368
|
+
decompressed_tables = {}
|
|
369
|
+
table_entries.each do |entry|
|
|
370
|
+
tag = entry.tag.dup.force_encoding("UTF-8")
|
|
371
|
+
decompressed_tables[tag] = table_data(tag)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Calculate offset table fields
|
|
375
|
+
num_tables = table_entries.length
|
|
376
|
+
search_range, entry_selector, range_shift = calculate_offset_table_fields(num_tables)
|
|
377
|
+
|
|
378
|
+
# Write offset table
|
|
379
|
+
io.write([sfnt_version].pack("N"))
|
|
380
|
+
io.write([num_tables].pack("n"))
|
|
381
|
+
io.write([search_range].pack("n"))
|
|
382
|
+
io.write([entry_selector].pack("n"))
|
|
383
|
+
io.write([range_shift].pack("n"))
|
|
384
|
+
|
|
385
|
+
# Calculate table offsets
|
|
386
|
+
offset = 12 + (num_tables * 16) # Header + directory
|
|
387
|
+
table_records = []
|
|
388
|
+
|
|
389
|
+
table_entries.each do |entry|
|
|
390
|
+
tag = entry.tag.dup.force_encoding("UTF-8")
|
|
391
|
+
data = decompressed_tables[tag]
|
|
392
|
+
length = data.bytesize
|
|
393
|
+
|
|
394
|
+
# Calculate checksum
|
|
395
|
+
checksum = Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
396
|
+
|
|
397
|
+
table_records << {
|
|
398
|
+
tag: entry.tag,
|
|
399
|
+
checksum: checksum,
|
|
400
|
+
offset: offset,
|
|
401
|
+
length: length,
|
|
402
|
+
data: data,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Update offset for next table (with padding)
|
|
406
|
+
offset += length
|
|
407
|
+
padding = (Constants::TABLE_ALIGNMENT - (length % Constants::TABLE_ALIGNMENT)) %
|
|
408
|
+
Constants::TABLE_ALIGNMENT
|
|
409
|
+
offset += padding
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Write table directory
|
|
413
|
+
table_records.each do |record|
|
|
414
|
+
io.write(record[:tag])
|
|
415
|
+
io.write([record[:checksum]].pack("N"))
|
|
416
|
+
io.write([record[:offset]].pack("N"))
|
|
417
|
+
io.write([record[:length]].pack("N"))
|
|
418
|
+
|
|
419
|
+
# Write table data
|
|
420
|
+
io.write(record[:data])
|
|
421
|
+
|
|
422
|
+
# Add padding
|
|
423
|
+
padding = (Constants::TABLE_ALIGNMENT - (record[:length] % Constants::TABLE_ALIGNMENT)) %
|
|
424
|
+
Constants::TABLE_ALIGNMENT
|
|
425
|
+
io.write("\x00" * padding) if padding.positive?
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
io.pos
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Update checksum adjustment in head table
|
|
432
|
+
update_checksum_adjustment_in_file(output_path)
|
|
433
|
+
|
|
434
|
+
File.size(output_path)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Calculate offset table fields
|
|
438
|
+
#
|
|
439
|
+
# @param num_tables [Integer] Number of tables
|
|
440
|
+
# @return [Array<Integer>] [searchRange, entrySelector, rangeShift]
|
|
441
|
+
def calculate_offset_table_fields(num_tables)
|
|
442
|
+
entry_selector = (Math.log(num_tables) / Math.log(2)).floor
|
|
443
|
+
search_range = (2**entry_selector) * 16
|
|
444
|
+
range_shift = num_tables * 16 - search_range
|
|
445
|
+
[search_range, entry_selector, range_shift]
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Update checksumAdjustment field in head table
|
|
449
|
+
#
|
|
450
|
+
# @param path [String] Path to the font file
|
|
451
|
+
# @return [void]
|
|
452
|
+
def update_checksum_adjustment_in_file(path)
|
|
453
|
+
# Calculate file checksum
|
|
454
|
+
checksum = Utilities::ChecksumCalculator.calculate_file_checksum(path)
|
|
455
|
+
|
|
456
|
+
# Calculate adjustment
|
|
457
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
458
|
+
|
|
459
|
+
# Find head table position in output file
|
|
460
|
+
File.open(path, "rb") do |io|
|
|
461
|
+
io.seek(4) # Skip sfnt_version
|
|
462
|
+
num_tables = io.read(2).unpack1("n")
|
|
463
|
+
io.seek(12) # Start of table directory
|
|
464
|
+
|
|
465
|
+
num_tables.times do
|
|
466
|
+
tag = io.read(4)
|
|
467
|
+
io.read(4) # checksum
|
|
468
|
+
offset = io.read(4).unpack1("N")
|
|
469
|
+
io.read(4) # length
|
|
470
|
+
|
|
471
|
+
if tag == Constants::HEAD_TAG
|
|
472
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
473
|
+
File.open(path, "r+b") do |write_io|
|
|
474
|
+
write_io.seek(offset + 8)
|
|
475
|
+
write_io.write([adjustment].pack("N"))
|
|
476
|
+
end
|
|
477
|
+
break
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
data/lib/fontisan.rb
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# _____
|
|
4
|
+
# _____
|
|
5
|
+
# _____
|
|
6
|
+
# | | <-----------------------| | font body
|
|
7
|
+
# | |(font) | |
|
|
8
|
+
# | |header | |
|
|
9
|
+
# | |
|
|
10
|
+
# \ \ \ \--> body data
|
|
11
|
+
# | | | |
|
|
12
|
+
# |_| |_|
|
|
13
|
+
# ^
|
|
14
|
+
# |
|
|
15
|
+
# ... meta data opposite the table headers
|
|
16
|
+
# data (instance variable bytes)
|
|
17
|
+
# ...
|
|
18
|
+
# Coupling rules:
|
|
19
|
+
# - a trueType font is composed of only one header and all 15
|
|
20
|
+
# - a head table has one of only two appearance... base and non zero top bit (mac format.k)
|
|
21
|
+
# - The most critical tables are
|
|
22
|
+
# - head # header table
|
|
23
|
+
# - hmtx # metrics array
|
|
24
|
+
# - post # glyph names
|
|
25
|
+
# - cmap # unicode mappings
|
|
26
|
+
# - /LOCA # glyph offsets
|
|
27
|
+
# - glyf # glyph outlines
|
|
28
|
+
# Without these you wouldn't be able to decode the font.
|
|
29
|
+
# - the only two required tables are head and cmap
|
|
30
|
+
|
|
3
31
|
require "logger"
|
|
32
|
+
require "bindata"
|
|
33
|
+
require "zlib"
|
|
34
|
+
require "stringio"
|
|
4
35
|
require "lutaml/model"
|
|
36
|
+
require "lutaml/model/xml_adapter/nokogiri_adapter"
|
|
37
|
+
|
|
38
|
+
# Configure lutaml-model to use Nokogiri adapter for XML serialization
|
|
39
|
+
Lutaml::Model::Config.xml_adapter = Lutaml::Model::Xml::NokogiriAdapter
|
|
5
40
|
|
|
6
41
|
# Core
|
|
7
42
|
require_relative "fontisan/version"
|
|
@@ -14,11 +49,23 @@ require_relative "fontisan/binary/base_record"
|
|
|
14
49
|
|
|
15
50
|
# Table parsers
|
|
16
51
|
require_relative "fontisan/tables/head"
|
|
52
|
+
require_relative "fontisan/tables/hhea"
|
|
53
|
+
require_relative "fontisan/tables/hmtx"
|
|
54
|
+
require_relative "fontisan/tables/maxp"
|
|
55
|
+
require_relative "fontisan/tables/loca"
|
|
56
|
+
require_relative "fontisan/tables/glyf"
|
|
17
57
|
require_relative "fontisan/tables/name"
|
|
18
58
|
require_relative "fontisan/tables/os2"
|
|
19
59
|
require_relative "fontisan/tables/post"
|
|
20
60
|
require_relative "fontisan/tables/cmap"
|
|
21
61
|
require_relative "fontisan/tables/fvar"
|
|
62
|
+
require_relative "fontisan/tables/variation_common"
|
|
63
|
+
require_relative "fontisan/tables/hvar"
|
|
64
|
+
require_relative "fontisan/tables/vvar"
|
|
65
|
+
require_relative "fontisan/tables/mvar"
|
|
66
|
+
require_relative "fontisan/tables/gvar"
|
|
67
|
+
require_relative "fontisan/tables/cvar"
|
|
68
|
+
require_relative "fontisan/tables/cff"
|
|
22
69
|
require_relative "fontisan/tables/layout_common"
|
|
23
70
|
require_relative "fontisan/tables/gsub"
|
|
24
71
|
require_relative "fontisan/tables/gpos"
|
|
@@ -28,27 +75,95 @@ require_relative "fontisan/true_type_font"
|
|
|
28
75
|
require_relative "fontisan/open_type_font"
|
|
29
76
|
require_relative "fontisan/true_type_collection"
|
|
30
77
|
require_relative "fontisan/open_type_collection"
|
|
78
|
+
require_relative "fontisan/woff_font"
|
|
79
|
+
require_relative "fontisan/woff2_font"
|
|
31
80
|
|
|
32
81
|
# Font loading
|
|
33
82
|
require_relative "fontisan/font_loader"
|
|
34
83
|
|
|
35
84
|
# Utilities
|
|
85
|
+
require_relative "fontisan/metrics_calculator"
|
|
86
|
+
require_relative "fontisan/glyph_accessor"
|
|
87
|
+
require_relative "fontisan/outline_extractor"
|
|
36
88
|
require_relative "fontisan/utilities/checksum_calculator"
|
|
89
|
+
require_relative "fontisan/font_writer"
|
|
37
90
|
|
|
38
91
|
# Information models (Lutaml::Model)
|
|
39
92
|
require_relative "fontisan/models/font_info"
|
|
40
93
|
require_relative "fontisan/models/table_info"
|
|
41
94
|
require_relative "fontisan/models/glyph_info"
|
|
95
|
+
require_relative "fontisan/models/glyph_outline"
|
|
42
96
|
require_relative "fontisan/models/unicode_mappings"
|
|
43
97
|
require_relative "fontisan/models/variable_font_info"
|
|
44
98
|
require_relative "fontisan/models/optical_size_info"
|
|
45
99
|
require_relative "fontisan/models/scripts_info"
|
|
46
100
|
require_relative "fontisan/models/features_info"
|
|
47
101
|
require_relative "fontisan/models/all_scripts_features_info"
|
|
102
|
+
require_relative "fontisan/models/validation_report"
|
|
103
|
+
require_relative "fontisan/models/font_export"
|
|
104
|
+
require_relative "fontisan/models/collection_font_summary"
|
|
105
|
+
require_relative "fontisan/models/collection_info"
|
|
106
|
+
require_relative "fontisan/models/collection_list_info"
|
|
107
|
+
require_relative "fontisan/models/font_summary"
|
|
108
|
+
require_relative "fontisan/models/table_sharing_info"
|
|
109
|
+
|
|
110
|
+
# Export infrastructure
|
|
111
|
+
require_relative "fontisan/export/table_serializer"
|
|
112
|
+
require_relative "fontisan/export/ttx_generator"
|
|
113
|
+
require_relative "fontisan/export/ttx_parser"
|
|
114
|
+
require_relative "fontisan/export/exporter"
|
|
115
|
+
|
|
116
|
+
# Validation infrastructure
|
|
117
|
+
require_relative "fontisan/validation/table_validator"
|
|
118
|
+
require_relative "fontisan/validation/structure_validator"
|
|
119
|
+
require_relative "fontisan/validation/consistency_validator"
|
|
120
|
+
require_relative "fontisan/validation/checksum_validator"
|
|
121
|
+
require_relative "fontisan/validation/validator"
|
|
122
|
+
|
|
123
|
+
# Subsetting infrastructure
|
|
124
|
+
require_relative "fontisan/subset/options"
|
|
125
|
+
require_relative "fontisan/subset/profile"
|
|
126
|
+
require_relative "fontisan/subset/glyph_mapping"
|
|
127
|
+
require_relative "fontisan/subset/table_subsetter"
|
|
128
|
+
require_relative "fontisan/subset/builder"
|
|
129
|
+
|
|
130
|
+
# Collection infrastructure
|
|
131
|
+
require_relative "fontisan/collection/table_analyzer"
|
|
132
|
+
require_relative "fontisan/collection/table_deduplicator"
|
|
133
|
+
require_relative "fontisan/collection/offset_calculator"
|
|
134
|
+
require_relative "fontisan/collection/writer"
|
|
135
|
+
require_relative "fontisan/collection/builder"
|
|
136
|
+
|
|
137
|
+
# Format conversion infrastructure
|
|
138
|
+
require_relative "fontisan/converters/conversion_strategy"
|
|
139
|
+
require_relative "fontisan/converters/table_copier"
|
|
140
|
+
require_relative "fontisan/converters/outline_converter"
|
|
141
|
+
require_relative "fontisan/converters/format_converter"
|
|
142
|
+
|
|
143
|
+
# Variation infrastructure
|
|
144
|
+
require_relative "fontisan/variation/interpolator"
|
|
145
|
+
require_relative "fontisan/variation/region_matcher"
|
|
146
|
+
require_relative "fontisan/variation/data_extractor"
|
|
147
|
+
require_relative "fontisan/variation/instance_generator"
|
|
148
|
+
require_relative "fontisan/variation/interpolator"
|
|
149
|
+
require_relative "fontisan/variation/region_matcher"
|
|
150
|
+
require_relative "fontisan/variation/metrics_adjuster"
|
|
151
|
+
require_relative "fontisan/variation/converter"
|
|
152
|
+
require_relative "fontisan/variation/delta_parser"
|
|
153
|
+
require_relative "fontisan/variation/delta_applier"
|
|
154
|
+
require_relative "fontisan/variation/blend_applier"
|
|
155
|
+
|
|
156
|
+
# Optimization infrastructure
|
|
157
|
+
require_relative "fontisan/optimizers/pattern_analyzer"
|
|
158
|
+
require_relative "fontisan/optimizers/subroutine_builder"
|
|
159
|
+
require_relative "fontisan/optimizers/charstring_rewriter"
|
|
160
|
+
require_relative "fontisan/optimizers/subroutine_optimizer"
|
|
161
|
+
require_relative "fontisan/optimizers/subroutine_generator"
|
|
48
162
|
|
|
49
163
|
# Commands
|
|
50
164
|
require_relative "fontisan/commands/base_command"
|
|
51
165
|
require_relative "fontisan/commands/info_command"
|
|
166
|
+
require_relative "fontisan/commands/ls_command"
|
|
52
167
|
require_relative "fontisan/commands/tables_command"
|
|
53
168
|
require_relative "fontisan/commands/glyphs_command"
|
|
54
169
|
require_relative "fontisan/commands/unicode_command"
|
|
@@ -57,6 +172,11 @@ require_relative "fontisan/commands/optical_size_command"
|
|
|
57
172
|
require_relative "fontisan/commands/scripts_command"
|
|
58
173
|
require_relative "fontisan/commands/features_command"
|
|
59
174
|
require_relative "fontisan/commands/dump_table_command"
|
|
175
|
+
require_relative "fontisan/commands/subset_command"
|
|
176
|
+
require_relative "fontisan/commands/convert_command"
|
|
177
|
+
require_relative "fontisan/commands/pack_command"
|
|
178
|
+
require_relative "fontisan/commands/unpack_command"
|
|
179
|
+
require_relative "fontisan/commands/validate_command"
|
|
60
180
|
|
|
61
181
|
# Formatters
|
|
62
182
|
require_relative "fontisan/formatters/text_formatter"
|