fontisan 0.2.0 → 0.2.1
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 +270 -131
- data/README.adoc +158 -4
- data/Rakefile +44 -47
- data/lib/fontisan/cli.rb +84 -33
- data/lib/fontisan/collection/builder.rb +81 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +16 -0
- data/lib/fontisan/commands/convert_command.rb +97 -170
- data/lib/fontisan/commands/instance_command.rb +71 -80
- data/lib/fontisan/commands/validate_command.rb +25 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +10 -0
- data/lib/fontisan/converters/format_converter.rb +150 -1
- data/lib/fontisan/converters/outline_converter.rb +80 -18
- data/lib/fontisan/converters/woff_writer.rb +1 -1
- data/lib/fontisan/font_loader.rb +3 -5
- data/lib/fontisan/font_writer.rb +7 -6
- data/lib/fontisan/hints/hint_converter.rb +133 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +221 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +100 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +127 -0
- data/lib/fontisan/loading_modes.rb +2 -0
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/hint.rb +173 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_font.rb +25 -9
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -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 +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/tables/cff/charstring.rb +33 -4
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +15 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +9 -4
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/true_type_font.rb +24 -9
- 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/converter.rb +120 -13
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- 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 +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +475 -470
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +12 -0
- metadata +31 -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,10 +59,9 @@ 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
|
|
|
@@ -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,19 @@ 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, options)}"
|
|
121
|
+
puts "Output format: #{options[:to] || 'same as input'}"
|
|
126
122
|
puts
|
|
127
123
|
puts "Use without --dry-run to actually generate the instance."
|
|
128
124
|
end
|
|
129
125
|
|
|
130
126
|
# Instance at specific coordinates
|
|
131
127
|
#
|
|
132
|
-
# @param
|
|
128
|
+
# @param font [Object] Font object
|
|
133
129
|
# @param coords [Hash] User coordinates
|
|
134
130
|
# @param output_path [String] Output file path
|
|
135
131
|
# @param options [Hash] Command options
|
|
136
|
-
def instance_coords(
|
|
132
|
+
def instance_coords(font, coords, output_path, options)
|
|
137
133
|
if coords.empty?
|
|
138
134
|
raise ArgumentError,
|
|
139
135
|
"No coordinates specified. Use --wght=700, --wdth=100, etc."
|
|
@@ -142,47 +138,64 @@ module Fontisan
|
|
|
142
138
|
# Show progress if requested
|
|
143
139
|
print "Generating instance..." if options[:progress]
|
|
144
140
|
|
|
145
|
-
# Generate instance
|
|
146
|
-
|
|
141
|
+
# Generate instance tables using InstanceGenerator
|
|
142
|
+
generator = Variation::InstanceGenerator.new(font, coords)
|
|
143
|
+
tables = generator.generate
|
|
147
144
|
|
|
148
145
|
puts " done" if options[:progress]
|
|
149
146
|
|
|
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
|
|
147
|
+
# Write instance using InstanceWriter
|
|
158
148
|
print "Writing output..." if options[:progress]
|
|
159
|
-
|
|
149
|
+
|
|
150
|
+
# Detect source format for conversion
|
|
151
|
+
source_format = detect_source_format(font)
|
|
152
|
+
|
|
153
|
+
Variation::InstanceWriter.write(
|
|
154
|
+
tables,
|
|
155
|
+
output_path,
|
|
156
|
+
format: options[:to]&.to_sym,
|
|
157
|
+
source_format: source_format,
|
|
158
|
+
optimize: options[:optimize] || false,
|
|
159
|
+
)
|
|
160
|
+
|
|
160
161
|
puts " done" if options[:progress]
|
|
161
162
|
end
|
|
162
163
|
|
|
163
164
|
# Instance using named instance
|
|
164
165
|
#
|
|
165
|
-
# @param
|
|
166
|
-
# @param
|
|
166
|
+
# @param font [Object] Font object
|
|
167
|
+
# @param instance_index [Integer] Named instance index
|
|
167
168
|
# @param output_path [String] Output file path
|
|
168
169
|
# @param options [Hash] Command options
|
|
169
|
-
def instance_named(
|
|
170
|
-
# Generate instance
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
def instance_named(font, instance_index, output_path, options)
|
|
171
|
+
# Generate instance using named instance
|
|
172
|
+
generator = Variation::InstanceGenerator.new(font)
|
|
173
|
+
tables = generator.generate_named_instance(instance_index)
|
|
174
|
+
|
|
175
|
+
# Detect source format
|
|
176
|
+
source_format = detect_source_format(font)
|
|
177
|
+
|
|
178
|
+
# Write instance
|
|
179
|
+
Variation::InstanceWriter.write(
|
|
180
|
+
tables,
|
|
181
|
+
output_path,
|
|
182
|
+
format: options[:to]&.to_sym,
|
|
183
|
+
source_format: source_format,
|
|
184
|
+
optimize: options[:optimize] || false,
|
|
185
|
+
)
|
|
178
186
|
end
|
|
179
187
|
|
|
180
188
|
# List available named instances
|
|
181
189
|
#
|
|
182
|
-
# @param
|
|
183
|
-
def list_instances(
|
|
184
|
-
|
|
190
|
+
# @param font [Object] Font object
|
|
191
|
+
def list_instances(font)
|
|
192
|
+
fvar = font.table("fvar")
|
|
193
|
+
unless fvar
|
|
194
|
+
puts "Not a variable font - no named instances available."
|
|
195
|
+
return
|
|
196
|
+
end
|
|
185
197
|
|
|
198
|
+
instances = fvar.instances
|
|
186
199
|
if instances.empty?
|
|
187
200
|
puts "No named instances defined in font."
|
|
188
201
|
return
|
|
@@ -191,11 +204,15 @@ module Fontisan
|
|
|
191
204
|
puts "Available named instances:"
|
|
192
205
|
puts
|
|
193
206
|
|
|
194
|
-
instances.
|
|
195
|
-
|
|
207
|
+
instances.each_with_index do |instance, index|
|
|
208
|
+
name_id = instance[:subfamily_name_id]
|
|
209
|
+
puts " [#{index}] Instance #{name_id}"
|
|
196
210
|
puts " Coordinates:"
|
|
197
|
-
instance[:coordinates].
|
|
198
|
-
|
|
211
|
+
instance[:coordinates].each_with_index do |value, axis_index|
|
|
212
|
+
next if axis_index >= fvar.axes.length
|
|
213
|
+
|
|
214
|
+
axis = fvar.axes[axis_index]
|
|
215
|
+
puts " #{axis.axis_tag}: #{value}"
|
|
199
216
|
end
|
|
200
217
|
puts
|
|
201
218
|
end
|
|
@@ -243,38 +260,12 @@ module Fontisan
|
|
|
243
260
|
"#{dir}/#{base}-instance.#{ext}"
|
|
244
261
|
end
|
|
245
262
|
|
|
246
|
-
#
|
|
263
|
+
# Detect source format from font
|
|
247
264
|
#
|
|
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
|
|
265
|
+
# @param font [Object] Font object
|
|
266
|
+
# @return [Symbol] Source format (:ttf or :otf)
|
|
267
|
+
def detect_source_format(font)
|
|
268
|
+
font.has_table?("CFF ") || font.has_table?("CFF2") ? :otf : :ttf
|
|
278
269
|
end
|
|
279
270
|
|
|
280
271
|
# 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,27 @@ 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
|
+
report.errors << { message: error, category: "variable_font" } if report.respond_to?(:errors)
|
|
88
|
+
end
|
|
89
|
+
elsif @verbose && !@quiet
|
|
90
|
+
puts "\n✓ Variable font structure valid"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
69
94
|
# Validate command parameters
|
|
70
95
|
#
|
|
71
96
|
# @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.
|
|
@@ -73,20 +73,50 @@ 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, options.merge(target_format: target_format))
|
|
106
|
+
tables = strategy.convert(font, options.merge(target_format: target_format))
|
|
107
|
+
|
|
108
|
+
# Preserve variation data if requested and font is variable
|
|
109
|
+
if options.fetch(:preserve_variation, true) && variable_font?(font)
|
|
110
|
+
tables = preserve_variation_data(
|
|
111
|
+
font,
|
|
112
|
+
tables,
|
|
113
|
+
source_format,
|
|
114
|
+
target_format,
|
|
115
|
+
options,
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
tables
|
|
90
120
|
end
|
|
91
121
|
|
|
92
122
|
# Check if a conversion is supported
|
|
@@ -137,6 +167,125 @@ module Fontisan
|
|
|
137
167
|
|
|
138
168
|
private
|
|
139
169
|
|
|
170
|
+
# Convert variable font to SVG at specific coordinates
|
|
171
|
+
#
|
|
172
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
173
|
+
# @param options [Hash] Conversion options
|
|
174
|
+
# @option options [Hash] :instance_coordinates Design space coordinates
|
|
175
|
+
# @option options [Integer] :instance_index Named instance index
|
|
176
|
+
# @return [Hash] Hash with :svg_xml key
|
|
177
|
+
def convert_variable_to_svg(font, options = {})
|
|
178
|
+
require_relative "../variation/variable_svg_generator"
|
|
179
|
+
|
|
180
|
+
coordinates = options[:instance_coordinates] || {}
|
|
181
|
+
generator = Variation::VariableSvgGenerator.new(font, coordinates)
|
|
182
|
+
|
|
183
|
+
# Use named instance if specified
|
|
184
|
+
if options[:instance_index]
|
|
185
|
+
generator.generate_named_instance(options[:instance_index], options)
|
|
186
|
+
else
|
|
187
|
+
generator.generate(options)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if font is a variable font
|
|
192
|
+
#
|
|
193
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to check
|
|
194
|
+
# @return [Boolean] True if font has fvar table
|
|
195
|
+
def variable_font?(font)
|
|
196
|
+
font.has_table?("fvar")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Preserve variation data from source to target
|
|
200
|
+
#
|
|
201
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
202
|
+
# @param tables [Hash<String, String>] Target tables
|
|
203
|
+
# @param source_format [Symbol] Source format
|
|
204
|
+
# @param target_format [Symbol] Target format
|
|
205
|
+
# @param options [Hash] Preservation options
|
|
206
|
+
# @return [Hash<String, String>] Tables with variation preserved
|
|
207
|
+
def preserve_variation_data(font, tables, source_format, target_format, options)
|
|
208
|
+
# Case 1: Compatible formats (same outline format) - just copy tables
|
|
209
|
+
if compatible_variation_formats?(source_format, target_format)
|
|
210
|
+
require_relative "../variation/variation_preserver"
|
|
211
|
+
Variation::VariationPreserver.preserve(font, tables, options)
|
|
212
|
+
|
|
213
|
+
# Case 2: Different outline formats - convert variation data
|
|
214
|
+
elsif convertible_variation_formats?(source_format, target_format)
|
|
215
|
+
convert_variation_data(font, tables, source_format, target_format, options)
|
|
216
|
+
|
|
217
|
+
# Case 3: Unsupported conversion
|
|
218
|
+
else
|
|
219
|
+
if options[:preserve_variation]
|
|
220
|
+
raise Fontisan::Error,
|
|
221
|
+
"Cannot preserve variation data for " \
|
|
222
|
+
"#{source_format} → #{target_format}"
|
|
223
|
+
end
|
|
224
|
+
tables
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Check if formats have compatible variation (same outline format)
|
|
229
|
+
#
|
|
230
|
+
# @param source [Symbol] Source format
|
|
231
|
+
# @param target [Symbol] Target format
|
|
232
|
+
# @return [Boolean] True if compatible
|
|
233
|
+
def compatible_variation_formats?(source, target)
|
|
234
|
+
# Same format (copy operation)
|
|
235
|
+
return true if source == target
|
|
236
|
+
|
|
237
|
+
# Same outline format (just packaging change)
|
|
238
|
+
(source == :ttf && target == :woff) ||
|
|
239
|
+
(source == :otf && target == :woff) ||
|
|
240
|
+
(source == :woff && target == :ttf) ||
|
|
241
|
+
(source == :woff && target == :otf) ||
|
|
242
|
+
(source == :ttf && target == :woff2) ||
|
|
243
|
+
(source == :otf && target == :woff2)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Check if formats allow variation conversion (different outline formats)
|
|
247
|
+
#
|
|
248
|
+
# @param source [Symbol] Source format
|
|
249
|
+
# @param target [Symbol] Target format
|
|
250
|
+
# @return [Boolean] True if convertible
|
|
251
|
+
def convertible_variation_formats?(source, target)
|
|
252
|
+
# Different outline formats (need variation conversion)
|
|
253
|
+
(source == :ttf && target == :otf) ||
|
|
254
|
+
(source == :otf && target == :ttf)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Convert variation data between outline formats
|
|
258
|
+
#
|
|
259
|
+
# This is a placeholder for full TTF↔OTF variation conversion.
|
|
260
|
+
# Full implementation would:
|
|
261
|
+
# 1. Use Variation::Converter to convert gvar ↔ CFF2 blend
|
|
262
|
+
# 2. Build appropriate variation tables for target format
|
|
263
|
+
# 3. Preserve common tables (fvar, avar, STAT, metrics)
|
|
264
|
+
#
|
|
265
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
266
|
+
# @param tables [Hash<String, String>] Target tables
|
|
267
|
+
# @param source_format [Symbol] Source format
|
|
268
|
+
# @param target_format [Symbol] Target format
|
|
269
|
+
# @param options [Hash] Conversion options
|
|
270
|
+
# @return [Hash<String, String>] Tables with converted variation
|
|
271
|
+
def convert_variation_data(font, tables, source_format, target_format, _options)
|
|
272
|
+
require_relative "../variation/variation_preserver"
|
|
273
|
+
require_relative "../variation/converter"
|
|
274
|
+
|
|
275
|
+
# For now, just preserve common tables and warn about conversion
|
|
276
|
+
warn "WARNING: Full variation conversion (#{source_format} → " \
|
|
277
|
+
"#{target_format}) not yet implemented. " \
|
|
278
|
+
"Preserving common variation tables only."
|
|
279
|
+
|
|
280
|
+
# Preserve common tables (fvar, avar, STAT) but not format-specific
|
|
281
|
+
Variation::VariationPreserver.preserve(
|
|
282
|
+
font,
|
|
283
|
+
tables,
|
|
284
|
+
preserve_format_specific: false,
|
|
285
|
+
preserve_metrics: true,
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
140
289
|
# Load conversion matrix from YAML config
|
|
141
290
|
#
|
|
142
291
|
# @param path [String, nil] Path to config file
|
|
@@ -153,6 +153,22 @@ module Fontisan
|
|
|
153
153
|
# Update head table for CFF
|
|
154
154
|
tables["head"] = update_head_for_cff(font)
|
|
155
155
|
|
|
156
|
+
# Convert and apply hints if preservation is enabled
|
|
157
|
+
if @preserve_hints && hints_per_glyph.any?
|
|
158
|
+
# Extract font-level hints separately
|
|
159
|
+
hint_set = extract_ttf_hint_set(font)
|
|
160
|
+
|
|
161
|
+
unless hint_set.empty?
|
|
162
|
+
# Convert TrueType hints to PostScript format
|
|
163
|
+
converter = Hints::HintConverter.new
|
|
164
|
+
ps_hint_set = converter.convert_hint_set(hint_set, :postscript)
|
|
165
|
+
|
|
166
|
+
# Apply PostScript hints (validation mode - CFF modification pending)
|
|
167
|
+
applier = Hints::PostScriptHintApplier.new
|
|
168
|
+
tables = applier.apply(ps_hint_set, tables)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
156
172
|
tables
|
|
157
173
|
end
|
|
158
174
|
|
|
@@ -183,6 +199,22 @@ module Fontisan
|
|
|
183
199
|
# Update head table for TrueType
|
|
184
200
|
tables["head"] = update_head_for_truetype(font, loca_format)
|
|
185
201
|
|
|
202
|
+
# Convert and apply hints if preservation is enabled
|
|
203
|
+
if @preserve_hints && hints_per_glyph.any?
|
|
204
|
+
# Extract font-level hints separately
|
|
205
|
+
hint_set = extract_cff_hint_set(font)
|
|
206
|
+
|
|
207
|
+
unless hint_set.empty?
|
|
208
|
+
# Convert PostScript hints to TrueType format
|
|
209
|
+
converter = Hints::HintConverter.new
|
|
210
|
+
tt_hint_set = converter.convert_hint_set(hint_set, :truetype)
|
|
211
|
+
|
|
212
|
+
# Apply TrueType hints (writes fpgm/prep/cvt tables)
|
|
213
|
+
applier = Hints::TrueTypeHintApplier.new
|
|
214
|
+
tables = applier.apply(tt_hint_set, tables)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
186
218
|
tables
|
|
187
219
|
end
|
|
188
220
|
|
|
@@ -493,19 +525,8 @@ module Fontisan
|
|
|
493
525
|
next
|
|
494
526
|
end
|
|
495
527
|
|
|
496
|
-
#
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
# Build glyph data
|
|
500
|
-
builder = Tables::Glyf::GlyphBuilder.new(
|
|
501
|
-
contours: contours,
|
|
502
|
-
x_min: outline.bbox[:x_min],
|
|
503
|
-
y_min: outline.bbox[:y_min],
|
|
504
|
-
x_max: outline.bbox[:x_max],
|
|
505
|
-
y_max: outline.bbox[:y_max],
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
glyph_data = builder.build
|
|
528
|
+
# Build glyph data using GlyphBuilder class method
|
|
529
|
+
glyph_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
|
|
509
530
|
glyf_data << glyph_data
|
|
510
531
|
|
|
511
532
|
# Add padding to 4-byte boundary
|
|
@@ -835,19 +856,31 @@ module Fontisan
|
|
|
835
856
|
def validate_source_tables(font, format)
|
|
836
857
|
case format
|
|
837
858
|
when :ttf
|
|
838
|
-
unless font.has_table?("glyf") && font.has_table?("loca")
|
|
839
|
-
|
|
859
|
+
unless font.has_table?("glyf") && font.has_table?("loca")
|
|
860
|
+
raise Fontisan::MissingTableError,
|
|
861
|
+
"TrueType font missing required glyf or loca table"
|
|
862
|
+
end
|
|
863
|
+
# Also verify tables can actually be loaded
|
|
864
|
+
unless font.table("glyf") && font.table("loca")
|
|
840
865
|
raise Fontisan::MissingTableError,
|
|
841
866
|
"TrueType font missing required glyf or loca table"
|
|
842
867
|
end
|
|
843
868
|
when :cff2
|
|
844
|
-
unless font.has_table?("CFF2")
|
|
869
|
+
unless font.has_table?("CFF2")
|
|
870
|
+
raise Fontisan::MissingTableError,
|
|
871
|
+
"CFF2 font missing required CFF2 table"
|
|
872
|
+
end
|
|
873
|
+
unless font.table("CFF2")
|
|
845
874
|
raise Fontisan::MissingTableError,
|
|
846
875
|
"CFF2 font missing required CFF2 table"
|
|
847
876
|
end
|
|
848
877
|
when :otf
|
|
849
|
-
unless
|
|
850
|
-
|
|
878
|
+
unless font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
879
|
+
raise Fontisan::MissingTableError,
|
|
880
|
+
"OpenType font missing required CFF or CFF2 table"
|
|
881
|
+
end
|
|
882
|
+
# Verify at least one can be loaded
|
|
883
|
+
unless font.table("CFF ") || font.table("CFF2")
|
|
851
884
|
raise Fontisan::MissingTableError,
|
|
852
885
|
"OpenType font missing required CFF or CFF2 table"
|
|
853
886
|
end
|
|
@@ -855,6 +888,11 @@ module Fontisan
|
|
|
855
888
|
|
|
856
889
|
# Common required tables
|
|
857
890
|
%w[head hhea maxp].each do |tag|
|
|
891
|
+
unless font.has_table?(tag)
|
|
892
|
+
raise Fontisan::MissingTableError,
|
|
893
|
+
"Font missing required #{tag} table"
|
|
894
|
+
end
|
|
895
|
+
# Verify table can actually be loaded
|
|
858
896
|
unless font.table(tag)
|
|
859
897
|
raise Fontisan::MissingTableError,
|
|
860
898
|
"Font missing required #{tag} table"
|
|
@@ -924,6 +962,30 @@ module Fontisan
|
|
|
924
962
|
{}
|
|
925
963
|
end
|
|
926
964
|
|
|
965
|
+
# Extract complete TrueType hint set from font
|
|
966
|
+
#
|
|
967
|
+
# @param font [TrueTypeFont] Source font
|
|
968
|
+
# @return [HintSet] Complete hint set
|
|
969
|
+
def extract_ttf_hint_set(font)
|
|
970
|
+
extractor = Hints::TrueTypeHintExtractor.new
|
|
971
|
+
extractor.extract_from_font(font)
|
|
972
|
+
rescue StandardError => e
|
|
973
|
+
warn "Failed to extract TrueType hint set: #{e.message}"
|
|
974
|
+
Models::HintSet.new(format: :truetype)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
# Extract complete PostScript hint set from font
|
|
978
|
+
#
|
|
979
|
+
# @param font [OpenTypeFont] Source font
|
|
980
|
+
# @return [HintSet] Complete hint set
|
|
981
|
+
def extract_cff_hint_set(font)
|
|
982
|
+
extractor = Hints::PostScriptHintExtractor.new
|
|
983
|
+
extractor.extract_from_font(font)
|
|
984
|
+
rescue StandardError => e
|
|
985
|
+
warn "Failed to extract PostScript hint set: #{e.message}"
|
|
986
|
+
Models::HintSet.new(format: :postscript)
|
|
987
|
+
end
|
|
988
|
+
|
|
927
989
|
# Check if font is a variable font
|
|
928
990
|
#
|
|
929
991
|
# @param font [TrueTypeFont, OpenTypeFont] Font to check
|
|
@@ -364,7 +364,7 @@ module Fontisan
|
|
|
364
364
|
# Sort tables by tag for consistent output (same order as directory)
|
|
365
365
|
sorted_tables = compressed_tables.sort_by { |tag, _| tag }
|
|
366
366
|
|
|
367
|
-
sorted_tables.
|
|
367
|
+
sorted_tables.each do |_tag, table_info|
|
|
368
368
|
io.write(table_info[:compressed_data])
|
|
369
369
|
end
|
|
370
370
|
end
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -63,14 +63,12 @@ module Fontisan
|
|
|
63
63
|
when "OTTO"
|
|
64
64
|
OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
65
65
|
when "wOFF"
|
|
66
|
-
|
|
67
|
-
"Unsupported font format: WOFF. Please convert to TTF/OTF first."
|
|
66
|
+
WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
68
67
|
when "wOF2"
|
|
69
|
-
|
|
70
|
-
"Unsupported font format: WOFF2. Please convert to TTF/OTF first."
|
|
68
|
+
Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
71
69
|
else
|
|
72
70
|
raise InvalidFontError,
|
|
73
|
-
"Unknown font format. Expected TTF, OTF, TTC, or
|
|
71
|
+
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
|
|
74
72
|
end
|
|
75
73
|
end
|
|
76
74
|
end
|