fontisan 0.2.0 → 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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- 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 +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- 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 +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- 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/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- 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/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "base_command"
|
|
5
|
-
require_relative "../
|
|
5
|
+
require_relative "../variation/instance_generator"
|
|
6
|
+
require_relative "../variation/instance_writer"
|
|
6
7
|
require_relative "../variation/validator"
|
|
7
|
-
require_relative "../variation/parallel_generator"
|
|
8
|
-
require_relative "../converters/format_converter"
|
|
9
8
|
require_relative "../error"
|
|
10
9
|
|
|
11
10
|
module Fontisan
|
|
@@ -20,19 +19,18 @@ module Fontisan
|
|
|
20
19
|
# - Validation before generation
|
|
21
20
|
# - Dry-run mode for previewing
|
|
22
21
|
# - Progress tracking
|
|
23
|
-
# - Parallel batch generation
|
|
24
22
|
#
|
|
25
23
|
# @example Instance at coordinates
|
|
26
24
|
# fontisan instance variable.ttf --wght=700 --output=bold.ttf
|
|
27
25
|
#
|
|
26
|
+
# @example Instance with format conversion
|
|
27
|
+
# fontisan instance variable.ttf --wght=700 --to=otf --output=bold.otf
|
|
28
|
+
#
|
|
28
29
|
# @example Instance with validation
|
|
29
30
|
# fontisan instance variable.ttf --wght=700 --validate --output=bold.ttf
|
|
30
31
|
#
|
|
31
32
|
# @example Dry-run to preview
|
|
32
33
|
# fontisan instance variable.ttf --wght=700 --dry-run
|
|
33
|
-
#
|
|
34
|
-
# @example Instance with progress
|
|
35
|
-
# fontisan instance variable.ttf --wght=700 --progress --output=bold.ttf
|
|
36
34
|
class InstanceCommand < BaseCommand
|
|
37
35
|
# Instance a variable font at specified coordinates
|
|
38
36
|
#
|
|
@@ -44,18 +42,15 @@ module Fontisan
|
|
|
44
42
|
# Validate font if requested
|
|
45
43
|
validate_font(font) if options[:validate]
|
|
46
44
|
|
|
47
|
-
# Create instancer
|
|
48
|
-
instancer = Variable::Instancer.new(font)
|
|
49
|
-
|
|
50
45
|
# Handle list-instances option
|
|
51
46
|
if options[:list_instances]
|
|
52
|
-
list_instances(
|
|
47
|
+
list_instances(font)
|
|
53
48
|
return
|
|
54
49
|
end
|
|
55
50
|
|
|
56
51
|
# Handle dry-run mode
|
|
57
52
|
if options[:dry_run]
|
|
58
|
-
preview_instance(
|
|
53
|
+
preview_instance(font, input_path, options)
|
|
59
54
|
return
|
|
60
55
|
end
|
|
61
56
|
|
|
@@ -64,20 +59,19 @@ module Fontisan
|
|
|
64
59
|
|
|
65
60
|
# Generate instance
|
|
66
61
|
if options[:named_instance]
|
|
67
|
-
instance_named(
|
|
68
|
-
options)
|
|
62
|
+
instance_named(font, options[:named_instance], output_path, options)
|
|
69
63
|
else
|
|
70
|
-
instance_coords(
|
|
64
|
+
instance_coords(font, extract_coordinates(options), output_path,
|
|
71
65
|
options)
|
|
72
66
|
end
|
|
73
67
|
|
|
74
68
|
puts "Static font instance written to: #{output_path}"
|
|
75
69
|
rescue VariationError => e
|
|
76
|
-
|
|
70
|
+
warn "Variation Error: #{e.detailed_message}"
|
|
77
71
|
exit 1
|
|
78
72
|
rescue StandardError => e
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
warn "Error: #{e.message}"
|
|
74
|
+
warn e.backtrace.first(5).join("\n") if options[:verbose]
|
|
81
75
|
exit 1
|
|
82
76
|
end
|
|
83
77
|
|
|
@@ -93,9 +87,9 @@ module Fontisan
|
|
|
93
87
|
errors = validator.validate
|
|
94
88
|
|
|
95
89
|
if errors.any?
|
|
96
|
-
|
|
90
|
+
warn "Validation errors found:"
|
|
97
91
|
errors.each do |error|
|
|
98
|
-
|
|
92
|
+
warn " - #{error}"
|
|
99
93
|
end
|
|
100
94
|
exit 1
|
|
101
95
|
end
|
|
@@ -105,9 +99,10 @@ module Fontisan
|
|
|
105
99
|
|
|
106
100
|
# Preview instance without generating
|
|
107
101
|
#
|
|
108
|
-
# @param
|
|
102
|
+
# @param font [Object] Font object
|
|
103
|
+
# @param input_path [String] Input file path
|
|
109
104
|
# @param options [Hash] Command options
|
|
110
|
-
def preview_instance(
|
|
105
|
+
def preview_instance(_font, input_path, options)
|
|
111
106
|
coords = extract_coordinates(options)
|
|
112
107
|
|
|
113
108
|
if coords.empty?
|
|
@@ -122,18 +117,20 @@ module Fontisan
|
|
|
122
117
|
puts " #{axis}: #{value}"
|
|
123
118
|
end
|
|
124
119
|
puts
|
|
125
|
-
puts "Output would be written to: #{determine_output_path(
|
|
120
|
+
puts "Output would be written to: #{determine_output_path(input_path,
|
|
121
|
+
options)}"
|
|
122
|
+
puts "Output format: #{options[:to] || 'same as input'}"
|
|
126
123
|
puts
|
|
127
124
|
puts "Use without --dry-run to actually generate the instance."
|
|
128
125
|
end
|
|
129
126
|
|
|
130
127
|
# Instance at specific coordinates
|
|
131
128
|
#
|
|
132
|
-
# @param
|
|
129
|
+
# @param font [Object] Font object
|
|
133
130
|
# @param coords [Hash] User coordinates
|
|
134
131
|
# @param output_path [String] Output file path
|
|
135
132
|
# @param options [Hash] Command options
|
|
136
|
-
def instance_coords(
|
|
133
|
+
def instance_coords(font, coords, output_path, options)
|
|
137
134
|
if coords.empty?
|
|
138
135
|
raise ArgumentError,
|
|
139
136
|
"No coordinates specified. Use --wght=700, --wdth=100, etc."
|
|
@@ -142,47 +139,64 @@ module Fontisan
|
|
|
142
139
|
# Show progress if requested
|
|
143
140
|
print "Generating instance..." if options[:progress]
|
|
144
141
|
|
|
145
|
-
# Generate instance
|
|
146
|
-
|
|
142
|
+
# Generate instance tables using InstanceGenerator
|
|
143
|
+
generator = Variation::InstanceGenerator.new(font, coords)
|
|
144
|
+
tables = generator.generate
|
|
147
145
|
|
|
148
146
|
puts " done" if options[:progress]
|
|
149
147
|
|
|
150
|
-
#
|
|
151
|
-
if options[:to]
|
|
152
|
-
print "Converting format..." if options[:progress]
|
|
153
|
-
binary = convert_format(binary, options)
|
|
154
|
-
puts " done" if options[:progress]
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Write to file
|
|
148
|
+
# Write instance using InstanceWriter
|
|
158
149
|
print "Writing output..." if options[:progress]
|
|
159
|
-
|
|
150
|
+
|
|
151
|
+
# Detect source format for conversion
|
|
152
|
+
source_format = detect_source_format(font)
|
|
153
|
+
|
|
154
|
+
Variation::InstanceWriter.write(
|
|
155
|
+
tables,
|
|
156
|
+
output_path,
|
|
157
|
+
format: options[:to]&.to_sym,
|
|
158
|
+
source_format: source_format,
|
|
159
|
+
optimize: options[:optimize] || false,
|
|
160
|
+
)
|
|
161
|
+
|
|
160
162
|
puts " done" if options[:progress]
|
|
161
163
|
end
|
|
162
164
|
|
|
163
165
|
# Instance using named instance
|
|
164
166
|
#
|
|
165
|
-
# @param
|
|
166
|
-
# @param
|
|
167
|
+
# @param font [Object] Font object
|
|
168
|
+
# @param instance_index [Integer] Named instance index
|
|
167
169
|
# @param output_path [String] Output file path
|
|
168
170
|
# @param options [Hash] Command options
|
|
169
|
-
def instance_named(
|
|
170
|
-
# Generate instance
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
def instance_named(font, instance_index, output_path, options)
|
|
172
|
+
# Generate instance using named instance
|
|
173
|
+
generator = Variation::InstanceGenerator.new(font)
|
|
174
|
+
tables = generator.generate_named_instance(instance_index)
|
|
175
|
+
|
|
176
|
+
# Detect source format
|
|
177
|
+
source_format = detect_source_format(font)
|
|
178
|
+
|
|
179
|
+
# Write instance
|
|
180
|
+
Variation::InstanceWriter.write(
|
|
181
|
+
tables,
|
|
182
|
+
output_path,
|
|
183
|
+
format: options[:to]&.to_sym,
|
|
184
|
+
source_format: source_format,
|
|
185
|
+
optimize: options[:optimize] || false,
|
|
186
|
+
)
|
|
178
187
|
end
|
|
179
188
|
|
|
180
189
|
# List available named instances
|
|
181
190
|
#
|
|
182
|
-
# @param
|
|
183
|
-
def list_instances(
|
|
184
|
-
|
|
191
|
+
# @param font [Object] Font object
|
|
192
|
+
def list_instances(font)
|
|
193
|
+
fvar = font.table("fvar")
|
|
194
|
+
unless fvar
|
|
195
|
+
puts "Not a variable font - no named instances available."
|
|
196
|
+
return
|
|
197
|
+
end
|
|
185
198
|
|
|
199
|
+
instances = fvar.instances
|
|
186
200
|
if instances.empty?
|
|
187
201
|
puts "No named instances defined in font."
|
|
188
202
|
return
|
|
@@ -191,11 +205,15 @@ module Fontisan
|
|
|
191
205
|
puts "Available named instances:"
|
|
192
206
|
puts
|
|
193
207
|
|
|
194
|
-
instances.
|
|
195
|
-
|
|
208
|
+
instances.each_with_index do |instance, index|
|
|
209
|
+
name_id = instance[:subfamily_name_id]
|
|
210
|
+
puts " [#{index}] Instance #{name_id}"
|
|
196
211
|
puts " Coordinates:"
|
|
197
|
-
instance[:coordinates].
|
|
198
|
-
|
|
212
|
+
instance[:coordinates].each_with_index do |value, axis_index|
|
|
213
|
+
next if axis_index >= fvar.axes.length
|
|
214
|
+
|
|
215
|
+
axis = fvar.axes[axis_index]
|
|
216
|
+
puts " #{axis.axis_tag}: #{value}"
|
|
199
217
|
end
|
|
200
218
|
puts
|
|
201
219
|
end
|
|
@@ -243,38 +261,12 @@ module Fontisan
|
|
|
243
261
|
"#{dir}/#{base}-instance.#{ext}"
|
|
244
262
|
end
|
|
245
263
|
|
|
246
|
-
#
|
|
264
|
+
# Detect source format from font
|
|
247
265
|
#
|
|
248
|
-
# @param
|
|
249
|
-
# @
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
target_format = options[:to].to_sym
|
|
253
|
-
|
|
254
|
-
# Load font from binary
|
|
255
|
-
require "tempfile"
|
|
256
|
-
Tempfile.create(["instance", ".ttf"]) do |temp_file|
|
|
257
|
-
temp_file.binmode
|
|
258
|
-
temp_file.write(binary)
|
|
259
|
-
temp_file.flush
|
|
260
|
-
|
|
261
|
-
font = FontLoader.load(temp_file.path)
|
|
262
|
-
converter = Converters::FormatConverter.new
|
|
263
|
-
|
|
264
|
-
result = converter.convert(font, target_format)
|
|
265
|
-
|
|
266
|
-
case target_format
|
|
267
|
-
when :woff, :woff2
|
|
268
|
-
result[:font_data]
|
|
269
|
-
when :svg
|
|
270
|
-
result[:svg_xml]
|
|
271
|
-
else
|
|
272
|
-
binary
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
rescue StandardError => e
|
|
276
|
-
warn "Format conversion failed: #{e.message}"
|
|
277
|
-
binary
|
|
266
|
+
# @param font [Object] Font object
|
|
267
|
+
# @return [Symbol] Source format (:ttf or :otf)
|
|
268
|
+
def detect_source_format(font)
|
|
269
|
+
font.has_table?("CFF ") || font.has_table?("CFF2") ? :otf : :ttf
|
|
278
270
|
end
|
|
279
271
|
|
|
280
272
|
# Load font from file
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base_command"
|
|
4
4
|
require_relative "../validation/validator"
|
|
5
|
+
require_relative "../validation/variable_font_validator"
|
|
5
6
|
require_relative "../font_loader"
|
|
6
7
|
|
|
7
8
|
module Fontisan
|
|
@@ -53,6 +54,9 @@ quiet: false)
|
|
|
53
54
|
# Run validation
|
|
54
55
|
report = validator.validate(font, @input)
|
|
55
56
|
|
|
57
|
+
# Add variable font validation if applicable
|
|
58
|
+
validate_variable_font(font, report) if font.has_table?("fvar")
|
|
59
|
+
|
|
56
60
|
# Output results unless quiet mode
|
|
57
61
|
output_report(report) unless @quiet
|
|
58
62
|
|
|
@@ -66,6 +70,30 @@ quiet: false)
|
|
|
66
70
|
|
|
67
71
|
private
|
|
68
72
|
|
|
73
|
+
# Validate variable font structure
|
|
74
|
+
#
|
|
75
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
76
|
+
# @param report [Models::ValidationReport] The validation report to update
|
|
77
|
+
# @return [void]
|
|
78
|
+
def validate_variable_font(font, report)
|
|
79
|
+
var_validator = Validation::VariableFontValidator.new(font)
|
|
80
|
+
errors = var_validator.validate
|
|
81
|
+
|
|
82
|
+
if errors.any?
|
|
83
|
+
puts "\nVariable font validation:" if @verbose && !@quiet
|
|
84
|
+
errors.each do |error|
|
|
85
|
+
puts " ERROR: #{error}" if @verbose && !@quiet
|
|
86
|
+
# Add to report if report supports adding errors
|
|
87
|
+
if report.respond_to?(:errors)
|
|
88
|
+
report.errors << { message: error,
|
|
89
|
+
category: "variable_font" }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
elsif @verbose && !@quiet
|
|
93
|
+
puts "\n✓ Variable font structure valid"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
69
97
|
# Validate command parameters
|
|
70
98
|
#
|
|
71
99
|
# @raise [ArgumentError] if parameters are invalid
|
|
@@ -23,7 +23,7 @@ required_tables:
|
|
|
23
23
|
|
|
24
24
|
# Additional tables required for OpenType/CFF fonts (.otf)
|
|
25
25
|
opentype_cff:
|
|
26
|
-
- CFF
|
|
26
|
+
- "CFF " # or CFF2 for CFF2 format (note: CFF has trailing space per OpenType spec)
|
|
27
27
|
|
|
28
28
|
# Additional tables required for variable fonts
|
|
29
29
|
variable:
|
data/lib/fontisan/constants.rb
CHANGED
|
@@ -87,6 +87,16 @@ module Fontisan
|
|
|
87
87
|
# CFF2 table tag identifier (CFF version 2 with variations)
|
|
88
88
|
CFF2_TAG = "CFF2"
|
|
89
89
|
|
|
90
|
+
# TrueType hinting tables
|
|
91
|
+
# Font Program table (TrueType bytecode executed once at font load)
|
|
92
|
+
FPGM_TAG = "fpgm"
|
|
93
|
+
|
|
94
|
+
# Control Value Program table (TrueType bytecode for initialization)
|
|
95
|
+
PREP_TAG = "prep"
|
|
96
|
+
|
|
97
|
+
# Control Value Table (metrics used by TrueType hinting)
|
|
98
|
+
CVT_TAG = "cvt "
|
|
99
|
+
|
|
90
100
|
# Magic number used for font file checksum adjustment calculation.
|
|
91
101
|
# This constant is used in conjunction with the file checksum to compute
|
|
92
102
|
# the checksumAdjustment value stored in the 'head' table.
|
|
@@ -107,30 +117,30 @@ module Fontisan
|
|
|
107
117
|
# These strings are frozen and reused to reduce memory allocations
|
|
108
118
|
# when parsing fonts with common subfamily names.
|
|
109
119
|
STRING_POOL = {
|
|
110
|
-
"Regular" => "Regular"
|
|
111
|
-
"Bold" => "Bold"
|
|
112
|
-
"Italic" => "Italic"
|
|
113
|
-
"Bold Italic" => "Bold Italic"
|
|
114
|
-
"BoldItalic" => "BoldItalic"
|
|
115
|
-
"Light" => "Light"
|
|
116
|
-
"Medium" => "Medium"
|
|
117
|
-
"Semibold" => "Semibold"
|
|
118
|
-
"SemiBold" => "SemiBold"
|
|
119
|
-
"Black" => "Black"
|
|
120
|
-
"Thin" => "Thin"
|
|
121
|
-
"ExtraLight" => "ExtraLight"
|
|
122
|
-
"Extra Light" => "Extra Light"
|
|
123
|
-
"ExtraBold" => "ExtraBold"
|
|
124
|
-
"Extra Bold" => "Extra Bold"
|
|
125
|
-
"Heavy" => "Heavy"
|
|
126
|
-
"Book" => "Book"
|
|
127
|
-
"Roman" => "Roman"
|
|
128
|
-
"Normal" => "Normal"
|
|
129
|
-
"Oblique" => "Oblique"
|
|
130
|
-
"Light Italic" => "Light Italic"
|
|
131
|
-
"Medium Italic" => "Medium Italic"
|
|
132
|
-
"Semibold Italic" => "Semibold Italic"
|
|
133
|
-
"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",
|
|
134
144
|
}.freeze
|
|
135
145
|
|
|
136
146
|
# Intern a string using the string pool
|
|
@@ -73,20 +73,51 @@ module Fontisan
|
|
|
73
73
|
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
74
74
|
# @param target_format [Symbol] Target format (:ttf, :otf, :woff2, :svg)
|
|
75
75
|
# @param options [Hash] Additional conversion options
|
|
76
|
+
# @option options [Boolean] :preserve_variation Preserve variation data
|
|
77
|
+
# (default: true)
|
|
78
|
+
# @option options [Boolean] :preserve_hints Preserve rendering hints
|
|
79
|
+
# (default: false)
|
|
80
|
+
# @option options [Hash] :instance_coordinates Coordinates for variable→SVG
|
|
81
|
+
# @option options [Integer] :instance_index Named instance index for variable→SVG
|
|
76
82
|
# @return [Hash<String, String>] Map of table tags to binary data
|
|
77
83
|
# @raise [ArgumentError] If parameters are invalid
|
|
78
84
|
# @raise [Error] If conversion is not supported
|
|
79
85
|
#
|
|
80
86
|
# @example
|
|
81
87
|
# tables = converter.convert(font, :otf)
|
|
88
|
+
#
|
|
89
|
+
# @example Variable font to SVG at specific weight
|
|
90
|
+
# result = converter.convert(variable_font, :svg, instance_coordinates: { "wght" => 700.0 })
|
|
91
|
+
#
|
|
92
|
+
# @example Convert with hint preservation
|
|
93
|
+
# tables = converter.convert(font, :otf, preserve_hints: true)
|
|
82
94
|
def convert(font, target_format, options = {})
|
|
83
95
|
validate_parameters!(font, target_format)
|
|
84
96
|
|
|
85
97
|
source_format = detect_format(font)
|
|
86
98
|
validate_conversion_supported!(source_format, target_format)
|
|
87
99
|
|
|
100
|
+
# Special case: Variable font to SVG
|
|
101
|
+
if variable_font?(font) && target_format == :svg
|
|
102
|
+
return convert_variable_to_svg(font, options)
|
|
103
|
+
end
|
|
104
|
+
|
|
88
105
|
strategy = select_strategy(source_format, target_format)
|
|
89
|
-
strategy.convert(font,
|
|
106
|
+
tables = strategy.convert(font,
|
|
107
|
+
options.merge(target_format: target_format))
|
|
108
|
+
|
|
109
|
+
# Preserve variation data if requested and font is variable
|
|
110
|
+
if options.fetch(:preserve_variation, true) && variable_font?(font)
|
|
111
|
+
tables = preserve_variation_data(
|
|
112
|
+
font,
|
|
113
|
+
tables,
|
|
114
|
+
source_format,
|
|
115
|
+
target_format,
|
|
116
|
+
options,
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
tables
|
|
90
121
|
end
|
|
91
122
|
|
|
92
123
|
# Check if a conversion is supported
|
|
@@ -137,6 +168,128 @@ module Fontisan
|
|
|
137
168
|
|
|
138
169
|
private
|
|
139
170
|
|
|
171
|
+
# Convert variable font to SVG at specific coordinates
|
|
172
|
+
#
|
|
173
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
174
|
+
# @param options [Hash] Conversion options
|
|
175
|
+
# @option options [Hash] :instance_coordinates Design space coordinates
|
|
176
|
+
# @option options [Integer] :instance_index Named instance index
|
|
177
|
+
# @return [Hash] Hash with :svg_xml key
|
|
178
|
+
def convert_variable_to_svg(font, options = {})
|
|
179
|
+
require_relative "../variation/variable_svg_generator"
|
|
180
|
+
|
|
181
|
+
coordinates = options[:instance_coordinates] || {}
|
|
182
|
+
generator = Variation::VariableSvgGenerator.new(font, coordinates)
|
|
183
|
+
|
|
184
|
+
# Use named instance if specified
|
|
185
|
+
if options[:instance_index]
|
|
186
|
+
generator.generate_named_instance(options[:instance_index], options)
|
|
187
|
+
else
|
|
188
|
+
generator.generate(options)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if font is a variable font
|
|
193
|
+
#
|
|
194
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to check
|
|
195
|
+
# @return [Boolean] True if font has fvar table
|
|
196
|
+
def variable_font?(font)
|
|
197
|
+
font.has_table?("fvar")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Preserve variation data from source to target
|
|
201
|
+
#
|
|
202
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
203
|
+
# @param tables [Hash<String, String>] Target tables
|
|
204
|
+
# @param source_format [Symbol] Source format
|
|
205
|
+
# @param target_format [Symbol] Target format
|
|
206
|
+
# @param options [Hash] Preservation options
|
|
207
|
+
# @return [Hash<String, String>] Tables with variation preserved
|
|
208
|
+
def preserve_variation_data(font, tables, source_format, target_format,
|
|
209
|
+
options)
|
|
210
|
+
# Case 1: Compatible formats (same outline format) - just copy tables
|
|
211
|
+
if compatible_variation_formats?(source_format, target_format)
|
|
212
|
+
require_relative "../variation/variation_preserver"
|
|
213
|
+
Variation::VariationPreserver.preserve(font, tables, options)
|
|
214
|
+
|
|
215
|
+
# Case 2: Different outline formats - convert variation data
|
|
216
|
+
elsif convertible_variation_formats?(source_format, target_format)
|
|
217
|
+
convert_variation_data(font, tables, source_format, target_format,
|
|
218
|
+
options)
|
|
219
|
+
|
|
220
|
+
# Case 3: Unsupported conversion
|
|
221
|
+
else
|
|
222
|
+
if options[:preserve_variation]
|
|
223
|
+
raise Fontisan::Error,
|
|
224
|
+
"Cannot preserve variation data for " \
|
|
225
|
+
"#{source_format} → #{target_format}"
|
|
226
|
+
end
|
|
227
|
+
tables
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Check if formats have compatible variation (same outline format)
|
|
232
|
+
#
|
|
233
|
+
# @param source [Symbol] Source format
|
|
234
|
+
# @param target [Symbol] Target format
|
|
235
|
+
# @return [Boolean] True if compatible
|
|
236
|
+
def compatible_variation_formats?(source, target)
|
|
237
|
+
# Same format (copy operation)
|
|
238
|
+
return true if source == target
|
|
239
|
+
|
|
240
|
+
# Same outline format (just packaging change)
|
|
241
|
+
(source == :ttf && target == :woff) ||
|
|
242
|
+
(source == :otf && target == :woff) ||
|
|
243
|
+
(source == :woff && target == :ttf) ||
|
|
244
|
+
(source == :woff && target == :otf) ||
|
|
245
|
+
(source == :ttf && target == :woff2) ||
|
|
246
|
+
(source == :otf && target == :woff2)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Check if formats allow variation conversion (different outline formats)
|
|
250
|
+
#
|
|
251
|
+
# @param source [Symbol] Source format
|
|
252
|
+
# @param target [Symbol] Target format
|
|
253
|
+
# @return [Boolean] True if convertible
|
|
254
|
+
def convertible_variation_formats?(source, target)
|
|
255
|
+
# Different outline formats (need variation conversion)
|
|
256
|
+
(source == :ttf && target == :otf) ||
|
|
257
|
+
(source == :otf && target == :ttf)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Convert variation data between outline formats
|
|
261
|
+
#
|
|
262
|
+
# This is a placeholder for full TTF↔OTF variation conversion.
|
|
263
|
+
# Full implementation would:
|
|
264
|
+
# 1. Use Variation::Converter to convert gvar ↔ CFF2 blend
|
|
265
|
+
# 2. Build appropriate variation tables for target format
|
|
266
|
+
# 3. Preserve common tables (fvar, avar, STAT, metrics)
|
|
267
|
+
#
|
|
268
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
269
|
+
# @param tables [Hash<String, String>] Target tables
|
|
270
|
+
# @param source_format [Symbol] Source format
|
|
271
|
+
# @param target_format [Symbol] Target format
|
|
272
|
+
# @param options [Hash] Conversion options
|
|
273
|
+
# @return [Hash<String, String>] Tables with converted variation
|
|
274
|
+
def convert_variation_data(font, tables, source_format, target_format,
|
|
275
|
+
_options)
|
|
276
|
+
require_relative "../variation/variation_preserver"
|
|
277
|
+
require_relative "../variation/converter"
|
|
278
|
+
|
|
279
|
+
# For now, just preserve common tables and warn about conversion
|
|
280
|
+
warn "WARNING: Full variation conversion (#{source_format} → " \
|
|
281
|
+
"#{target_format}) not yet implemented. " \
|
|
282
|
+
"Preserving common variation tables only."
|
|
283
|
+
|
|
284
|
+
# Preserve common tables (fvar, avar, STAT) but not format-specific
|
|
285
|
+
Variation::VariationPreserver.preserve(
|
|
286
|
+
font,
|
|
287
|
+
tables,
|
|
288
|
+
preserve_format_specific: false,
|
|
289
|
+
preserve_metrics: true,
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
140
293
|
# Load conversion matrix from YAML config
|
|
141
294
|
#
|
|
142
295
|
# @param path [String, nil] Path to config file
|