fontisan 0.2.1 → 0.2.2
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 +57 -385
- data/README.adoc +1483 -1435
- data/Rakefile +3 -2
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +10 -3
- data/lib/fontisan/collection/builder.rb +2 -1
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/commands/base_command.rb +5 -2
- data/lib/fontisan/commands/convert_command.rb +6 -2
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +8 -7
- data/lib/fontisan/commands/validate_command.rb +4 -1
- data/lib/fontisan/constants.rb +24 -24
- data/lib/fontisan/converters/format_converter.rb +8 -4
- data/lib/fontisan/converters/outline_converter.rb +21 -16
- data/lib/fontisan/converters/woff_writer.rb +8 -3
- data/lib/fontisan/font_loader.rb +11 -4
- data/lib/fontisan/font_writer.rb +2 -0
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +43 -47
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
- data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
- data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +4 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +22 -23
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_font.rb +3 -1
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/output_writer.rb +8 -3
- data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +38 -12
- data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
- data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
- data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
- data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
- data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +3 -1
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +2 -1
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/variation_preserver.rb +4 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
- data/lib/fontisan/woff2_font.rb +31 -15
- data/lib/fontisan.rb +42 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +8 -2
|
@@ -41,12 +41,70 @@ module Fontisan
|
|
|
41
41
|
|
|
42
42
|
# Get collection information
|
|
43
43
|
#
|
|
44
|
-
# @return [Models::CollectionInfo] Collection metadata
|
|
44
|
+
# @return [Models::CollectionInfo, Models::CollectionBriefInfo] Collection metadata
|
|
45
45
|
def collection_info
|
|
46
46
|
collection = FontLoader.load_collection(@font_path)
|
|
47
47
|
|
|
48
48
|
File.open(@font_path, "rb") do |io|
|
|
49
|
-
|
|
49
|
+
if @options[:brief]
|
|
50
|
+
# Brief mode: load each font and populate brief info
|
|
51
|
+
brief_info = Models::CollectionBriefInfo.new
|
|
52
|
+
brief_info.collection_path = @font_path
|
|
53
|
+
brief_info.num_fonts = collection.num_fonts
|
|
54
|
+
brief_info.fonts = []
|
|
55
|
+
|
|
56
|
+
collection.num_fonts.times do |index|
|
|
57
|
+
# Load individual font from collection
|
|
58
|
+
font = FontLoader.load(@font_path, font_index: index, mode: LoadingModes::METADATA)
|
|
59
|
+
|
|
60
|
+
# Populate brief info for this font
|
|
61
|
+
info = Models::FontInfo.new
|
|
62
|
+
|
|
63
|
+
# Font format and variable status
|
|
64
|
+
info.font_format = case font
|
|
65
|
+
when TrueTypeFont
|
|
66
|
+
"truetype"
|
|
67
|
+
when OpenTypeFont
|
|
68
|
+
"cff"
|
|
69
|
+
else
|
|
70
|
+
"unknown"
|
|
71
|
+
end
|
|
72
|
+
info.is_variable = font.has_table?(Constants::FVAR_TAG)
|
|
73
|
+
|
|
74
|
+
# Collection offset (only populated for fonts in collections)
|
|
75
|
+
info.collection_offset = collection.font_offsets[index]
|
|
76
|
+
|
|
77
|
+
# Essential names
|
|
78
|
+
if font.has_table?(Constants::NAME_TAG)
|
|
79
|
+
name_table = font.table(Constants::NAME_TAG)
|
|
80
|
+
info.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
81
|
+
info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
82
|
+
info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
83
|
+
info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
84
|
+
info.version = name_table.english_name(Tables::Name::VERSION)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Essential metrics
|
|
88
|
+
if font.has_table?(Constants::HEAD_TAG)
|
|
89
|
+
head = font.table(Constants::HEAD_TAG)
|
|
90
|
+
info.font_revision = head.font_revision
|
|
91
|
+
info.units_per_em = head.units_per_em
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Vendor ID
|
|
95
|
+
if font.has_table?(Constants::OS2_TAG)
|
|
96
|
+
os2_table = font.table(Constants::OS2_TAG)
|
|
97
|
+
info.vendor_id = os2_table.vendor_id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
brief_info.fonts << info
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
brief_info
|
|
104
|
+
else
|
|
105
|
+
# Full mode: show detailed sharing statistics
|
|
106
|
+
collection.collection_info(io, @font_path)
|
|
107
|
+
end
|
|
50
108
|
end
|
|
51
109
|
end
|
|
52
110
|
|
|
@@ -56,9 +114,14 @@ module Fontisan
|
|
|
56
114
|
def font_info
|
|
57
115
|
info = Models::FontInfo.new
|
|
58
116
|
populate_font_format(info)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
117
|
+
|
|
118
|
+
# In brief mode, only populate essential fields for fast identification
|
|
119
|
+
if @options[:brief]
|
|
120
|
+
populate_brief_fields(info)
|
|
121
|
+
else
|
|
122
|
+
populate_full_fields(info)
|
|
123
|
+
end
|
|
124
|
+
|
|
62
125
|
info
|
|
63
126
|
end
|
|
64
127
|
|
|
@@ -80,6 +143,49 @@ module Fontisan
|
|
|
80
143
|
info.is_variable = font.has_table?(Constants::FVAR_TAG)
|
|
81
144
|
end
|
|
82
145
|
|
|
146
|
+
# Populate essential fields for brief mode (metadata tables only).
|
|
147
|
+
#
|
|
148
|
+
# Brief mode provides fast font identification by loading only 13 essential
|
|
149
|
+
# attributes from metadata tables (name, head, OS/2). This is 5x faster than
|
|
150
|
+
# full mode and optimized for font indexing systems.
|
|
151
|
+
#
|
|
152
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
153
|
+
def populate_brief_fields(info)
|
|
154
|
+
# Essential names from name table
|
|
155
|
+
if font.has_table?(Constants::NAME_TAG)
|
|
156
|
+
name_table = font.table(Constants::NAME_TAG)
|
|
157
|
+
info.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
158
|
+
info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
159
|
+
info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
160
|
+
info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
161
|
+
info.version = name_table.english_name(Tables::Name::VERSION)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Essential metrics from head table
|
|
165
|
+
if font.has_table?(Constants::HEAD_TAG)
|
|
166
|
+
head = font.table(Constants::HEAD_TAG)
|
|
167
|
+
info.font_revision = head.font_revision
|
|
168
|
+
info.units_per_em = head.units_per_em
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Vendor ID from OS/2 table
|
|
172
|
+
if font.has_table?(Constants::OS2_TAG)
|
|
173
|
+
os2_table = font.table(Constants::OS2_TAG)
|
|
174
|
+
info.vendor_id = os2_table.vendor_id
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Populate all fields for full mode.
|
|
179
|
+
#
|
|
180
|
+
# Full mode extracts comprehensive metadata from all available tables.
|
|
181
|
+
#
|
|
182
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
183
|
+
def populate_full_fields(info)
|
|
184
|
+
populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
|
|
185
|
+
populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
|
|
186
|
+
populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
|
|
187
|
+
end
|
|
188
|
+
|
|
83
189
|
# Populate FontInfo from the name table.
|
|
84
190
|
#
|
|
85
191
|
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
@@ -67,11 +67,11 @@ module Fontisan
|
|
|
67
67
|
|
|
68
68
|
puts "Static font instance written to: #{output_path}"
|
|
69
69
|
rescue VariationError => e
|
|
70
|
-
|
|
70
|
+
warn "Variation Error: #{e.detailed_message}"
|
|
71
71
|
exit 1
|
|
72
72
|
rescue StandardError => e
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
warn "Error: #{e.message}"
|
|
74
|
+
warn e.backtrace.first(5).join("\n") if options[:verbose]
|
|
75
75
|
exit 1
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -87,9 +87,9 @@ module Fontisan
|
|
|
87
87
|
errors = validator.validate
|
|
88
88
|
|
|
89
89
|
if errors.any?
|
|
90
|
-
|
|
90
|
+
warn "Validation errors found:"
|
|
91
91
|
errors.each do |error|
|
|
92
|
-
|
|
92
|
+
warn " - #{error}"
|
|
93
93
|
end
|
|
94
94
|
exit 1
|
|
95
95
|
end
|
|
@@ -102,7 +102,7 @@ module Fontisan
|
|
|
102
102
|
# @param font [Object] Font object
|
|
103
103
|
# @param input_path [String] Input file path
|
|
104
104
|
# @param options [Hash] Command options
|
|
105
|
-
def preview_instance(
|
|
105
|
+
def preview_instance(_font, input_path, options)
|
|
106
106
|
coords = extract_coordinates(options)
|
|
107
107
|
|
|
108
108
|
if coords.empty?
|
|
@@ -117,7 +117,8 @@ module Fontisan
|
|
|
117
117
|
puts " #{axis}: #{value}"
|
|
118
118
|
end
|
|
119
119
|
puts
|
|
120
|
-
puts "Output would be written to: #{determine_output_path(input_path,
|
|
120
|
+
puts "Output would be written to: #{determine_output_path(input_path,
|
|
121
|
+
options)}"
|
|
121
122
|
puts "Output format: #{options[:to] || 'same as input'}"
|
|
122
123
|
puts
|
|
123
124
|
puts "Use without --dry-run to actually generate the instance."
|
|
@@ -84,7 +84,10 @@ quiet: false)
|
|
|
84
84
|
errors.each do |error|
|
|
85
85
|
puts " ERROR: #{error}" if @verbose && !@quiet
|
|
86
86
|
# Add to report if report supports adding errors
|
|
87
|
-
|
|
87
|
+
if report.respond_to?(:errors)
|
|
88
|
+
report.errors << { message: error,
|
|
89
|
+
category: "variable_font" }
|
|
90
|
+
end
|
|
88
91
|
end
|
|
89
92
|
elsif @verbose && !@quiet
|
|
90
93
|
puts "\n✓ Variable font structure valid"
|
data/lib/fontisan/constants.rb
CHANGED
|
@@ -117,30 +117,30 @@ module Fontisan
|
|
|
117
117
|
# These strings are frozen and reused to reduce memory allocations
|
|
118
118
|
# when parsing fonts with common subfamily names.
|
|
119
119
|
STRING_POOL = {
|
|
120
|
-
"Regular" => "Regular"
|
|
121
|
-
"Bold" => "Bold"
|
|
122
|
-
"Italic" => "Italic"
|
|
123
|
-
"Bold Italic" => "Bold Italic"
|
|
124
|
-
"BoldItalic" => "BoldItalic"
|
|
125
|
-
"Light" => "Light"
|
|
126
|
-
"Medium" => "Medium"
|
|
127
|
-
"Semibold" => "Semibold"
|
|
128
|
-
"SemiBold" => "SemiBold"
|
|
129
|
-
"Black" => "Black"
|
|
130
|
-
"Thin" => "Thin"
|
|
131
|
-
"ExtraLight" => "ExtraLight"
|
|
132
|
-
"Extra Light" => "Extra Light"
|
|
133
|
-
"ExtraBold" => "ExtraBold"
|
|
134
|
-
"Extra Bold" => "Extra Bold"
|
|
135
|
-
"Heavy" => "Heavy"
|
|
136
|
-
"Book" => "Book"
|
|
137
|
-
"Roman" => "Roman"
|
|
138
|
-
"Normal" => "Normal"
|
|
139
|
-
"Oblique" => "Oblique"
|
|
140
|
-
"Light Italic" => "Light Italic"
|
|
141
|
-
"Medium Italic" => "Medium Italic"
|
|
142
|
-
"Semibold Italic" => "Semibold Italic"
|
|
143
|
-
"Bold Oblique" => "Bold Oblique"
|
|
120
|
+
"Regular" => "Regular",
|
|
121
|
+
"Bold" => "Bold",
|
|
122
|
+
"Italic" => "Italic",
|
|
123
|
+
"Bold Italic" => "Bold Italic",
|
|
124
|
+
"BoldItalic" => "BoldItalic",
|
|
125
|
+
"Light" => "Light",
|
|
126
|
+
"Medium" => "Medium",
|
|
127
|
+
"Semibold" => "Semibold",
|
|
128
|
+
"SemiBold" => "SemiBold",
|
|
129
|
+
"Black" => "Black",
|
|
130
|
+
"Thin" => "Thin",
|
|
131
|
+
"ExtraLight" => "ExtraLight",
|
|
132
|
+
"Extra Light" => "Extra Light",
|
|
133
|
+
"ExtraBold" => "ExtraBold",
|
|
134
|
+
"Extra Bold" => "Extra Bold",
|
|
135
|
+
"Heavy" => "Heavy",
|
|
136
|
+
"Book" => "Book",
|
|
137
|
+
"Roman" => "Roman",
|
|
138
|
+
"Normal" => "Normal",
|
|
139
|
+
"Oblique" => "Oblique",
|
|
140
|
+
"Light Italic" => "Light Italic",
|
|
141
|
+
"Medium Italic" => "Medium Italic",
|
|
142
|
+
"Semibold Italic" => "Semibold Italic",
|
|
143
|
+
"Bold Oblique" => "Bold Oblique",
|
|
144
144
|
}.freeze
|
|
145
145
|
|
|
146
146
|
# Intern a string using the string pool
|
|
@@ -103,7 +103,8 @@ module Fontisan
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
strategy = select_strategy(source_format, target_format)
|
|
106
|
-
tables = strategy.convert(font,
|
|
106
|
+
tables = strategy.convert(font,
|
|
107
|
+
options.merge(target_format: target_format))
|
|
107
108
|
|
|
108
109
|
# Preserve variation data if requested and font is variable
|
|
109
110
|
if options.fetch(:preserve_variation, true) && variable_font?(font)
|
|
@@ -204,7 +205,8 @@ module Fontisan
|
|
|
204
205
|
# @param target_format [Symbol] Target format
|
|
205
206
|
# @param options [Hash] Preservation options
|
|
206
207
|
# @return [Hash<String, String>] Tables with variation preserved
|
|
207
|
-
def preserve_variation_data(font, tables, source_format, target_format,
|
|
208
|
+
def preserve_variation_data(font, tables, source_format, target_format,
|
|
209
|
+
options)
|
|
208
210
|
# Case 1: Compatible formats (same outline format) - just copy tables
|
|
209
211
|
if compatible_variation_formats?(source_format, target_format)
|
|
210
212
|
require_relative "../variation/variation_preserver"
|
|
@@ -212,7 +214,8 @@ module Fontisan
|
|
|
212
214
|
|
|
213
215
|
# Case 2: Different outline formats - convert variation data
|
|
214
216
|
elsif convertible_variation_formats?(source_format, target_format)
|
|
215
|
-
convert_variation_data(font, tables, source_format, target_format,
|
|
217
|
+
convert_variation_data(font, tables, source_format, target_format,
|
|
218
|
+
options)
|
|
216
219
|
|
|
217
220
|
# Case 3: Unsupported conversion
|
|
218
221
|
else
|
|
@@ -268,7 +271,8 @@ module Fontisan
|
|
|
268
271
|
# @param target_format [Symbol] Target format
|
|
269
272
|
# @param options [Hash] Conversion options
|
|
270
273
|
# @return [Hash<String, String>] Tables with converted variation
|
|
271
|
-
def convert_variation_data(font, tables, source_format, target_format,
|
|
274
|
+
def convert_variation_data(font, tables, source_format, target_format,
|
|
275
|
+
_options)
|
|
272
276
|
require_relative "../variation/variation_preserver"
|
|
273
277
|
require_relative "../variation/converter"
|
|
274
278
|
|
|
@@ -131,7 +131,7 @@ module Fontisan
|
|
|
131
131
|
# @param font [TrueTypeFont] Source font
|
|
132
132
|
# @param options [Hash] Conversion options (currently unused)
|
|
133
133
|
# @return [Hash<String, String>] Target tables
|
|
134
|
-
def convert_ttf_to_otf(font,
|
|
134
|
+
def convert_ttf_to_otf(font, _options = {})
|
|
135
135
|
# Extract all glyphs from glyf table
|
|
136
136
|
outlines = extract_ttf_outlines(font)
|
|
137
137
|
|
|
@@ -184,7 +184,8 @@ module Fontisan
|
|
|
184
184
|
hints_per_glyph = @preserve_hints ? extract_cff_hints(font) : {}
|
|
185
185
|
|
|
186
186
|
# Build glyf and loca tables
|
|
187
|
-
glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines,
|
|
187
|
+
glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines,
|
|
188
|
+
hints_per_glyph)
|
|
188
189
|
|
|
189
190
|
# Copy all tables except CFF
|
|
190
191
|
tables = copy_tables(font, ["CFF ", "CFF2"])
|
|
@@ -357,7 +358,7 @@ module Fontisan
|
|
|
357
358
|
# @param outlines [Array<Outline>] Glyph outlines
|
|
358
359
|
# @param font [TrueTypeFont] Source font (for metadata)
|
|
359
360
|
# @return [String] CFF table binary data
|
|
360
|
-
def build_cff_table(outlines, font,
|
|
361
|
+
def build_cff_table(outlines, font, _hints_per_glyph)
|
|
361
362
|
# Build CharStrings INDEX from outlines
|
|
362
363
|
begin
|
|
363
364
|
charstrings = outlines.map do |outline|
|
|
@@ -478,7 +479,8 @@ module Fontisan
|
|
|
478
479
|
top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
|
|
479
480
|
top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
|
|
480
481
|
rescue StandardError => e
|
|
481
|
-
raise Fontisan::Error,
|
|
482
|
+
raise Fontisan::Error,
|
|
483
|
+
"Failed to calculate CFF table offsets: #{e.message}"
|
|
482
484
|
end
|
|
483
485
|
|
|
484
486
|
# Build CFF Header
|
|
@@ -512,7 +514,7 @@ module Fontisan
|
|
|
512
514
|
#
|
|
513
515
|
# @param outlines [Array<Outline>] Glyph outlines
|
|
514
516
|
# @return [Array<String, String, Integer>] [glyf_data, loca_data, loca_format]
|
|
515
|
-
def build_glyf_loca_tables(outlines,
|
|
517
|
+
def build_glyf_loca_tables(outlines, _hints_per_glyph)
|
|
516
518
|
glyf_data = "".b
|
|
517
519
|
offsets = []
|
|
518
520
|
|
|
@@ -687,7 +689,7 @@ module Fontisan
|
|
|
687
689
|
# Analyze patterns
|
|
688
690
|
analyzer = Optimizers::PatternAnalyzer.new(
|
|
689
691
|
min_length: 10,
|
|
690
|
-
stack_aware: true
|
|
692
|
+
stack_aware: true,
|
|
691
693
|
)
|
|
692
694
|
patterns = analyzer.analyze(charstrings_hash)
|
|
693
695
|
|
|
@@ -695,7 +697,8 @@ module Fontisan
|
|
|
695
697
|
return [charstrings, []] if patterns.empty?
|
|
696
698
|
|
|
697
699
|
# Optimize selection
|
|
698
|
-
optimizer = Optimizers::SubroutineOptimizer.new(patterns,
|
|
700
|
+
optimizer = Optimizers::SubroutineOptimizer.new(patterns,
|
|
701
|
+
max_subrs: 65_535)
|
|
699
702
|
selected_patterns = optimizer.optimize_selection
|
|
700
703
|
|
|
701
704
|
# Optimize ordering
|
|
@@ -705,7 +708,8 @@ module Fontisan
|
|
|
705
708
|
return [charstrings, []] if selected_patterns.empty?
|
|
706
709
|
|
|
707
710
|
# Build subroutines
|
|
708
|
-
builder = Optimizers::SubroutineBuilder.new(selected_patterns,
|
|
711
|
+
builder = Optimizers::SubroutineBuilder.new(selected_patterns,
|
|
712
|
+
type: :local)
|
|
709
713
|
local_subrs = builder.build
|
|
710
714
|
|
|
711
715
|
# Build subroutine map
|
|
@@ -718,7 +722,9 @@ module Fontisan
|
|
|
718
722
|
rewriter = Optimizers::CharstringRewriter.new(subroutine_map, builder)
|
|
719
723
|
optimized_charstrings = charstrings.map.with_index do |charstring, glyph_id|
|
|
720
724
|
# Find patterns for this glyph
|
|
721
|
-
glyph_patterns = selected_patterns.select
|
|
725
|
+
glyph_patterns = selected_patterns.select do |p|
|
|
726
|
+
p.glyphs.include?(glyph_id)
|
|
727
|
+
end
|
|
722
728
|
|
|
723
729
|
if glyph_patterns.empty?
|
|
724
730
|
charstring
|
|
@@ -743,13 +749,16 @@ module Fontisan
|
|
|
743
749
|
def generate_static_instance(font, source_format, target_format)
|
|
744
750
|
# Generate instance at specified coordinates
|
|
745
751
|
fvar = font.table("fvar")
|
|
746
|
-
|
|
752
|
+
fvar ? fvar.axes : []
|
|
747
753
|
|
|
748
|
-
generator = Variation::InstanceGenerator.new(font,
|
|
754
|
+
generator = Variation::InstanceGenerator.new(font,
|
|
755
|
+
@instance_coordinates)
|
|
749
756
|
instance_tables = generator.generate
|
|
750
757
|
|
|
751
758
|
# If target format differs from source, convert outlines
|
|
752
|
-
if source_format
|
|
759
|
+
if source_format == target_format
|
|
760
|
+
instance_tables
|
|
761
|
+
else
|
|
753
762
|
# Create temporary font with instance tables
|
|
754
763
|
temp_font = font.class.new
|
|
755
764
|
temp_font.instance_variable_set(:@table_data, instance_tables)
|
|
@@ -763,8 +772,6 @@ module Fontisan
|
|
|
763
772
|
else
|
|
764
773
|
instance_tables
|
|
765
774
|
end
|
|
766
|
-
else
|
|
767
|
-
instance_tables
|
|
768
775
|
end
|
|
769
776
|
end
|
|
770
777
|
|
|
@@ -801,8 +808,6 @@ module Fontisan
|
|
|
801
808
|
when %i[otf ttf], %i[cff2 ttf]
|
|
802
809
|
# blend → gvar
|
|
803
810
|
converter.blend_to_gvar(glyph_id)
|
|
804
|
-
else
|
|
805
|
-
nil
|
|
806
811
|
end
|
|
807
812
|
|
|
808
813
|
variation_data[glyph_id] = data if data
|
|
@@ -128,7 +128,8 @@ module Fontisan
|
|
|
128
128
|
# @raise [ArgumentError] if compression level is invalid
|
|
129
129
|
def validate_compression_level!
|
|
130
130
|
unless @compression_level.between?(0, 9)
|
|
131
|
-
raise ArgumentError,
|
|
131
|
+
raise ArgumentError,
|
|
132
|
+
"Compression level must be between 0 and 9, got #{@compression_level}"
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
|
|
@@ -235,7 +236,9 @@ module Fontisan
|
|
|
235
236
|
metadata_size = compressed_metadata ? compressed_metadata[:compressed_length] : 0
|
|
236
237
|
|
|
237
238
|
# Calculate total compressed data size
|
|
238
|
-
total_compressed_size = compressed_tables.values.sum
|
|
239
|
+
total_compressed_size = compressed_tables.values.sum do |table|
|
|
240
|
+
table[:compressed_length]
|
|
241
|
+
end
|
|
239
242
|
|
|
240
243
|
# Calculate private data offset (after table data + metadata)
|
|
241
244
|
private_offset = data_offset + total_compressed_size + metadata_size
|
|
@@ -245,7 +248,9 @@ module Fontisan
|
|
|
245
248
|
total_size = private_offset + private_size
|
|
246
249
|
|
|
247
250
|
# Calculate total SFNT size (uncompressed)
|
|
248
|
-
total_sfnt_size = compressed_tables.values.sum
|
|
251
|
+
total_sfnt_size = compressed_tables.values.sum do |table|
|
|
252
|
+
table[:original_length]
|
|
253
|
+
end +
|
|
249
254
|
header_size + table_dir_size
|
|
250
255
|
|
|
251
256
|
# Write WOFF header
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -46,7 +46,11 @@ module Fontisan
|
|
|
46
46
|
|
|
47
47
|
# Resolve mode and lazy parameters with environment variables
|
|
48
48
|
resolved_mode = mode || env_mode || LoadingModes::FULL
|
|
49
|
-
resolved_lazy = lazy.nil?
|
|
49
|
+
resolved_lazy = if lazy.nil?
|
|
50
|
+
env_lazy.nil? ? false : env_lazy
|
|
51
|
+
else
|
|
52
|
+
lazy
|
|
53
|
+
end
|
|
50
54
|
|
|
51
55
|
# Validate mode
|
|
52
56
|
LoadingModes.validate_mode!(resolved_mode)
|
|
@@ -57,7 +61,8 @@ module Fontisan
|
|
|
57
61
|
|
|
58
62
|
case signature
|
|
59
63
|
when Constants::TTC_TAG
|
|
60
|
-
load_from_collection(io, path, font_index, mode: resolved_mode,
|
|
64
|
+
load_from_collection(io, path, font_index, mode: resolved_mode,
|
|
65
|
+
lazy: resolved_lazy)
|
|
61
66
|
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
|
|
62
67
|
TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
63
68
|
when "OTTO"
|
|
@@ -169,7 +174,8 @@ module Fontisan
|
|
|
169
174
|
# @param lazy [Boolean] If true, load tables on demand
|
|
170
175
|
# @return [TrueTypeFont, OpenTypeFont] The loaded font object
|
|
171
176
|
# @raise [InvalidFontError] if collection type cannot be determined
|
|
172
|
-
def self.load_from_collection(io, path, font_index,
|
|
177
|
+
def self.load_from_collection(io, path, font_index,
|
|
178
|
+
mode: LoadingModes::FULL, lazy: true)
|
|
173
179
|
# Read collection header to get font offsets
|
|
174
180
|
io.seek(12) # Skip tag (4) + major_version (2) + minor_version (2) + num_fonts marker (4)
|
|
175
181
|
num_fonts = io.read(4).unpack1("N")
|
|
@@ -211,6 +217,7 @@ module Fontisan
|
|
|
211
217
|
[value].pack("N")
|
|
212
218
|
end
|
|
213
219
|
|
|
214
|
-
private_class_method :load_from_collection, :pack_uint32, :env_mode,
|
|
220
|
+
private_class_method :load_from_collection, :pack_uint32, :env_mode,
|
|
221
|
+
:env_lazy
|
|
215
222
|
end
|
|
216
223
|
end
|
data/lib/fontisan/font_writer.rb
CHANGED
|
@@ -113,6 +113,7 @@ module Fontisan
|
|
|
113
113
|
# Write offset table (sfnt header)
|
|
114
114
|
font_data << write_offset_table(table_entries.size)
|
|
115
115
|
|
|
116
|
+
# rubocop:disable Style/CombinableLoops
|
|
116
117
|
# Write table directory (ALL entries first)
|
|
117
118
|
table_entries.each do |entry|
|
|
118
119
|
font_data << write_table_entry(entry)
|
|
@@ -123,6 +124,7 @@ module Fontisan
|
|
|
123
124
|
font_data << entry[:data]
|
|
124
125
|
font_data << entry[:padding]
|
|
125
126
|
end
|
|
127
|
+
# rubocop:enable Style/CombinableLoops
|
|
126
128
|
|
|
127
129
|
# Calculate and update head table checksum adjustment
|
|
128
130
|
update_checksum_adjustment!(font_data, table_entries)
|
|
@@ -42,6 +42,8 @@ module Fontisan
|
|
|
42
42
|
format_font_summary(model)
|
|
43
43
|
when Models::CollectionInfo
|
|
44
44
|
format_collection_info(model)
|
|
45
|
+
when Models::CollectionBriefInfo
|
|
46
|
+
format_collection_brief_info(model)
|
|
45
47
|
else
|
|
46
48
|
model.to_s
|
|
47
49
|
end
|
|
@@ -312,7 +314,8 @@ module Fontisan
|
|
|
312
314
|
font_format
|
|
313
315
|
end
|
|
314
316
|
|
|
315
|
-
|
|
317
|
+
# Always show variable status explicitly
|
|
318
|
+
type += is_variable ? " (Variable)" : " (Not Variable)"
|
|
316
319
|
type
|
|
317
320
|
end
|
|
318
321
|
|
|
@@ -396,6 +399,47 @@ module Fontisan
|
|
|
396
399
|
lines.join("\n")
|
|
397
400
|
end
|
|
398
401
|
|
|
402
|
+
# Format CollectionBriefInfo as human-readable text.
|
|
403
|
+
#
|
|
404
|
+
# @param info [Models::CollectionBriefInfo] Collection brief information to format
|
|
405
|
+
# @return [String] Formatted text with collection header and each font's brief info
|
|
406
|
+
def format_collection_brief_info(info)
|
|
407
|
+
lines = []
|
|
408
|
+
|
|
409
|
+
# Collection header
|
|
410
|
+
lines << "Collection: #{info.collection_path}"
|
|
411
|
+
lines << "Fonts: #{info.num_fonts}"
|
|
412
|
+
lines << ""
|
|
413
|
+
|
|
414
|
+
# Each font's brief info
|
|
415
|
+
info.fonts.each_with_index do |font_info, index|
|
|
416
|
+
# Show font index with offset
|
|
417
|
+
if font_info.collection_offset
|
|
418
|
+
lines << "Font #{index} (offset: #{font_info.collection_offset}):"
|
|
419
|
+
else
|
|
420
|
+
lines << "Font #{index}:"
|
|
421
|
+
end
|
|
422
|
+
lines << ""
|
|
423
|
+
|
|
424
|
+
# Format each font using same structure as individual fonts
|
|
425
|
+
font_type_display = format_font_type_display(font_info.font_format, font_info.is_variable)
|
|
426
|
+
add_line(lines, "Font type", font_type_display)
|
|
427
|
+
add_line(lines, "Family", font_info.family_name)
|
|
428
|
+
add_line(lines, "Subfamily", font_info.subfamily_name)
|
|
429
|
+
add_line(lines, "Full name", font_info.full_name)
|
|
430
|
+
add_line(lines, "PostScript name", font_info.postscript_name)
|
|
431
|
+
add_line(lines, "Version", font_info.version)
|
|
432
|
+
add_line(lines, "Vendor ID", font_info.vendor_id)
|
|
433
|
+
add_line(lines, "Font revision", format_float(font_info.font_revision))
|
|
434
|
+
add_line(lines, "Units per em", font_info.units_per_em)
|
|
435
|
+
|
|
436
|
+
# Blank line between fonts (except after last)
|
|
437
|
+
lines << "" unless index == info.num_fonts - 1
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
lines.join("\n")
|
|
441
|
+
end
|
|
442
|
+
|
|
399
443
|
# Format bytes for human-readable display.
|
|
400
444
|
#
|
|
401
445
|
# @param bytes [Integer] Number of bytes
|