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,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# WOFF2 Table Directory Entry
|
|
6
|
+
#
|
|
7
|
+
# [`Woff2::Directory`](lib/fontisan/woff2/directory.rb) represents
|
|
8
|
+
# a single table entry in the WOFF2 table directory. Unlike WOFF,
|
|
9
|
+
# WOFF2 uses variable-length encoding for sizes and supports table
|
|
10
|
+
# transformations for better compression.
|
|
11
|
+
#
|
|
12
|
+
# Each entry contains:
|
|
13
|
+
# - flags (1 byte): Contains tag index and transformation version
|
|
14
|
+
# - tag (0 or 4 bytes): Table tag (omitted if using known tag index)
|
|
15
|
+
# - origLength (UIntBase128): Original uncompressed table length
|
|
16
|
+
# - transformLength (UIntBase128, optional): Transformed data length
|
|
17
|
+
#
|
|
18
|
+
# Flags byte structure:
|
|
19
|
+
# - Bits 0-5: Table tag index (0-62 = known tags, 63 = custom tag)
|
|
20
|
+
# - Bits 6-7: Transformation version
|
|
21
|
+
#
|
|
22
|
+
# Reference: https://www.w3.org/TR/WOFF2/#table_dir_format
|
|
23
|
+
#
|
|
24
|
+
# @example Create entry for known table
|
|
25
|
+
# entry = Directory::Entry.new
|
|
26
|
+
# entry.tag = "glyf"
|
|
27
|
+
# entry.orig_length = 12000
|
|
28
|
+
# entry.flags = entry.calculate_flags
|
|
29
|
+
#
|
|
30
|
+
# @example Create entry for custom table
|
|
31
|
+
# entry = Directory::Entry.new
|
|
32
|
+
# entry.tag = "CUST"
|
|
33
|
+
# entry.orig_length = 5000
|
|
34
|
+
# entry.flags = 0x3F # Custom tag indicator
|
|
35
|
+
module Directory
|
|
36
|
+
# Known table tags with assigned indices (0-62)
|
|
37
|
+
# Index 63 (0x3F) indicates a custom tag follows
|
|
38
|
+
KNOWN_TAGS = [
|
|
39
|
+
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
|
|
40
|
+
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
|
41
|
+
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
|
|
42
|
+
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
|
43
|
+
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
|
|
44
|
+
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
|
45
|
+
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
|
|
46
|
+
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
# Transformation versions
|
|
50
|
+
TRANSFORM_NONE = 0
|
|
51
|
+
TRANSFORM_GLYF_LOCA = 0 # Applied to both glyf and loca
|
|
52
|
+
TRANSFORM_HMTX = 0 # Applied to hmtx
|
|
53
|
+
|
|
54
|
+
# Custom tag indicator
|
|
55
|
+
CUSTOM_TAG_INDEX = 0x3F
|
|
56
|
+
|
|
57
|
+
# WOFF2 Table Directory Entry
|
|
58
|
+
#
|
|
59
|
+
# Represents a single table in the WOFF2 font with all metadata
|
|
60
|
+
# needed for decompression and reconstruction.
|
|
61
|
+
class Entry
|
|
62
|
+
attr_accessor :tag, :flags, :orig_length, :transform_length, :offset # Calculated during encoding
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
@tag = nil
|
|
66
|
+
@flags = 0
|
|
67
|
+
@orig_length = 0
|
|
68
|
+
@transform_length = nil
|
|
69
|
+
@offset = 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Calculate flags byte for this entry
|
|
73
|
+
#
|
|
74
|
+
# @return [Integer] Flags byte (0-255)
|
|
75
|
+
def calculate_flags
|
|
76
|
+
tag_index = KNOWN_TAGS.index(tag) || CUSTOM_TAG_INDEX
|
|
77
|
+
transform_version = determine_transform_version
|
|
78
|
+
|
|
79
|
+
# Combine tag index (bits 0-5) and transform version (bits 6-7)
|
|
80
|
+
(transform_version << 6) | tag_index
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if table uses a known tag
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if known tag
|
|
86
|
+
def known_tag?
|
|
87
|
+
KNOWN_TAGS.include?(tag)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if table is transformed
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] True if transformed
|
|
93
|
+
def transformed?
|
|
94
|
+
transform_version != TRANSFORM_NONE && transform_length
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get transformation version from flags
|
|
98
|
+
#
|
|
99
|
+
# @return [Integer] Transform version (0-3)
|
|
100
|
+
def transform_version
|
|
101
|
+
(flags >> 6) & 0x03
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get tag index from flags
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer] Tag index (0-63)
|
|
107
|
+
def tag_index
|
|
108
|
+
flags & 0x3F
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Determine if this table should be transformed
|
|
112
|
+
#
|
|
113
|
+
# For Phase 2 Milestone 2.1, we support transformation flags
|
|
114
|
+
# but don't implement the actual transformations yet.
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer] Transform version
|
|
117
|
+
def determine_transform_version
|
|
118
|
+
# For this milestone, we don't apply transformations
|
|
119
|
+
# but we recognize which tables could be transformed
|
|
120
|
+
TRANSFORM_NONE
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check if table can be transformed (glyf, loca, hmtx)
|
|
124
|
+
#
|
|
125
|
+
# @return [Boolean] True if transformable
|
|
126
|
+
def transformable?
|
|
127
|
+
%w[glyf loca hmtx].include?(tag)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Calculate size of this entry when serialized
|
|
131
|
+
#
|
|
132
|
+
# @return [Integer] Size in bytes
|
|
133
|
+
def serialized_size
|
|
134
|
+
size = 1 # flags byte
|
|
135
|
+
size += 4 unless known_tag? # custom tag
|
|
136
|
+
size += uint_base128_size(orig_length)
|
|
137
|
+
size += uint_base128_size(transform_length) if transformed?
|
|
138
|
+
size
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Estimate size of UIntBase128 encoded value
|
|
144
|
+
#
|
|
145
|
+
# @param value [Integer] Value to encode
|
|
146
|
+
# @return [Integer] Size in bytes (1-5)
|
|
147
|
+
def uint_base128_size(value)
|
|
148
|
+
return 1 if value.nil? || value < 128
|
|
149
|
+
|
|
150
|
+
bytes = 0
|
|
151
|
+
v = value
|
|
152
|
+
while v.positive?
|
|
153
|
+
bytes += 1
|
|
154
|
+
v >>= 7
|
|
155
|
+
end
|
|
156
|
+
[bytes, 5].min # Max 5 bytes
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Encode an integer as UIntBase128
|
|
161
|
+
#
|
|
162
|
+
# Variable-length encoding where:
|
|
163
|
+
# - If value < 128, use 1 byte
|
|
164
|
+
# - Otherwise, use high bit to indicate continuation
|
|
165
|
+
#
|
|
166
|
+
# @param value [Integer] Value to encode
|
|
167
|
+
# @return [String] Binary encoded data
|
|
168
|
+
def self.encode_uint_base128(value)
|
|
169
|
+
return [value].pack("C") if value < 128
|
|
170
|
+
|
|
171
|
+
bytes = []
|
|
172
|
+
v = value
|
|
173
|
+
|
|
174
|
+
# Build bytes from least to most significant
|
|
175
|
+
loop do
|
|
176
|
+
bytes.unshift(v & 0x7F)
|
|
177
|
+
v >>= 7
|
|
178
|
+
break if v.zero?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Set high bit on all but last byte
|
|
182
|
+
(0...bytes.length - 1).each do |i|
|
|
183
|
+
bytes[i] |= 0x80
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
bytes.pack("C*")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Decode UIntBase128 from IO
|
|
190
|
+
#
|
|
191
|
+
# @param io [IO] Input stream
|
|
192
|
+
# @return [Integer] Decoded value
|
|
193
|
+
# @raise [Error] If encoding is invalid
|
|
194
|
+
def self.decode_uint_base128(io)
|
|
195
|
+
result = 0
|
|
196
|
+
5.times do
|
|
197
|
+
byte = io.read(1)&.unpack1("C")
|
|
198
|
+
return nil unless byte
|
|
199
|
+
|
|
200
|
+
# Check if high bit is set (continuation)
|
|
201
|
+
if (byte & 0x80).zero?
|
|
202
|
+
return (result << 7) | byte
|
|
203
|
+
else
|
|
204
|
+
result = (result << 7) | (byte & 0x7F)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# If we're here, encoding is invalid (> 5 bytes)
|
|
209
|
+
raise Fontisan::Error, "Invalid UIntBase128 encoding"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Encode 255UInt16 value
|
|
213
|
+
#
|
|
214
|
+
# Used in transformed glyf table:
|
|
215
|
+
# - 0-252: value itself (1 byte)
|
|
216
|
+
# - 253: next byte + 253 (2 bytes)
|
|
217
|
+
# - 254: next 2 bytes as big-endian (3 bytes)
|
|
218
|
+
# - 255: next 2 bytes + 506 (3 bytes)
|
|
219
|
+
#
|
|
220
|
+
# @param value [Integer] Value to encode (0-65535)
|
|
221
|
+
# @return [String] Binary encoded data
|
|
222
|
+
def self.encode_255_uint16(value)
|
|
223
|
+
if value < 253
|
|
224
|
+
[value].pack("C")
|
|
225
|
+
elsif value < 506
|
|
226
|
+
[253, value - 253].pack("C*")
|
|
227
|
+
elsif value < 65536
|
|
228
|
+
[254].pack("C") + [value].pack("n")
|
|
229
|
+
else
|
|
230
|
+
[255].pack("C") + [value - 506].pack("n")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Decode 255UInt16 from IO
|
|
235
|
+
#
|
|
236
|
+
# @param io [IO] Input stream
|
|
237
|
+
# @return [Integer] Decoded value
|
|
238
|
+
def self.decode_255_uint16(io)
|
|
239
|
+
first = io.read(1)&.unpack1("C")
|
|
240
|
+
return nil unless first
|
|
241
|
+
|
|
242
|
+
case first
|
|
243
|
+
when 0..252
|
|
244
|
+
first
|
|
245
|
+
when 253
|
|
246
|
+
second = io.read(1)&.unpack1("C")
|
|
247
|
+
253 + second
|
|
248
|
+
when 254
|
|
249
|
+
io.read(2)&.unpack1("n")
|
|
250
|
+
when 255
|
|
251
|
+
value = io.read(2)&.unpack1("n")
|
|
252
|
+
value + 506
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# WOFF2 Header structure
|
|
8
|
+
#
|
|
9
|
+
# [`Woff2::Header`](lib/fontisan/woff2/header.rb) represents the main
|
|
10
|
+
# header of a WOFF2 file according to W3C WOFF2 specification.
|
|
11
|
+
#
|
|
12
|
+
# The header is more compact than WOFF, using 48 bytes.
|
|
13
|
+
#
|
|
14
|
+
# Structure (all big-endian):
|
|
15
|
+
# - uint32: signature (0x774F4632 'wOF2')
|
|
16
|
+
# - uint32: flavor (0x00010000 for TTF, 0x4F54544F for CFF)
|
|
17
|
+
# - uint32: file_length (total WOFF2 file size)
|
|
18
|
+
# - uint16: numTables (number of font tables)
|
|
19
|
+
# - uint16: reserved (must be 0)
|
|
20
|
+
# - uint32: totalSfntSize (uncompressed font size)
|
|
21
|
+
# - uint32: totalCompressedSize (size of compressed data block)
|
|
22
|
+
# - uint16: majorVersion (major version of WOFF file)
|
|
23
|
+
# - uint16: minorVersion (minor version of WOFF file)
|
|
24
|
+
# - uint32: metaOffset (offset to metadata, 0 if none)
|
|
25
|
+
# - uint32: metaLength (compressed metadata length)
|
|
26
|
+
# - uint32: metaOrigLength (uncompressed metadata length)
|
|
27
|
+
# - uint32: privOffset (offset to private data, 0 if none)
|
|
28
|
+
# - uint32: privLength (length of private data)
|
|
29
|
+
#
|
|
30
|
+
# Reference: https://www.w3.org/TR/WOFF2/#woff20Header
|
|
31
|
+
#
|
|
32
|
+
# @example Create a header
|
|
33
|
+
# header = Woff2::Header.new
|
|
34
|
+
# header.signature = 0x774F4632
|
|
35
|
+
# header.flavor = 0x00010000
|
|
36
|
+
# header.num_tables = 10
|
|
37
|
+
class Woff2Header < BinData::Record
|
|
38
|
+
endian :big
|
|
39
|
+
|
|
40
|
+
uint32 :signature # 'wOF2' magic number
|
|
41
|
+
uint32 :flavor # Font format (TTF or CFF)
|
|
42
|
+
uint32 :file_length # Total WOFF2 file size
|
|
43
|
+
uint16 :num_tables # Number of font tables
|
|
44
|
+
uint16 :reserved # Reserved, must be 0
|
|
45
|
+
uint32 :total_sfnt_size # Uncompressed font size
|
|
46
|
+
uint32 :total_compressed_size # Compressed data block size
|
|
47
|
+
uint16 :major_version # Major version number
|
|
48
|
+
uint16 :minor_version # Minor version number
|
|
49
|
+
uint32 :meta_offset # Metadata block offset (0 if none)
|
|
50
|
+
uint32 :meta_length # Compressed metadata length
|
|
51
|
+
uint32 :meta_orig_length # Uncompressed metadata length
|
|
52
|
+
uint32 :priv_offset # Private data offset (0 if none)
|
|
53
|
+
uint32 :priv_length # Private data length
|
|
54
|
+
|
|
55
|
+
# WOFF2 signature constant
|
|
56
|
+
SIGNATURE = 0x774F4632 # 'wOF2'
|
|
57
|
+
|
|
58
|
+
# Check if signature is valid
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] True if signature is valid
|
|
61
|
+
def valid_signature?
|
|
62
|
+
signature == SIGNATURE
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if font is TrueType flavored
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean] True if TrueType
|
|
68
|
+
def truetype?
|
|
69
|
+
[0x00010000, 0x74727565].include?(flavor) # 'true'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if font is CFF flavored
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] True if CFF/OpenType
|
|
75
|
+
def cff?
|
|
76
|
+
flavor == 0x4F54544F # 'OTTO'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if metadata is present
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] True if metadata exists
|
|
82
|
+
def has_metadata?
|
|
83
|
+
meta_offset.positive? && meta_length.positive?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if private data is present
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] True if private data exists
|
|
89
|
+
def has_private_data?
|
|
90
|
+
priv_offset.positive? && priv_length.positive?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get header size in bytes
|
|
94
|
+
#
|
|
95
|
+
# @return [Integer] Header size (always 48 bytes)
|
|
96
|
+
def self.header_size
|
|
97
|
+
48
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Table transformer for WOFF2 encoding
|
|
6
|
+
#
|
|
7
|
+
# [`Woff2::TableTransformer`](lib/fontisan/woff2/table_transformer.rb)
|
|
8
|
+
# handles table transformations that improve compression in WOFF2.
|
|
9
|
+
# The WOFF2 spec defines transformations for glyf/loca and hmtx tables.
|
|
10
|
+
#
|
|
11
|
+
# For Phase 2 Milestone 2.1:
|
|
12
|
+
# - Architecture is in place for transformations
|
|
13
|
+
# - Actual transformation implementations are marked as TODO
|
|
14
|
+
# - Tables are copied as-is without transformation
|
|
15
|
+
# - This allows valid WOFF2 generation while leaving room for optimization
|
|
16
|
+
#
|
|
17
|
+
# Future milestones will implement:
|
|
18
|
+
# - glyf/loca transformation (combined stream, delta encoding)
|
|
19
|
+
# - hmtx transformation (compact representation)
|
|
20
|
+
#
|
|
21
|
+
# Reference: https://www.w3.org/TR/WOFF2/#table_tranforms
|
|
22
|
+
#
|
|
23
|
+
# @example Transform tables for WOFF2
|
|
24
|
+
# transformer = TableTransformer.new(font)
|
|
25
|
+
# glyf_data = transformer.transform_table("glyf")
|
|
26
|
+
class TableTransformer
|
|
27
|
+
# @return [Object] Font object with table access
|
|
28
|
+
attr_reader :font
|
|
29
|
+
|
|
30
|
+
# Initialize transformer with font
|
|
31
|
+
#
|
|
32
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
33
|
+
def initialize(font)
|
|
34
|
+
@font = font
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Transform a table for WOFF2 encoding
|
|
38
|
+
#
|
|
39
|
+
# For Milestone 2.1, this returns the original table data
|
|
40
|
+
# without transformation. The architecture supports future
|
|
41
|
+
# implementation of actual transformations.
|
|
42
|
+
#
|
|
43
|
+
# @param tag [String] Table tag
|
|
44
|
+
# @return [String, nil] Transformed (or original) table data
|
|
45
|
+
def transform_table(tag)
|
|
46
|
+
case tag
|
|
47
|
+
when "glyf"
|
|
48
|
+
transform_glyf
|
|
49
|
+
when "loca"
|
|
50
|
+
transform_loca
|
|
51
|
+
when "hmtx"
|
|
52
|
+
transform_hmtx
|
|
53
|
+
else
|
|
54
|
+
# No transformation, return original data
|
|
55
|
+
get_table_data(tag)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if a table can be transformed
|
|
60
|
+
#
|
|
61
|
+
# @param tag [String] Table tag
|
|
62
|
+
# @return [Boolean] True if table supports transformation
|
|
63
|
+
def transformable?(tag)
|
|
64
|
+
%w[glyf loca hmtx].include?(tag)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine transformation version for a table
|
|
68
|
+
#
|
|
69
|
+
# For Milestone 2.1, always returns TRANSFORM_NONE since
|
|
70
|
+
# we don't implement transformations yet.
|
|
71
|
+
#
|
|
72
|
+
# @param tag [String] Table tag
|
|
73
|
+
# @return [Integer] Transformation version (0 = none)
|
|
74
|
+
def transformation_version(_tag)
|
|
75
|
+
# For this milestone, no transformations are applied
|
|
76
|
+
Directory::TRANSFORM_NONE
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Transform glyf table
|
|
82
|
+
#
|
|
83
|
+
# The WOFF2 glyf transformation combines glyf and loca into a
|
|
84
|
+
# single stream with delta-encoded coordinates and flags.
|
|
85
|
+
#
|
|
86
|
+
# TODO: Implement full glyf transformation for better compression.
|
|
87
|
+
# For now, returns original table data.
|
|
88
|
+
#
|
|
89
|
+
# @return [String, nil] Transformed glyf data
|
|
90
|
+
def transform_glyf
|
|
91
|
+
# TODO: Implement glyf transformation
|
|
92
|
+
# This would involve:
|
|
93
|
+
# 1. Parse all glyphs from glyf table
|
|
94
|
+
# 2. Combine with loca offsets
|
|
95
|
+
# 3. Create transformed stream with:
|
|
96
|
+
# - nContour values
|
|
97
|
+
# - nPoints values
|
|
98
|
+
# - Flag bytes (with run-length encoding)
|
|
99
|
+
# - x-coordinates (delta-encoded)
|
|
100
|
+
# - y-coordinates (delta-encoded)
|
|
101
|
+
# - Instruction bytes
|
|
102
|
+
# 4. Use 255UInt16 encoding for variable-length integers
|
|
103
|
+
#
|
|
104
|
+
# Reference: https://www.w3.org/TR/WOFF2/#glyf_table_format
|
|
105
|
+
|
|
106
|
+
get_table_data("glyf")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Transform loca table
|
|
110
|
+
#
|
|
111
|
+
# In WOFF2, loca is combined with glyf during transformation.
|
|
112
|
+
# When glyf is transformed, loca table is omitted from output.
|
|
113
|
+
#
|
|
114
|
+
# TODO: Implement loca transformation (combined with glyf).
|
|
115
|
+
# For now, returns original table data.
|
|
116
|
+
#
|
|
117
|
+
# @return [String, nil] Transformed loca data
|
|
118
|
+
def transform_loca
|
|
119
|
+
# TODO: Implement loca transformation
|
|
120
|
+
# When glyf transformation is implemented, loca will be:
|
|
121
|
+
# 1. Combined into the transformed glyf stream
|
|
122
|
+
# 2. Reconstructed during decompression
|
|
123
|
+
# 3. Not present as separate table in WOFF2
|
|
124
|
+
|
|
125
|
+
get_table_data("loca")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Transform hmtx table
|
|
129
|
+
#
|
|
130
|
+
# The WOFF2 hmtx transformation stores advance widths more efficiently
|
|
131
|
+
# by exploiting redundancy (many glyphs have same advance width).
|
|
132
|
+
#
|
|
133
|
+
# TODO: Implement hmtx transformation for better compression.
|
|
134
|
+
# For now, returns original table data.
|
|
135
|
+
#
|
|
136
|
+
# @return [String, nil] Transformed hmtx data
|
|
137
|
+
def transform_hmtx
|
|
138
|
+
# TODO: Implement hmtx transformation
|
|
139
|
+
# This would involve:
|
|
140
|
+
# 1. Parse hmtx table
|
|
141
|
+
# 2. Extract common advance widths
|
|
142
|
+
# 3. Identify proportional vs monospace sections
|
|
143
|
+
# 4. Use flags to indicate structure
|
|
144
|
+
# 5. Store only unique advance widths
|
|
145
|
+
# 6. Store LSB array separately
|
|
146
|
+
#
|
|
147
|
+
# Reference: https://www.w3.org/TR/WOFF2/#hmtx_table_format
|
|
148
|
+
|
|
149
|
+
get_table_data("hmtx")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get raw table data from font
|
|
153
|
+
#
|
|
154
|
+
# @param tag [String] Table tag
|
|
155
|
+
# @return [String, nil] Table data or nil if not found
|
|
156
|
+
def get_table_data(tag)
|
|
157
|
+
return nil unless font.respond_to?(:table_data)
|
|
158
|
+
|
|
159
|
+
font.table_data(tag)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|