fontisan 0.2.7 → 0.2.9
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.yml +103 -0
- data/.rubocop_todo.yml +65 -361
- data/CHANGELOG.md +116 -0
- data/Gemfile +1 -1
- data/README.adoc +106 -27
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
- data/docs/COLLECTION_VALIDATION.adoc +143 -0
- data/docs/COLOR_FONTS.adoc +127 -0
- data/docs/DOCUMENTATION_SUMMARY.md +141 -0
- data/docs/FONT_HINTING.adoc +9 -1
- data/docs/VALIDATION.adoc +254 -0
- data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
- data/lib/fontisan/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- data/lib/fontisan/commands/convert_command.rb +2 -4
- data/lib/fontisan/commands/info_command.rb +3 -3
- data/lib/fontisan/commands/pack_command.rb +2 -1
- data/lib/fontisan/commands/validate_command.rb +157 -6
- data/lib/fontisan/converters/collection_converter.rb +22 -13
- data/lib/fontisan/converters/svg_generator.rb +2 -1
- data/lib/fontisan/converters/woff2_encoder.rb +6 -6
- data/lib/fontisan/converters/woff_writer.rb +3 -1
- data/lib/fontisan/font_loader.rb +7 -6
- data/lib/fontisan/formatters/text_formatter.rb +18 -14
- data/lib/fontisan/hints/hint_converter.rb +1 -1
- data/lib/fontisan/hints/hint_validator.rb +13 -10
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
- data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
- data/lib/fontisan/models/collection_validation_report.rb +104 -0
- data/lib/fontisan/models/font_report.rb +24 -0
- data/lib/fontisan/models/validation_report.rb +7 -2
- data/lib/fontisan/open_type_font.rb +18 -425
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
- data/lib/fontisan/sfnt_font.rb +699 -0
- data/lib/fontisan/sfnt_table.rb +264 -0
- data/lib/fontisan/subset/glyph_mapping.rb +2 -0
- data/lib/fontisan/subset/table_subsetter.rb +2 -2
- data/lib/fontisan/tables/cblc.rb +8 -4
- data/lib/fontisan/tables/cff/index.rb +2 -0
- data/lib/fontisan/tables/cff.rb +6 -3
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/cmap.rb +5 -5
- data/lib/fontisan/tables/cmap_table.rb +231 -0
- data/lib/fontisan/tables/glyf.rb +8 -10
- data/lib/fontisan/tables/glyf_table.rb +255 -0
- data/lib/fontisan/tables/head.rb +3 -3
- data/lib/fontisan/tables/head_table.rb +111 -0
- data/lib/fontisan/tables/hhea.rb +4 -4
- data/lib/fontisan/tables/hhea_table.rb +255 -0
- data/lib/fontisan/tables/hmtx_table.rb +191 -0
- data/lib/fontisan/tables/loca_table.rb +212 -0
- data/lib/fontisan/tables/maxp.rb +2 -2
- data/lib/fontisan/tables/maxp_table.rb +258 -0
- data/lib/fontisan/tables/name.rb +1 -1
- data/lib/fontisan/tables/name_table.rb +176 -0
- data/lib/fontisan/tables/os2.rb +8 -8
- data/lib/fontisan/tables/os2_table.rb +329 -0
- data/lib/fontisan/tables/post.rb +2 -2
- data/lib/fontisan/tables/post_table.rb +183 -0
- data/lib/fontisan/tables/sbix.rb +5 -4
- data/lib/fontisan/true_type_font.rb +12 -464
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
- data/lib/fontisan/validation/collection_validator.rb +4 -2
- data/lib/fontisan/validators/basic_validator.rb +11 -21
- data/lib/fontisan/validators/font_book_validator.rb +29 -50
- data/lib/fontisan/validators/opentype_validator.rb +24 -28
- data/lib/fontisan/validators/validator.rb +87 -66
- data/lib/fontisan/validators/web_font_validator.rb +16 -21
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
- data/lib/fontisan/woff2/table_transformer.rb +4 -2
- data/lib/fontisan/woff2_font.rb +4 -2
- data/lib/fontisan/woff_font.rb +46 -30
- data/lib/fontisan.rb +2 -2
- data/scripts/compare_stack_aware.rb +1 -1
- data/scripts/measure_optimization.rb +1 -2
- metadata +23 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "base_command"
|
|
4
4
|
require_relative "../validators/profile_loader"
|
|
5
5
|
require_relative "../font_loader"
|
|
6
|
+
require_relative "../tables/name"
|
|
6
7
|
|
|
7
8
|
module Fontisan
|
|
8
9
|
module Commands
|
|
@@ -70,12 +71,40 @@ module Fontisan
|
|
|
70
71
|
# Load font with appropriate mode
|
|
71
72
|
profile_config = Validators::ProfileLoader.profile_info(@profile)
|
|
72
73
|
unless profile_config
|
|
73
|
-
|
|
74
|
+
unless @suppress_warnings
|
|
75
|
+
puts "Error: Unknown profile '#{@profile}'"
|
|
76
|
+
puts ""
|
|
77
|
+
puts "Available profiles:"
|
|
78
|
+
Validators::ProfileLoader.all_profiles.each do |name, config|
|
|
79
|
+
puts " #{name.to_s.ljust(20)} - #{config[:description]}"
|
|
80
|
+
end
|
|
81
|
+
puts ""
|
|
82
|
+
puts "Use --list to see all available profiles"
|
|
83
|
+
end
|
|
74
84
|
return 1
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
mode = profile_config[:loading_mode].to_sym
|
|
78
88
|
|
|
89
|
+
# Check if input is a collection
|
|
90
|
+
if FontLoader.collection?(@input)
|
|
91
|
+
validate_collection(mode)
|
|
92
|
+
else
|
|
93
|
+
validate_single_font(mode)
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
puts "Error: #{e.message}" unless @suppress_warnings
|
|
97
|
+
puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
|
|
98
|
+
1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Validate a single font file
|
|
104
|
+
#
|
|
105
|
+
# @param mode [Symbol] Loading mode
|
|
106
|
+
# @return [Integer] Exit code
|
|
107
|
+
def validate_single_font(mode)
|
|
79
108
|
font = FontLoader.load(@input, mode: mode)
|
|
80
109
|
|
|
81
110
|
# Select validator
|
|
@@ -102,13 +131,134 @@ module Fontisan
|
|
|
102
131
|
|
|
103
132
|
# Return exit code
|
|
104
133
|
exit_code(report)
|
|
105
|
-
rescue => e
|
|
106
|
-
puts "Error: #{e.message}" unless @suppress_warnings
|
|
107
|
-
puts e.backtrace.join("\n") if @verbose && !@suppress_warnings
|
|
108
|
-
1
|
|
109
134
|
end
|
|
110
135
|
|
|
111
|
-
|
|
136
|
+
# Validate all fonts in a collection
|
|
137
|
+
#
|
|
138
|
+
# @param mode [Symbol] Loading mode
|
|
139
|
+
# @return [Integer] Exit code
|
|
140
|
+
def validate_collection(mode)
|
|
141
|
+
require_relative "../models/collection_validation_report"
|
|
142
|
+
require_relative "../models/font_report"
|
|
143
|
+
|
|
144
|
+
# Load collection metadata
|
|
145
|
+
collection = FontLoader.load_collection(@input)
|
|
146
|
+
|
|
147
|
+
# Create collection report
|
|
148
|
+
collection_report = Models::CollectionValidationReport.new(
|
|
149
|
+
collection_path: @input,
|
|
150
|
+
collection_type: collection.class.collection_format,
|
|
151
|
+
num_fonts: collection.num_fonts,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Get validator
|
|
155
|
+
validator = Validators::ProfileLoader.load(@profile)
|
|
156
|
+
|
|
157
|
+
# Validate each font
|
|
158
|
+
collection.num_fonts.times do |index|
|
|
159
|
+
font = FontLoader.load(@input, font_index: index, mode: mode)
|
|
160
|
+
font_report = validator.validate(font)
|
|
161
|
+
|
|
162
|
+
# Set font_path to indicate collection file with index
|
|
163
|
+
# Fonts in collections don't have individual file paths
|
|
164
|
+
font_report.font_path = "#{@input}:#{index}"
|
|
165
|
+
|
|
166
|
+
# Extract font name
|
|
167
|
+
font_name = extract_font_name(font, index)
|
|
168
|
+
|
|
169
|
+
# Create and add font report
|
|
170
|
+
collection_report.add_font_report(
|
|
171
|
+
Models::FontReport.new(
|
|
172
|
+
font_index: index,
|
|
173
|
+
font_name: font_name,
|
|
174
|
+
report: font_report,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
# Create error report for failed font loading
|
|
179
|
+
# Use collection file path with index for fonts in collections
|
|
180
|
+
error_report = Models::ValidationReport.new(
|
|
181
|
+
font_path: "#{@input}:#{index}",
|
|
182
|
+
valid: false,
|
|
183
|
+
)
|
|
184
|
+
error_report.add_error("font_loading",
|
|
185
|
+
"Failed to load font #{index}: #{e.message}", nil)
|
|
186
|
+
|
|
187
|
+
collection_report.add_font_report(
|
|
188
|
+
Models::FontReport.new(
|
|
189
|
+
font_index: index,
|
|
190
|
+
font_name: "Font #{index}",
|
|
191
|
+
report: error_report,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Generate output based on format
|
|
197
|
+
output = case @format
|
|
198
|
+
when :yaml
|
|
199
|
+
collection_report.to_yaml
|
|
200
|
+
when :json
|
|
201
|
+
collection_report.to_json
|
|
202
|
+
else
|
|
203
|
+
collection_report.text_summary
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Write to file or stdout
|
|
207
|
+
if @output
|
|
208
|
+
File.write(@output, output)
|
|
209
|
+
puts "Validation report written to #{@output}" if @verbose && !@suppress_warnings
|
|
210
|
+
else
|
|
211
|
+
puts output unless @suppress_warnings
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Return exit code based on worst status
|
|
215
|
+
collection_exit_code(collection_report)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Extract font name from font object
|
|
219
|
+
#
|
|
220
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font object
|
|
221
|
+
# @param index [Integer] Font index (fallback)
|
|
222
|
+
# @return [String] Font name
|
|
223
|
+
def extract_font_name(font, index)
|
|
224
|
+
return "Font #{index}" unless font.respond_to?(:table)
|
|
225
|
+
|
|
226
|
+
name_table = font.table("name")
|
|
227
|
+
return "Font #{index}" unless name_table
|
|
228
|
+
|
|
229
|
+
full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
230
|
+
return full_name if full_name && !full_name.empty?
|
|
231
|
+
|
|
232
|
+
postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
233
|
+
return postscript_name if postscript_name && !postscript_name.empty?
|
|
234
|
+
|
|
235
|
+
"Font #{index}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Calculate exit code for collection validation
|
|
239
|
+
#
|
|
240
|
+
# Uses worst status across all fonts
|
|
241
|
+
#
|
|
242
|
+
# @param report [CollectionValidationReport] Collection report
|
|
243
|
+
# @return [Integer] Exit code
|
|
244
|
+
def collection_exit_code(report)
|
|
245
|
+
return 0 unless @return_value_results
|
|
246
|
+
|
|
247
|
+
# Check for fatal errors first
|
|
248
|
+
return 2 if report.font_reports.any? do |fr|
|
|
249
|
+
fr.report.fatal_errors.any?
|
|
250
|
+
end
|
|
251
|
+
# Then check for errors
|
|
252
|
+
return 3 if report.font_reports.any? { |fr| fr.report.errors_only.any? }
|
|
253
|
+
# Then check for warnings
|
|
254
|
+
return 4 if report.font_reports.any? do |fr|
|
|
255
|
+
fr.report.warnings_only.any?
|
|
256
|
+
end
|
|
257
|
+
# Then check for info
|
|
258
|
+
return 5 if report.font_reports.any? { |fr| fr.report.info_only.any? }
|
|
259
|
+
|
|
260
|
+
0
|
|
261
|
+
end
|
|
112
262
|
|
|
113
263
|
# Generate output based on requested format
|
|
114
264
|
#
|
|
@@ -155,6 +305,7 @@ module Fontisan
|
|
|
155
305
|
return 3 if report.errors_only.any?
|
|
156
306
|
return 4 if report.warnings_only.any?
|
|
157
307
|
return 5 if report.info_only.any?
|
|
308
|
+
|
|
158
309
|
0
|
|
159
310
|
end
|
|
160
311
|
end
|
|
@@ -59,11 +59,12 @@ module Fontisan
|
|
|
59
59
|
|
|
60
60
|
verbose = options.fetch(:verbose, false)
|
|
61
61
|
output_path = options[:output]
|
|
62
|
-
target_format = options.fetch(:target_format,
|
|
62
|
+
target_format = options.fetch(:target_format, "preserve").to_s
|
|
63
63
|
|
|
64
64
|
# Validate target_format
|
|
65
65
|
unless %w[preserve ttf otf].include?(target_format)
|
|
66
|
-
raise ArgumentError,
|
|
66
|
+
raise ArgumentError,
|
|
67
|
+
"Invalid target_format: #{target_format}. Must be 'preserve', 'ttf', or 'otf'"
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
puts "Converting collection to #{target_type.to_s.upcase}..." if verbose
|
|
@@ -76,19 +77,22 @@ module Fontisan
|
|
|
76
77
|
if source_type == target_type
|
|
77
78
|
puts " Source and target formats are the same, copying collection..." if verbose
|
|
78
79
|
FileUtils.cp(collection_path, output_path)
|
|
79
|
-
return build_result(collection_path, output_path, source_type,
|
|
80
|
+
return build_result(collection_path, output_path, source_type,
|
|
81
|
+
target_type, fonts.size, [])
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
# Step 2: Convert - transform fonts if requested
|
|
83
85
|
puts " Converting #{fonts.size} font(s)..." if verbose
|
|
84
|
-
converted_fonts, conversions = convert_fonts(fonts, source_type,
|
|
86
|
+
converted_fonts, conversions = convert_fonts(fonts, source_type,
|
|
87
|
+
target_type, options.merge(target_format: target_format))
|
|
85
88
|
|
|
86
89
|
# Step 3: Repack - build target collection
|
|
87
90
|
puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
|
|
88
91
|
repack_fonts(converted_fonts, target_type, output_path, options)
|
|
89
92
|
|
|
90
93
|
# Build result
|
|
91
|
-
result = build_result(collection_path, output_path, source_type,
|
|
94
|
+
result = build_result(collection_path, output_path, source_type,
|
|
95
|
+
target_type, fonts.size, conversions)
|
|
92
96
|
|
|
93
97
|
if verbose
|
|
94
98
|
display_result(result)
|
|
@@ -111,7 +115,8 @@ module Fontisan
|
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
unless %i[ttc otc dfont].include?(target_type)
|
|
114
|
-
raise ArgumentError,
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"Invalid target type: #{target_type}. Must be :ttc, :otc, or :dfont"
|
|
115
120
|
end
|
|
116
121
|
|
|
117
122
|
unless options[:output]
|
|
@@ -202,20 +207,21 @@ module Fontisan
|
|
|
202
207
|
# @param target_type [Symbol] Target collection type
|
|
203
208
|
# @param options [Hash] Conversion options
|
|
204
209
|
# @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
|
|
205
|
-
def convert_fonts(fonts,
|
|
210
|
+
def convert_fonts(fonts, _source_type, target_type, options)
|
|
206
211
|
converted_fonts = []
|
|
207
212
|
conversions = []
|
|
208
213
|
|
|
209
214
|
# Determine if outline conversion is needed
|
|
210
|
-
target_format = options.fetch(:target_format,
|
|
215
|
+
target_format = options.fetch(:target_format, "preserve").to_s
|
|
211
216
|
|
|
212
217
|
fonts.each_with_index do |font, index|
|
|
213
218
|
source_format = detect_font_format(font)
|
|
214
|
-
needs_conversion = outline_conversion_needed?(source_format,
|
|
219
|
+
needs_conversion = outline_conversion_needed?(source_format,
|
|
220
|
+
target_format)
|
|
215
221
|
|
|
216
222
|
if needs_conversion
|
|
217
223
|
# Convert outline format
|
|
218
|
-
desired_format = target_format ==
|
|
224
|
+
desired_format = target_format == "preserve" ? source_format : target_format.to_sym
|
|
219
225
|
converter = FormatConverter.new
|
|
220
226
|
|
|
221
227
|
begin
|
|
@@ -266,7 +272,7 @@ module Fontisan
|
|
|
266
272
|
# @return [Boolean] true if conversion needed
|
|
267
273
|
def outline_conversion_needed?(source_format, target_format)
|
|
268
274
|
# 'preserve' means keep original format
|
|
269
|
-
return false if target_format ==
|
|
275
|
+
return false if target_format == "preserve"
|
|
270
276
|
|
|
271
277
|
# Convert if target format differs from source
|
|
272
278
|
target_format.to_sym != source_format
|
|
@@ -401,7 +407,8 @@ module Fontisan
|
|
|
401
407
|
# @param num_fonts [Integer] Number of fonts
|
|
402
408
|
# @param conversions [Array<Hash>] Conversion details
|
|
403
409
|
# @return [Hash] Result
|
|
404
|
-
def build_result(input, output, source_type, target_type, num_fonts,
|
|
410
|
+
def build_result(input, output, source_type, target_type, num_fonts,
|
|
411
|
+
conversions)
|
|
405
412
|
{
|
|
406
413
|
input: input,
|
|
407
414
|
output: output,
|
|
@@ -425,7 +432,9 @@ module Fontisan
|
|
|
425
432
|
puts "Fonts: #{result[:num_fonts]}"
|
|
426
433
|
|
|
427
434
|
if result[:conversions].any?
|
|
428
|
-
converted_count = result[:conversions].count
|
|
435
|
+
converted_count = result[:conversions].count do |c|
|
|
436
|
+
c[:status] == :converted
|
|
437
|
+
end
|
|
429
438
|
if converted_count.positive?
|
|
430
439
|
puts "Outline conversions: #{converted_count}"
|
|
431
440
|
end
|
|
@@ -107,7 +107,8 @@ module Fontisan
|
|
|
107
107
|
# @param extractor [OutlineExtractor] Outline extractor for layer glyphs
|
|
108
108
|
# @param palette_index [Integer] Palette index to use (default: 0)
|
|
109
109
|
# @return [String, nil] SVG path data with color layers, or nil if not color glyph
|
|
110
|
-
def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor,
|
|
110
|
+
def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor,
|
|
111
|
+
palette_index: 0)
|
|
111
112
|
# Get layers for this glyph
|
|
112
113
|
layers = colr_table.layers_for_glyph(glyph_id)
|
|
113
114
|
return nil if layers.empty?
|
|
@@ -109,7 +109,7 @@ module Fontisan
|
|
|
109
109
|
woff2_binary = assemble_woff2(header, entries, compressed_data)
|
|
110
110
|
|
|
111
111
|
# Prepare result
|
|
112
|
-
|
|
112
|
+
{ woff2_binary: woff2_binary }
|
|
113
113
|
|
|
114
114
|
# Optional validation
|
|
115
115
|
# Temporarily disabled - will be reimplemented with new DSL framework
|
|
@@ -117,8 +117,6 @@ module Fontisan
|
|
|
117
117
|
# validation_report = validate_encoding(woff2_binary, options)
|
|
118
118
|
# result[:validation_report] = validation_report
|
|
119
119
|
# end
|
|
120
|
-
|
|
121
|
-
result
|
|
122
120
|
end
|
|
123
121
|
|
|
124
122
|
# Get list of supported conversions
|
|
@@ -185,14 +183,16 @@ module Fontisan
|
|
|
185
183
|
end
|
|
186
184
|
|
|
187
185
|
# Read table directory
|
|
188
|
-
woff2.table_entries = Woff2Font.read_table_directory_from_io(io,
|
|
186
|
+
woff2.table_entries = Woff2Font.read_table_directory_from_io(io,
|
|
187
|
+
woff2.header)
|
|
189
188
|
|
|
190
189
|
# Decompress tables
|
|
191
190
|
woff2.decompressed_tables = Woff2Font.decompress_tables(io, woff2.header,
|
|
192
|
-
|
|
191
|
+
woff2.table_entries)
|
|
193
192
|
|
|
194
193
|
# Apply transformations
|
|
195
|
-
Woff2Font.apply_transformations!(woff2.table_entries,
|
|
194
|
+
Woff2Font.apply_transformations!(woff2.table_entries,
|
|
195
|
+
woff2.decompressed_tables)
|
|
196
196
|
|
|
197
197
|
woff2
|
|
198
198
|
end
|
|
@@ -369,9 +369,11 @@ module Fontisan
|
|
|
369
369
|
# Sort tables by tag for consistent output (same order as directory)
|
|
370
370
|
sorted_tables = compressed_tables.sort_by { |tag, _| tag }
|
|
371
371
|
|
|
372
|
-
sorted_tables
|
|
372
|
+
# rubocop:disable Style/HashEachMethods - sorted_tables is an Array, not a Hash
|
|
373
|
+
sorted_tables.each do |_, table_info|
|
|
373
374
|
io.write(table_info[:compressed_data])
|
|
374
375
|
end
|
|
376
|
+
# rubocop:enable Style/HashEachMethods
|
|
375
377
|
end
|
|
376
378
|
|
|
377
379
|
# Write metadata to output
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -72,7 +72,8 @@ module Fontisan
|
|
|
72
72
|
when "wOF2"
|
|
73
73
|
Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
74
74
|
when Constants::DFONT_RESOURCE_HEADER
|
|
75
|
-
extract_and_load_dfont(io, path, font_index, resolved_mode,
|
|
75
|
+
extract_and_load_dfont(io, path, font_index, resolved_mode,
|
|
76
|
+
resolved_lazy)
|
|
76
77
|
else
|
|
77
78
|
raise InvalidFontError,
|
|
78
79
|
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
|
|
@@ -185,7 +186,7 @@ module Fontisan
|
|
|
185
186
|
num_fonts = io.read(4).unpack1("N")
|
|
186
187
|
|
|
187
188
|
# Read all font offsets
|
|
188
|
-
font_offsets = num_fonts
|
|
189
|
+
font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
|
|
189
190
|
|
|
190
191
|
# Scan all fonts to determine collection type (not just first)
|
|
191
192
|
truetype_count = 0
|
|
@@ -212,7 +213,7 @@ module Fontisan
|
|
|
212
213
|
# Determine collection type based on what fonts are inside
|
|
213
214
|
# If ANY font is OpenType, use OpenTypeCollection (more general format)
|
|
214
215
|
# Only use TrueTypeCollection if ALL fonts are TrueType
|
|
215
|
-
if opentype_count
|
|
216
|
+
if opentype_count.positive?
|
|
216
217
|
OpenTypeCollection.from_file(path)
|
|
217
218
|
else
|
|
218
219
|
# All fonts are TrueType
|
|
@@ -282,7 +283,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
282
283
|
end
|
|
283
284
|
|
|
284
285
|
# Read all font offsets
|
|
285
|
-
font_offsets = num_fonts
|
|
286
|
+
font_offsets = Array.new(num_fonts) { io.read(4).unpack1("N") }
|
|
286
287
|
|
|
287
288
|
# Scan all fonts to determine collection type (not just first)
|
|
288
289
|
truetype_count = 0
|
|
@@ -308,7 +309,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
308
309
|
|
|
309
310
|
# If ANY font is OpenType, use OpenTypeCollection (more general format)
|
|
310
311
|
# Only use TrueTypeCollection if ALL fonts are TrueType
|
|
311
|
-
if opentype_count
|
|
312
|
+
if opentype_count.positive?
|
|
312
313
|
# OpenType Collection
|
|
313
314
|
otc = OpenTypeCollection.from_file(path)
|
|
314
315
|
File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
|
|
@@ -328,7 +329,7 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
328
329
|
# @param lazy [Boolean] Lazy loading flag
|
|
329
330
|
# @return [TrueTypeFont, OpenTypeFont] Loaded font
|
|
330
331
|
# @api private
|
|
331
|
-
def self.extract_and_load_dfont(io,
|
|
332
|
+
def self.extract_and_load_dfont(io, _path, font_index, mode, lazy)
|
|
332
333
|
require_relative "parsers/dfont_parser"
|
|
333
334
|
|
|
334
335
|
# Extract SFNT data from resource fork
|
|
@@ -410,15 +410,16 @@ module Fontisan
|
|
|
410
410
|
|
|
411
411
|
info.fonts.each_with_index do |font_info, index|
|
|
412
412
|
# Show font index with offset
|
|
413
|
-
if font_info.collection_offset
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
413
|
+
lines << if font_info.collection_offset
|
|
414
|
+
"Font #{index} (offset: #{font_info.collection_offset}):"
|
|
415
|
+
else
|
|
416
|
+
"Font #{index}:"
|
|
417
|
+
end
|
|
418
418
|
lines << ""
|
|
419
419
|
|
|
420
420
|
# Format each font using same structure as brief mode
|
|
421
|
-
font_type_display = format_font_type_display(font_info.font_format,
|
|
421
|
+
font_type_display = format_font_type_display(font_info.font_format,
|
|
422
|
+
font_info.is_variable)
|
|
422
423
|
add_line(lines, "Font type", font_type_display)
|
|
423
424
|
add_line(lines, "Family", font_info.family_name)
|
|
424
425
|
add_line(lines, "Subfamily", font_info.subfamily_name)
|
|
@@ -426,7 +427,8 @@ module Fontisan
|
|
|
426
427
|
add_line(lines, "PostScript name", font_info.postscript_name)
|
|
427
428
|
add_line(lines, "Version", font_info.version)
|
|
428
429
|
add_line(lines, "Vendor ID", font_info.vendor_id)
|
|
429
|
-
add_line(lines, "Font revision",
|
|
430
|
+
add_line(lines, "Font revision",
|
|
431
|
+
format_float(font_info.font_revision))
|
|
430
432
|
add_line(lines, "Units per em", font_info.units_per_em)
|
|
431
433
|
|
|
432
434
|
# Blank line between fonts (except after last)
|
|
@@ -467,15 +469,16 @@ module Fontisan
|
|
|
467
469
|
# Each font's brief info
|
|
468
470
|
info.fonts.each_with_index do |font_info, index|
|
|
469
471
|
# Show font index with offset
|
|
470
|
-
if font_info.collection_offset
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
472
|
+
lines << if font_info.collection_offset
|
|
473
|
+
"Font #{index} (offset: #{font_info.collection_offset}):"
|
|
474
|
+
else
|
|
475
|
+
"Font #{index}:"
|
|
476
|
+
end
|
|
475
477
|
lines << ""
|
|
476
478
|
|
|
477
479
|
# Format each font using same structure as individual fonts
|
|
478
|
-
font_type_display = format_font_type_display(font_info.font_format,
|
|
480
|
+
font_type_display = format_font_type_display(font_info.font_format,
|
|
481
|
+
font_info.is_variable)
|
|
479
482
|
add_line(lines, "Font type", font_type_display)
|
|
480
483
|
add_line(lines, "Family", font_info.family_name)
|
|
481
484
|
add_line(lines, "Subfamily", font_info.subfamily_name)
|
|
@@ -483,7 +486,8 @@ module Fontisan
|
|
|
483
486
|
add_line(lines, "PostScript name", font_info.postscript_name)
|
|
484
487
|
add_line(lines, "Version", font_info.version)
|
|
485
488
|
add_line(lines, "Vendor ID", font_info.vendor_id)
|
|
486
|
-
add_line(lines, "Font revision",
|
|
489
|
+
add_line(lines, "Font revision",
|
|
490
|
+
format_float(font_info.font_revision))
|
|
487
491
|
add_line(lines, "Units per em", font_info.units_per_em)
|
|
488
492
|
|
|
489
493
|
# Blank line between fonts (except after last)
|
|
@@ -244,7 +244,7 @@ module Fontisan
|
|
|
244
244
|
# CVT values typically contain standard widths at the beginning
|
|
245
245
|
if cvt && !cvt.empty?
|
|
246
246
|
# First CVT value often represents standard horizontal stem
|
|
247
|
-
hints[:std_hw] = cvt[0].abs if cvt.length
|
|
247
|
+
hints[:std_hw] = cvt[0].abs if cvt.length.positive?
|
|
248
248
|
# Second CVT value often represents standard vertical stem
|
|
249
249
|
hints[:std_vw] = cvt[1].abs if cvt.length > 1
|
|
250
250
|
end
|
|
@@ -40,7 +40,10 @@ module Fontisan
|
|
|
40
40
|
# @param instructions [String] Binary instruction bytes
|
|
41
41
|
# @return [Hash] Validation result with :valid, :errors, :warnings keys
|
|
42
42
|
def validate_truetype_instructions(instructions)
|
|
43
|
-
|
|
43
|
+
if instructions.nil? || instructions.empty?
|
|
44
|
+
return { valid: true, errors: [],
|
|
45
|
+
warnings: [] }
|
|
46
|
+
end
|
|
44
47
|
|
|
45
48
|
errors = []
|
|
46
49
|
warnings = []
|
|
@@ -114,7 +117,6 @@ module Fontisan
|
|
|
114
117
|
if stack_depth != 0
|
|
115
118
|
warnings << "Stack not neutral: #{stack_depth} value(s) remaining"
|
|
116
119
|
end
|
|
117
|
-
|
|
118
120
|
rescue StandardError => e
|
|
119
121
|
errors << "Exception during validation: #{e.message}"
|
|
120
122
|
end
|
|
@@ -162,14 +164,14 @@ module Fontisan
|
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
# Validate stem widths
|
|
165
|
-
[
|
|
167
|
+
%i[std_hw std_vw].each do |key|
|
|
166
168
|
if hints[key] && hints[key] <= 0
|
|
167
169
|
errors << "#{key} must be positive: #{hints[key]}"
|
|
168
170
|
end
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
# Validate stem snaps
|
|
172
|
-
[
|
|
174
|
+
%i[stem_snap_h stem_snap_v].each do |key|
|
|
173
175
|
if hints[key]
|
|
174
176
|
if hints[key].length > MAX_STEM_SNAP
|
|
175
177
|
errors << "#{key} exceeds maximum (#{MAX_STEM_SNAP}): #{hints[key].length}"
|
|
@@ -191,10 +193,8 @@ module Fontisan
|
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
# Validate language_group
|
|
194
|
-
if hints[:language_group]
|
|
195
|
-
|
|
196
|
-
errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
|
|
197
|
-
end
|
|
196
|
+
if hints[:language_group] && ![0, 1].include?(hints[:language_group])
|
|
197
|
+
errors << "language_group must be 0 (Latin) or 1 (CJK): #{hints[:language_group]}"
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
{
|
|
@@ -212,7 +212,10 @@ module Fontisan
|
|
|
212
212
|
# @param instructions [String] Binary instruction bytes
|
|
213
213
|
# @return [Hash] Result with :neutral, :stack_depth, :errors keys
|
|
214
214
|
def validate_stack_neutrality(instructions)
|
|
215
|
-
|
|
215
|
+
if instructions.nil? || instructions.empty?
|
|
216
|
+
return { neutral: true, stack_depth: 0,
|
|
217
|
+
errors: [] }
|
|
218
|
+
end
|
|
216
219
|
|
|
217
220
|
errors = []
|
|
218
221
|
stack_depth = 0
|
|
@@ -257,7 +260,7 @@ module Fontisan
|
|
|
257
260
|
end
|
|
258
261
|
|
|
259
262
|
{
|
|
260
|
-
neutral: stack_depth
|
|
263
|
+
neutral: stack_depth.zero?,
|
|
261
264
|
stack_depth: stack_depth,
|
|
262
265
|
errors: errors,
|
|
263
266
|
}
|
|
@@ -29,7 +29,7 @@ module Fontisan
|
|
|
29
29
|
RCVT = 0x45 # Read CVT
|
|
30
30
|
WCVTP = 0x44 # Write CVT (in Pixels)
|
|
31
31
|
WCVTF = 0x70 # Write CVT (in FUnits)
|
|
32
|
-
MDAP = [0x2E, 0x2F].freeze
|
|
32
|
+
MDAP = [0x2E, 0x2F].freeze # Move Direct Absolute Point
|
|
33
33
|
SCVTCI = 0x1D # Set Control Value Table Cut In
|
|
34
34
|
SSWCI = 0x1E # Set Single Width Cut In
|
|
35
35
|
SSW = 0x1F # Set Single Width
|
|
@@ -106,7 +106,7 @@ module Fontisan
|
|
|
106
106
|
# Pattern: value cvt_index WCVTP (stack top to bottom)
|
|
107
107
|
if stack.length >= 2
|
|
108
108
|
value = stack.pop
|
|
109
|
-
|
|
109
|
+
stack.pop
|
|
110
110
|
# Track CVT modifications (useful for understanding setup)
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -168,7 +168,7 @@ module Fontisan
|
|
|
168
168
|
|
|
169
169
|
{
|
|
170
170
|
fpgm_size: size,
|
|
171
|
-
has_functions: size
|
|
171
|
+
has_functions: size.positive?,
|
|
172
172
|
complexity: complexity,
|
|
173
173
|
}
|
|
174
174
|
rescue StandardError
|
|
@@ -208,13 +208,19 @@ module Fontisan
|
|
|
208
208
|
xheight_min = (450 * scale_factor).to_i
|
|
209
209
|
xheight_max = (600 * scale_factor).to_i
|
|
210
210
|
capheight_min = (650 * scale_factor).to_i
|
|
211
|
-
capheight_max = (1500 * scale_factor).to_i
|
|
211
|
+
capheight_max = (1500 * scale_factor).to_i # Wider range for large UPM
|
|
212
212
|
|
|
213
213
|
# Group CVT values by typical alignment zones
|
|
214
|
-
descender_values = cvt.select
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
descender_values = cvt.select do |v|
|
|
215
|
+
v < descender_max && v > descender_min
|
|
216
|
+
end
|
|
217
|
+
baseline_values = cvt.select do |v|
|
|
218
|
+
v >= -baseline_range && v <= baseline_range
|
|
219
|
+
end
|
|
220
|
+
cvt.select { |v| v >= xheight_min && v <= xheight_max }
|
|
221
|
+
capheight_values = cvt.select do |v|
|
|
222
|
+
v >= capheight_min && v <= capheight_max
|
|
223
|
+
end
|
|
218
224
|
|
|
219
225
|
# Build blue_values (baseline and top zones)
|
|
220
226
|
blue_values = []
|
|
@@ -254,6 +260,7 @@ module Fontisan
|
|
|
254
260
|
def estimate_complexity(bytes)
|
|
255
261
|
return :simple if bytes.length < 50
|
|
256
262
|
return :moderate if bytes.length < 200
|
|
263
|
+
|
|
257
264
|
:complex
|
|
258
265
|
end
|
|
259
266
|
end
|