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,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "constants"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
# FontWriter handles writing font binaries from table data
|
|
8
|
+
#
|
|
9
|
+
# This class assembles a complete font binary from individual table data,
|
|
10
|
+
# including:
|
|
11
|
+
# - Writing the sfnt header (offset table)
|
|
12
|
+
# - Building the table directory
|
|
13
|
+
# - Writing table data with proper 4-byte alignment
|
|
14
|
+
# - Calculating all checksums
|
|
15
|
+
# - Updating the head table's checksumAdjustment field
|
|
16
|
+
#
|
|
17
|
+
# @example Write font from tables
|
|
18
|
+
# tables = {
|
|
19
|
+
# 'head' => head_data,
|
|
20
|
+
# 'hhea' => hhea_data,
|
|
21
|
+
# 'maxp' => maxp_data,
|
|
22
|
+
# 'hmtx' => hmtx_data,
|
|
23
|
+
# 'cmap' => cmap_data
|
|
24
|
+
# }
|
|
25
|
+
# binary = FontWriter.write_font(tables)
|
|
26
|
+
# File.binwrite('subset.ttf', binary)
|
|
27
|
+
#
|
|
28
|
+
# @example Write to file directly
|
|
29
|
+
# FontWriter.write_to_file(tables, 'subset.ttf')
|
|
30
|
+
#
|
|
31
|
+
# Reference: OpenType spec section on font file structure
|
|
32
|
+
class FontWriter
|
|
33
|
+
# OpenType/TrueType table ordering (recommended order)
|
|
34
|
+
TRUETYPE_TABLE_ORDER = %w[
|
|
35
|
+
head hhea maxp OS/2 hmtx LTSH VDMX hdmx cmap fpgm prep cvt
|
|
36
|
+
loca glyf kern name post gasp PCLT DSIG
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# OpenType/CFF table ordering (recommended order)
|
|
40
|
+
OPENTYPE_TABLE_ORDER = %w[
|
|
41
|
+
head hhea maxp OS/2 name cmap post CFF CFF2
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# Write complete font binary from table data
|
|
45
|
+
#
|
|
46
|
+
# @param tables_hash [Hash<String, String>] Map of table tag to binary data
|
|
47
|
+
# @param sfnt_version [Integer, nil] Font sfnt version (0x00010000 for TrueType,
|
|
48
|
+
# 0x4F54544F for OpenType/CFF). If nil, auto-detects based on tables.
|
|
49
|
+
# @return [String] Complete font binary
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# binary = FontWriter.write_font(tables_hash)
|
|
53
|
+
# binary = FontWriter.write_font(tables_hash, sfnt_version: 0x4F54544F)
|
|
54
|
+
def self.write_font(tables_hash, sfnt_version: nil)
|
|
55
|
+
# Auto-detect sfnt version if not provided
|
|
56
|
+
sfnt_version ||= detect_sfnt_version(tables_hash)
|
|
57
|
+
new(tables_hash, sfnt_version: sfnt_version).write
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Detect sfnt version based on table presence
|
|
61
|
+
#
|
|
62
|
+
# @param tables_hash [Hash<String, String>] Map of table tag to binary data
|
|
63
|
+
# @return [Integer] Detected sfnt version
|
|
64
|
+
def self.detect_sfnt_version(tables_hash)
|
|
65
|
+
if tables_hash.key?("CFF ") || tables_hash.key?("CFF2")
|
|
66
|
+
0x4F54544F # 'OTTO' for OpenType/CFF
|
|
67
|
+
else
|
|
68
|
+
0x00010000 # 1.0 for TrueType
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Write font binary to file
|
|
73
|
+
#
|
|
74
|
+
# @param tables_hash [Hash<String, String>] Map of table tag to binary data
|
|
75
|
+
# @param path [String] Output file path
|
|
76
|
+
# @param sfnt_version [Integer, nil] Font sfnt version. If nil, auto-detects.
|
|
77
|
+
# @return [Integer] Number of bytes written
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# FontWriter.write_to_file(tables_hash, 'output.ttf')
|
|
81
|
+
def self.write_to_file(tables_hash, path, sfnt_version: nil)
|
|
82
|
+
binary = write_font(tables_hash, sfnt_version: sfnt_version)
|
|
83
|
+
|
|
84
|
+
# Create parent directories if they don't exist
|
|
85
|
+
dir = File.dirname(path)
|
|
86
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
87
|
+
|
|
88
|
+
File.binwrite(path, binary)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Initialize writer with table data
|
|
92
|
+
#
|
|
93
|
+
# @param tables_hash [Hash<String, String>] Map of table tag to binary data
|
|
94
|
+
# @param sfnt_version [Integer] Font sfnt version
|
|
95
|
+
def initialize(tables_hash, sfnt_version: 0x00010000)
|
|
96
|
+
@tables = tables_hash
|
|
97
|
+
@sfnt_version = sfnt_version
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Write the complete font binary
|
|
101
|
+
#
|
|
102
|
+
# @return [String] Complete font binary
|
|
103
|
+
def write
|
|
104
|
+
# Order tables according to format
|
|
105
|
+
ordered_tags = order_tables
|
|
106
|
+
|
|
107
|
+
# Calculate table offsets
|
|
108
|
+
table_entries = calculate_table_entries(ordered_tags)
|
|
109
|
+
|
|
110
|
+
# Build font binary
|
|
111
|
+
font_data = String.new(encoding: Encoding::BINARY)
|
|
112
|
+
|
|
113
|
+
# Write offset table (sfnt header)
|
|
114
|
+
font_data << write_offset_table(table_entries.size)
|
|
115
|
+
|
|
116
|
+
# Write table directory (ALL entries first)
|
|
117
|
+
table_entries.each do |entry|
|
|
118
|
+
font_data << write_table_entry(entry)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Write table data (ALL data after directory)
|
|
122
|
+
table_entries.each do |entry|
|
|
123
|
+
font_data << entry[:data]
|
|
124
|
+
font_data << entry[:padding]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Calculate and update head table checksum adjustment
|
|
128
|
+
update_checksum_adjustment!(font_data, table_entries)
|
|
129
|
+
|
|
130
|
+
font_data
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
# Order tables according to recommended order
|
|
136
|
+
#
|
|
137
|
+
# @return [Array<String>] Ordered table tags
|
|
138
|
+
def order_tables
|
|
139
|
+
# Determine if this is OpenType/CFF or TrueType
|
|
140
|
+
is_cff = @tables.key?("CFF ") || @tables.key?("CFF2")
|
|
141
|
+
order = is_cff ? OPENTYPE_TABLE_ORDER : TRUETYPE_TABLE_ORDER
|
|
142
|
+
|
|
143
|
+
# Start with tables in recommended order that exist
|
|
144
|
+
ordered = order.select { |tag| @tables.key?(tag) }
|
|
145
|
+
|
|
146
|
+
# Add any remaining tables not in the recommended order
|
|
147
|
+
remaining = @tables.keys - ordered
|
|
148
|
+
ordered + remaining.sort
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Calculate table directory entries with offsets
|
|
152
|
+
#
|
|
153
|
+
# @param tags [Array<String>] Ordered table tags
|
|
154
|
+
# @return [Array<Hash>] Table entries with offsets, checksums, data
|
|
155
|
+
def calculate_table_entries(tags)
|
|
156
|
+
# Calculate offset for first table
|
|
157
|
+
# Offset table (12 bytes) + table directory (16 bytes per table)
|
|
158
|
+
offset = 12 + (tags.size * 16)
|
|
159
|
+
|
|
160
|
+
entries = []
|
|
161
|
+
|
|
162
|
+
tags.each do |tag|
|
|
163
|
+
data = @tables[tag]
|
|
164
|
+
checksum = calculate_table_checksum(data)
|
|
165
|
+
|
|
166
|
+
# Calculate padding to 4-byte boundary
|
|
167
|
+
padding_length = (4 - (data.bytesize % 4)) % 4
|
|
168
|
+
padding = "\0" * padding_length
|
|
169
|
+
|
|
170
|
+
entries << {
|
|
171
|
+
tag: tag,
|
|
172
|
+
checksum: checksum,
|
|
173
|
+
offset: offset,
|
|
174
|
+
length: data.bytesize,
|
|
175
|
+
data: data,
|
|
176
|
+
padding: padding,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Update offset for next table
|
|
180
|
+
offset += data.bytesize + padding_length
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
entries
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Write offset table (sfnt header)
|
|
187
|
+
#
|
|
188
|
+
# @param num_tables [Integer] Number of tables
|
|
189
|
+
# @return [String] Offset table binary data
|
|
190
|
+
def write_offset_table(num_tables)
|
|
191
|
+
# Calculate search range, entry selector, and range shift
|
|
192
|
+
# searchRange = (maximum power of 2 <= num_tables) * 16
|
|
193
|
+
# entrySelector = log2(maximum power of 2 <= num_tables)
|
|
194
|
+
# rangeShift = num_tables * 16 - searchRange
|
|
195
|
+
|
|
196
|
+
max_power = 0
|
|
197
|
+
n = num_tables
|
|
198
|
+
while n > 1
|
|
199
|
+
n >>= 1
|
|
200
|
+
max_power += 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
search_range = (1 << max_power) * 16
|
|
204
|
+
entry_selector = max_power
|
|
205
|
+
range_shift = (num_tables * 16) - search_range
|
|
206
|
+
|
|
207
|
+
[
|
|
208
|
+
@sfnt_version, # uint32 - sfnt version
|
|
209
|
+
num_tables, # uint16 - number of tables
|
|
210
|
+
search_range, # uint16 - search range
|
|
211
|
+
entry_selector, # uint16 - entry selector
|
|
212
|
+
range_shift, # uint16 - range shift
|
|
213
|
+
].pack("N n n n n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Write a table directory entry
|
|
217
|
+
#
|
|
218
|
+
# @param entry [Hash] Table entry with tag, checksum, offset, length
|
|
219
|
+
# @return [String] Table directory entry binary data
|
|
220
|
+
def write_table_entry(entry)
|
|
221
|
+
[
|
|
222
|
+
entry[:tag], # char[4] - table tag
|
|
223
|
+
entry[:checksum], # uint32 - checksum
|
|
224
|
+
entry[:offset], # uint32 - offset
|
|
225
|
+
entry[:length], # uint32 - length
|
|
226
|
+
].pack("a4 N N N")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Calculate checksum for a table
|
|
230
|
+
#
|
|
231
|
+
# The checksum is calculated by summing all uint32 values in the table.
|
|
232
|
+
# The table is padded with zeros to a multiple of 4 bytes if necessary.
|
|
233
|
+
#
|
|
234
|
+
# @param data [String] Table binary data
|
|
235
|
+
# @return [Integer] Table checksum
|
|
236
|
+
def calculate_table_checksum(data)
|
|
237
|
+
# Pad to 4-byte boundary
|
|
238
|
+
padded_data = data.dup
|
|
239
|
+
padding_length = (4 - (data.bytesize % 4)) % 4
|
|
240
|
+
padded_data << ("\0" * padding_length) if padding_length.positive?
|
|
241
|
+
|
|
242
|
+
# Sum all uint32 values
|
|
243
|
+
sum = 0
|
|
244
|
+
(0...padded_data.bytesize).step(4) do |i|
|
|
245
|
+
value = padded_data[i, 4].unpack1("N")
|
|
246
|
+
sum = (sum + value) & 0xFFFFFFFF
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sum
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Update head table checksum adjustment
|
|
253
|
+
#
|
|
254
|
+
# The checksumAdjustment field in the head table (at offset 8) must be
|
|
255
|
+
# set such that the sum of all uint32 values in the entire font equals
|
|
256
|
+
# the magic number 0xB1B0AFBA.
|
|
257
|
+
#
|
|
258
|
+
# @param font_data [String] Complete font binary (modified in place)
|
|
259
|
+
# @param table_entries [Array<Hash>] Table entries
|
|
260
|
+
# @return [void]
|
|
261
|
+
def update_checksum_adjustment!(font_data, table_entries)
|
|
262
|
+
# Find head table entry
|
|
263
|
+
head_entry = table_entries.find { |e| e[:tag] == "head" }
|
|
264
|
+
return unless head_entry
|
|
265
|
+
|
|
266
|
+
# Calculate font checksum (with head checksumAdjustment set to 0)
|
|
267
|
+
# The head table at offset 8 should already be 0 from original table
|
|
268
|
+
font_checksum = calculate_font_checksum(font_data)
|
|
269
|
+
|
|
270
|
+
# Calculate adjustment
|
|
271
|
+
adjustment = (Constants::CHECKSUM_ADJUSTMENT_MAGIC - font_checksum) & 0xFFFFFFFF
|
|
272
|
+
|
|
273
|
+
# Update head table checksumAdjustment field (offset 8 in head table)
|
|
274
|
+
head_offset = head_entry[:offset]
|
|
275
|
+
checksum_offset = head_offset + 8
|
|
276
|
+
|
|
277
|
+
# Write adjustment as uint32 big-endian
|
|
278
|
+
font_data[checksum_offset, 4] = [adjustment].pack("N")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Calculate checksum of entire font file
|
|
282
|
+
#
|
|
283
|
+
# @param data [String] Complete font binary
|
|
284
|
+
# @return [Integer] Font checksum
|
|
285
|
+
def calculate_font_checksum(data)
|
|
286
|
+
# Pad to 4-byte boundary
|
|
287
|
+
padded_data = data.dup
|
|
288
|
+
padding_length = (4 - (data.bytesize % 4)) % 4
|
|
289
|
+
padded_data << ("\0" * padding_length) if padding_length.positive?
|
|
290
|
+
|
|
291
|
+
# Sum all uint32 values
|
|
292
|
+
sum = 0
|
|
293
|
+
(0...padded_data.bytesize).step(4) do |i|
|
|
294
|
+
value = padded_data[i, 4].unpack1("N")
|
|
295
|
+
sum = (sum + value) & 0xFFFFFFFF
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
sum
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -36,6 +36,12 @@ module Fontisan
|
|
|
36
36
|
format_all_scripts_features_info(model)
|
|
37
37
|
when Models::FeaturesInfo
|
|
38
38
|
format_features_info(model)
|
|
39
|
+
when Models::CollectionListInfo
|
|
40
|
+
format_collection_list_info(model)
|
|
41
|
+
when Models::FontSummary
|
|
42
|
+
format_font_summary(model)
|
|
43
|
+
when Models::CollectionInfo
|
|
44
|
+
format_collection_info(model)
|
|
39
45
|
else
|
|
40
46
|
model.to_s
|
|
41
47
|
end
|
|
@@ -309,6 +315,102 @@ module Fontisan
|
|
|
309
315
|
type += " (Variable)" if is_variable
|
|
310
316
|
type
|
|
311
317
|
end
|
|
318
|
+
|
|
319
|
+
# Format CollectionListInfo as human-readable text.
|
|
320
|
+
#
|
|
321
|
+
# @param info [Models::CollectionListInfo] Collection list information to format
|
|
322
|
+
# @return [String] Formatted text with fonts in collection
|
|
323
|
+
def format_collection_list_info(info)
|
|
324
|
+
lines = []
|
|
325
|
+
|
|
326
|
+
lines << "Collection: #{info.collection_path}"
|
|
327
|
+
lines << "Fonts: #{info.num_fonts}"
|
|
328
|
+
lines << ""
|
|
329
|
+
|
|
330
|
+
info.fonts.each do |font|
|
|
331
|
+
lines << "#{font.index}. #{font.family_name} #{font.subfamily_name}"
|
|
332
|
+
lines << " PostScript: #{font.postscript_name}"
|
|
333
|
+
lines << " Format: #{font.font_format}"
|
|
334
|
+
lines << " Glyphs: #{font.num_glyphs}, Tables: #{font.num_tables}"
|
|
335
|
+
lines << "" unless font.index == info.num_fonts - 1
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
lines.join("\n")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Format FontSummary as human-readable text.
|
|
342
|
+
#
|
|
343
|
+
# @param summary [Models::FontSummary] Font summary to format
|
|
344
|
+
# @return [String] Formatted text with font summary
|
|
345
|
+
def format_font_summary(summary)
|
|
346
|
+
lines = []
|
|
347
|
+
|
|
348
|
+
lines << "Font: #{summary.font_path}"
|
|
349
|
+
lines << "Family: #{summary.family_name} #{summary.subfamily_name}"
|
|
350
|
+
lines << "Format: #{summary.font_format}"
|
|
351
|
+
lines << "Glyphs: #{summary.num_glyphs}"
|
|
352
|
+
lines << "Tables: #{summary.num_tables}"
|
|
353
|
+
|
|
354
|
+
lines.join("\n")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Format CollectionInfo as human-readable text.
|
|
358
|
+
#
|
|
359
|
+
# @param info [Models::CollectionInfo] Collection information to format
|
|
360
|
+
# @return [String] Formatted text with collection metadata
|
|
361
|
+
def format_collection_info(info)
|
|
362
|
+
lines = []
|
|
363
|
+
|
|
364
|
+
# Header section
|
|
365
|
+
lines << "=== Collection Information ==="
|
|
366
|
+
lines << ""
|
|
367
|
+
lines << "File: #{info.collection_path}"
|
|
368
|
+
lines << "Format: #{info.collection_format}"
|
|
369
|
+
lines << "Size: #{format_bytes(info.file_size_bytes)}"
|
|
370
|
+
lines << ""
|
|
371
|
+
|
|
372
|
+
# Header details
|
|
373
|
+
lines << "=== Header ==="
|
|
374
|
+
lines << "Tag: #{info.ttc_tag}"
|
|
375
|
+
lines << "Version: #{info.version_string} (#{info.version_hex})"
|
|
376
|
+
lines << "Number of fonts: #{info.num_fonts}"
|
|
377
|
+
lines << ""
|
|
378
|
+
|
|
379
|
+
# Font offsets
|
|
380
|
+
lines << "=== Font Offsets ==="
|
|
381
|
+
info.font_offsets.each_with_index do |offset, index|
|
|
382
|
+
lines << Kernel.format(" %d. Offset: %8d (0x%08X)",
|
|
383
|
+
index, offset, offset)
|
|
384
|
+
end
|
|
385
|
+
lines << ""
|
|
386
|
+
|
|
387
|
+
# Table sharing statistics
|
|
388
|
+
if info.table_sharing
|
|
389
|
+
lines << "=== Table Sharing ==="
|
|
390
|
+
lines << "Shared tables: #{info.table_sharing.shared_tables}"
|
|
391
|
+
lines << "Unique tables: #{info.table_sharing.unique_tables}"
|
|
392
|
+
lines << "Sharing: #{format_float(info.table_sharing.sharing_percentage)}%"
|
|
393
|
+
lines << "Space saved: #{format_bytes(info.table_sharing.space_saved_bytes)}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
lines.join("\n")
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Format bytes for human-readable display.
|
|
400
|
+
#
|
|
401
|
+
# @param bytes [Integer] Number of bytes
|
|
402
|
+
# @return [String] Formatted byte size
|
|
403
|
+
def format_bytes(bytes)
|
|
404
|
+
return "0 B" if bytes.nil? || bytes.zero?
|
|
405
|
+
|
|
406
|
+
if bytes < 1024
|
|
407
|
+
"#{bytes} B"
|
|
408
|
+
elsif bytes < 1024 * 1024
|
|
409
|
+
"#{(bytes / 1024.0).round(2)} KB"
|
|
410
|
+
else
|
|
411
|
+
"#{(bytes / (1024.0 * 1024)).round(2)} MB"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
312
414
|
end
|
|
313
415
|
end
|
|
314
416
|
end
|