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,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "conversion_strategy"
|
|
4
|
+
require_relative "../woff2/header"
|
|
5
|
+
require_relative "../woff2/directory"
|
|
6
|
+
require_relative "../woff2/table_transformer"
|
|
7
|
+
require_relative "../utilities/brotli_wrapper"
|
|
8
|
+
require_relative "../utilities/checksum_calculator"
|
|
9
|
+
require "yaml"
|
|
10
|
+
|
|
11
|
+
module Fontisan
|
|
12
|
+
module Converters
|
|
13
|
+
# WOFF2 encoder conversion strategy
|
|
14
|
+
#
|
|
15
|
+
# [`Woff2Encoder`](lib/fontisan/converters/woff2_encoder.rb) implements
|
|
16
|
+
# the ConversionStrategy interface to convert TTF or OTF fonts to WOFF2
|
|
17
|
+
# format with Brotli compression.
|
|
18
|
+
#
|
|
19
|
+
# WOFF2 encoding process:
|
|
20
|
+
# 1. Load configuration settings
|
|
21
|
+
# 2. Determine font flavor (TTF or CFF)
|
|
22
|
+
# 3. Collect and order tables
|
|
23
|
+
# 4. Transform tables (placeholder for glyf/loca/hmtx optimization)
|
|
24
|
+
# 5. Compress all tables with single Brotli stream
|
|
25
|
+
# 6. Build WOFF2 header and table directory
|
|
26
|
+
# 7. Assemble complete WOFF2 binary
|
|
27
|
+
#
|
|
28
|
+
# For Phase 2 Milestone 2.1:
|
|
29
|
+
# - Basic WOFF2 structure generation
|
|
30
|
+
# - Brotli compression of table data
|
|
31
|
+
# - Valid WOFF2 files for web font delivery
|
|
32
|
+
# - Table transformations are architectural placeholders
|
|
33
|
+
#
|
|
34
|
+
# @example Convert TTF to WOFF2
|
|
35
|
+
# encoder = Woff2Encoder.new
|
|
36
|
+
# woff2_binary = encoder.convert(font)
|
|
37
|
+
# File.binwrite('font.woff2', woff2_binary)
|
|
38
|
+
class Woff2Encoder
|
|
39
|
+
include ConversionStrategy
|
|
40
|
+
|
|
41
|
+
# @return [Hash] Configuration settings
|
|
42
|
+
attr_reader :config
|
|
43
|
+
|
|
44
|
+
# Initialize encoder with configuration
|
|
45
|
+
#
|
|
46
|
+
# @param config_path [String, nil] Path to config file
|
|
47
|
+
def initialize(config_path: nil)
|
|
48
|
+
@config = load_configuration(config_path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convert font to WOFF2 format
|
|
52
|
+
#
|
|
53
|
+
# Returns a hash with :woff2_binary key containing complete WOFF2 file.
|
|
54
|
+
# This is different from other converters that return table data.
|
|
55
|
+
#
|
|
56
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
57
|
+
# @param options [Hash] Conversion options
|
|
58
|
+
# @option options [Integer] :quality Brotli quality (0-11)
|
|
59
|
+
# @option options [Boolean] :transform_tables Apply table transformations
|
|
60
|
+
# @return [Hash] Hash with :woff2_binary key containing WOFF2 binary
|
|
61
|
+
# @raise [Error] If encoding fails
|
|
62
|
+
def convert(font, options = {})
|
|
63
|
+
validate(font, :woff2)
|
|
64
|
+
|
|
65
|
+
# Get Brotli quality from options or config
|
|
66
|
+
quality = options[:quality] || config["brotli"]["quality"]
|
|
67
|
+
|
|
68
|
+
# Detect font flavor
|
|
69
|
+
flavor = detect_flavor(font)
|
|
70
|
+
|
|
71
|
+
# Collect all tables
|
|
72
|
+
table_data = collect_tables(font, options)
|
|
73
|
+
|
|
74
|
+
# Transform tables (if enabled)
|
|
75
|
+
transformer = Woff2::TableTransformer.new(font)
|
|
76
|
+
transform_enabled = options.fetch(:transform_tables, false)
|
|
77
|
+
|
|
78
|
+
# Build table directory entries
|
|
79
|
+
entries = build_table_entries(table_data, transformer,
|
|
80
|
+
transform_enabled)
|
|
81
|
+
|
|
82
|
+
# Compress all table data into single stream
|
|
83
|
+
compressed_data = compress_tables(entries, table_data, quality)
|
|
84
|
+
|
|
85
|
+
# Calculate sizes
|
|
86
|
+
total_sfnt_size = calculate_sfnt_size(table_data)
|
|
87
|
+
total_compressed_size = compressed_data.bytesize
|
|
88
|
+
|
|
89
|
+
# Build WOFF2 header
|
|
90
|
+
header = build_header(
|
|
91
|
+
flavor: flavor,
|
|
92
|
+
num_tables: entries.size,
|
|
93
|
+
total_sfnt_size: total_sfnt_size,
|
|
94
|
+
total_compressed_size: total_compressed_size,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Assemble WOFF2 binary
|
|
98
|
+
woff2_binary = assemble_woff2(header, entries, compressed_data)
|
|
99
|
+
|
|
100
|
+
# Return in special format for ConvertCommand to handle
|
|
101
|
+
{ woff2_binary: woff2_binary }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get list of supported conversions
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Array<Symbol>>] Supported conversion pairs
|
|
107
|
+
def supported_conversions
|
|
108
|
+
[
|
|
109
|
+
%i[ttf woff2],
|
|
110
|
+
%i[otf woff2],
|
|
111
|
+
]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate that conversion is possible
|
|
115
|
+
#
|
|
116
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
117
|
+
# @param target_format [Symbol] Target format
|
|
118
|
+
# @return [Boolean] True if valid
|
|
119
|
+
# @raise [Error] If conversion is not possible
|
|
120
|
+
def validate(font, target_format)
|
|
121
|
+
unless target_format == :woff2
|
|
122
|
+
raise Fontisan::Error,
|
|
123
|
+
"Woff2Encoder only supports conversion to woff2, " \
|
|
124
|
+
"got: #{target_format}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Verify font has required tables
|
|
128
|
+
required_tables = %w[head hhea maxp]
|
|
129
|
+
required_tables.each do |tag|
|
|
130
|
+
unless font.table(tag)
|
|
131
|
+
raise Fontisan::Error,
|
|
132
|
+
"Font is missing required table: #{tag}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Verify font has either glyf or CFF table
|
|
137
|
+
unless font.has_table?("glyf") || font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
138
|
+
raise Fontisan::Error,
|
|
139
|
+
"Font must have either glyf or CFF/CFF2 table"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Load configuration from YAML file
|
|
148
|
+
#
|
|
149
|
+
# @param path [String, nil] Path to config file
|
|
150
|
+
# @return [Hash] Configuration settings
|
|
151
|
+
def load_configuration(path)
|
|
152
|
+
config_path = path || default_config_path
|
|
153
|
+
|
|
154
|
+
if File.exist?(config_path)
|
|
155
|
+
YAML.load_file(config_path)
|
|
156
|
+
else
|
|
157
|
+
default_configuration
|
|
158
|
+
end
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
warn "Failed to load WOFF2 configuration: #{e.message}"
|
|
161
|
+
default_configuration
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get default configuration path
|
|
165
|
+
#
|
|
166
|
+
# @return [String] Path to config file
|
|
167
|
+
def default_config_path
|
|
168
|
+
File.join(
|
|
169
|
+
__dir__,
|
|
170
|
+
"..",
|
|
171
|
+
"config",
|
|
172
|
+
"woff2_settings.yml",
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Get default configuration
|
|
177
|
+
#
|
|
178
|
+
# @return [Hash] Default settings
|
|
179
|
+
def default_configuration
|
|
180
|
+
{
|
|
181
|
+
"brotli" => {
|
|
182
|
+
"quality" => 11,
|
|
183
|
+
"mode" => "font",
|
|
184
|
+
},
|
|
185
|
+
"transformations" => {
|
|
186
|
+
"enabled" => false, # Disabled for Milestone 2.1
|
|
187
|
+
"glyf_loca" => false,
|
|
188
|
+
"hmtx" => false,
|
|
189
|
+
},
|
|
190
|
+
"metadata" => {
|
|
191
|
+
"include" => false,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Detect font flavor (TTF or CFF)
|
|
197
|
+
#
|
|
198
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to detect
|
|
199
|
+
# @return [Integer] Flavor value
|
|
200
|
+
def detect_flavor(font)
|
|
201
|
+
if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
202
|
+
0x4F54544F # 'OTTO' for CFF
|
|
203
|
+
elsif font.has_table?("glyf")
|
|
204
|
+
0x00010000 # TrueType
|
|
205
|
+
else
|
|
206
|
+
raise Fontisan::Error,
|
|
207
|
+
"Cannot determine font flavor: missing glyf and CFF tables"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Collect all tables from font
|
|
212
|
+
#
|
|
213
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
214
|
+
# @param options [Hash] Conversion options
|
|
215
|
+
# @return [Hash<String, String>] Map of tag to table data
|
|
216
|
+
def collect_tables(font, _options = {})
|
|
217
|
+
tables = {}
|
|
218
|
+
|
|
219
|
+
# Get all table names from font
|
|
220
|
+
table_names = if font.respond_to?(:table_names)
|
|
221
|
+
font.table_names
|
|
222
|
+
else
|
|
223
|
+
# Fallback: try common tables
|
|
224
|
+
%w[head hhea maxp OS/2 name cmap post hmtx glyf loca
|
|
225
|
+
CFF]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
table_names.each do |tag|
|
|
229
|
+
data = get_table_data(font, tag)
|
|
230
|
+
tables[tag] = data if data && !data.empty?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
tables
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Get table data from font
|
|
237
|
+
#
|
|
238
|
+
# @param font [Object] Font object
|
|
239
|
+
# @param tag [String] Table tag
|
|
240
|
+
# @return [String, nil] Table data
|
|
241
|
+
def get_table_data(font, tag)
|
|
242
|
+
if font.respond_to?(:table_data)
|
|
243
|
+
font.table_data[tag]
|
|
244
|
+
elsif font.respond_to?(:table)
|
|
245
|
+
table = font.table(tag)
|
|
246
|
+
table&.to_binary_s if table.respond_to?(:to_binary_s)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Build table directory entries
|
|
251
|
+
#
|
|
252
|
+
# @param table_data [Hash<String, String>] Table data map
|
|
253
|
+
# @param transformer [Woff2::TableTransformer] Table transformer
|
|
254
|
+
# @param transform_enabled [Boolean] Enable transformations
|
|
255
|
+
# @return [Array<Woff2::Directory::Entry>] Table entries
|
|
256
|
+
def build_table_entries(table_data, transformer, transform_enabled)
|
|
257
|
+
entries = []
|
|
258
|
+
|
|
259
|
+
# Sort tables by tag for consistent output
|
|
260
|
+
sorted_tags = table_data.keys.sort
|
|
261
|
+
|
|
262
|
+
sorted_tags.each do |tag|
|
|
263
|
+
entry = Woff2::Directory::Entry.new
|
|
264
|
+
entry.tag = tag
|
|
265
|
+
|
|
266
|
+
# Get original table data
|
|
267
|
+
data = table_data[tag]
|
|
268
|
+
entry.orig_length = data.bytesize
|
|
269
|
+
|
|
270
|
+
# Apply transformation if enabled and supported
|
|
271
|
+
if transform_enabled && transformer.transformable?(tag)
|
|
272
|
+
transformed = transformer.transform_table(tag)
|
|
273
|
+
if transformed && transformed.bytesize < data.bytesize
|
|
274
|
+
entry.transform_length = transformed.bytesize
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Calculate flags
|
|
279
|
+
entry.flags = entry.calculate_flags
|
|
280
|
+
|
|
281
|
+
entries << entry
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
entries
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Compress all tables into single Brotli stream
|
|
288
|
+
#
|
|
289
|
+
# @param entries [Array<Woff2::Directory::Entry>] Table entries
|
|
290
|
+
# @param table_data [Hash<String, String>] Original table data
|
|
291
|
+
# @param quality [Integer] Brotli quality
|
|
292
|
+
# @return [String] Compressed data
|
|
293
|
+
def compress_tables(entries, table_data, quality)
|
|
294
|
+
# Concatenate all table data in entry order
|
|
295
|
+
combined_data = String.new(encoding: Encoding::BINARY)
|
|
296
|
+
|
|
297
|
+
entries.each do |entry|
|
|
298
|
+
# Get table data
|
|
299
|
+
data = table_data[entry.tag]
|
|
300
|
+
next unless data
|
|
301
|
+
|
|
302
|
+
# For this milestone, we don't have transformed data yet
|
|
303
|
+
# Use original table data
|
|
304
|
+
combined_data << data
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Compress with Brotli
|
|
308
|
+
Utilities::BrotliWrapper.compress(
|
|
309
|
+
combined_data,
|
|
310
|
+
quality: quality,
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Calculate total SFNT size (uncompressed)
|
|
315
|
+
#
|
|
316
|
+
# @param table_data [Hash<String, String>] Table data map
|
|
317
|
+
# @return [Integer] Total size in bytes
|
|
318
|
+
def calculate_sfnt_size(table_data)
|
|
319
|
+
# Header size (offset table)
|
|
320
|
+
size = 12
|
|
321
|
+
|
|
322
|
+
# Table directory size
|
|
323
|
+
size += table_data.size * 16
|
|
324
|
+
|
|
325
|
+
# Table data size (with padding)
|
|
326
|
+
table_data.each_value do |data|
|
|
327
|
+
size += data.bytesize
|
|
328
|
+
# Add padding to 4-byte boundary
|
|
329
|
+
padding = (4 - (data.bytesize % 4)) % 4
|
|
330
|
+
size += padding
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
size
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Build WOFF2 header
|
|
337
|
+
#
|
|
338
|
+
# @param flavor [Integer] Font flavor
|
|
339
|
+
# @param num_tables [Integer] Number of tables
|
|
340
|
+
# @param total_sfnt_size [Integer] Uncompressed size
|
|
341
|
+
# @param total_compressed_size [Integer] Compressed size
|
|
342
|
+
# @return [Woff2::Woff2Header] WOFF2 header
|
|
343
|
+
def build_header(flavor:, num_tables:, total_sfnt_size:,
|
|
344
|
+
total_compressed_size:)
|
|
345
|
+
header = Woff2::Woff2Header.new
|
|
346
|
+
header.signature = Woff2::Woff2Header::SIGNATURE
|
|
347
|
+
header.flavor = flavor
|
|
348
|
+
header.file_length = 0 # Will be updated later
|
|
349
|
+
header.num_tables = num_tables
|
|
350
|
+
header.reserved = 0
|
|
351
|
+
header.total_sfnt_size = total_sfnt_size
|
|
352
|
+
header.total_compressed_size = total_compressed_size
|
|
353
|
+
header.major_version = 1
|
|
354
|
+
header.minor_version = 0
|
|
355
|
+
header.meta_offset = 0
|
|
356
|
+
header.meta_length = 0
|
|
357
|
+
header.meta_orig_length = 0
|
|
358
|
+
header.priv_offset = 0
|
|
359
|
+
header.priv_length = 0
|
|
360
|
+
|
|
361
|
+
header
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Assemble complete WOFF2 binary
|
|
365
|
+
#
|
|
366
|
+
# @param header [Woff2::Woff2Header] WOFF2 header
|
|
367
|
+
# @param entries [Array<Woff2::Directory::Entry>] Table entries
|
|
368
|
+
# @param compressed_data [String] Compressed table data
|
|
369
|
+
# @return [String] Complete WOFF2 binary
|
|
370
|
+
def assemble_woff2(header, entries, compressed_data)
|
|
371
|
+
woff2_data = String.new(encoding: Encoding::BINARY)
|
|
372
|
+
|
|
373
|
+
# Write header (placeholder, we'll update file_length later)
|
|
374
|
+
header_binary = header.to_binary_s
|
|
375
|
+
woff2_data << header_binary
|
|
376
|
+
|
|
377
|
+
# Write table directory
|
|
378
|
+
entries.each do |entry|
|
|
379
|
+
woff2_data << [entry.flags].pack("C")
|
|
380
|
+
|
|
381
|
+
# Write custom tag if needed
|
|
382
|
+
unless entry.known_tag?
|
|
383
|
+
woff2_data << entry.tag.ljust(4, "\x00")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Write orig_length (UIntBase128)
|
|
387
|
+
woff2_data << Woff2::Directory.encode_uint_base128(entry.orig_length)
|
|
388
|
+
|
|
389
|
+
# Write transform_length if present
|
|
390
|
+
if entry.transformed?
|
|
391
|
+
woff2_data << Woff2::Directory.encode_uint_base128(entry.transform_length)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Write compressed data
|
|
396
|
+
woff2_data << compressed_data
|
|
397
|
+
|
|
398
|
+
# Update header file_length field
|
|
399
|
+
update_woff2_length!(woff2_data)
|
|
400
|
+
|
|
401
|
+
woff2_data
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Update WOFF2 file length in header
|
|
405
|
+
#
|
|
406
|
+
# @param woff2_data [String] WOFF2 binary (modified in place)
|
|
407
|
+
# @return [void]
|
|
408
|
+
def update_woff2_length!(woff2_data)
|
|
409
|
+
total_length = woff2_data.bytesize
|
|
410
|
+
|
|
411
|
+
# file_length field is at offset 8 in header (uint32)
|
|
412
|
+
woff2_data[8, 4] = [total_length].pack("N")
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|