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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "instance_generator"
|
|
4
|
+
require_relative "../converters/svg_generator"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Generates SVG fonts from variable fonts at specific coordinates
|
|
9
|
+
#
|
|
10
|
+
# [`VariableSvgGenerator`](lib/fontisan/variation/variable_svg_generator.rb)
|
|
11
|
+
# combines instance generation with SVG conversion to create static SVG
|
|
12
|
+
# fonts from variable fonts at any point in the design space.
|
|
13
|
+
#
|
|
14
|
+
# Process:
|
|
15
|
+
# 1. Accept variable font + axis coordinates
|
|
16
|
+
# 2. Generate static instance using InstanceGenerator
|
|
17
|
+
# 3. Build temporary font from instance tables
|
|
18
|
+
# 4. Delegate to SvgGenerator for SVG creation
|
|
19
|
+
# 5. Return SVG with variation metadata
|
|
20
|
+
#
|
|
21
|
+
# This enables generating SVG fonts at specific weights, widths, or other
|
|
22
|
+
# variation axes without creating intermediate font files.
|
|
23
|
+
#
|
|
24
|
+
# @example Generate SVG at Bold weight
|
|
25
|
+
# generator = VariableSvgGenerator.new(variable_font, { "wght" => 700.0 })
|
|
26
|
+
# svg_result = generator.generate
|
|
27
|
+
# File.write("bold.svg", svg_result[:svg_xml])
|
|
28
|
+
#
|
|
29
|
+
# @example Generate SVG at specific width and weight
|
|
30
|
+
# coords = { "wght" => 700.0, "wdth" => 75.0 }
|
|
31
|
+
# generator = VariableSvgGenerator.new(variable_font, coords)
|
|
32
|
+
# svg_result = generator.generate(pretty_print: true)
|
|
33
|
+
class VariableSvgGenerator
|
|
34
|
+
# @return [TrueTypeFont, OpenTypeFont] Variable font
|
|
35
|
+
attr_reader :font
|
|
36
|
+
|
|
37
|
+
# @return [Hash<String, Float>] Design space coordinates
|
|
38
|
+
attr_reader :coordinates
|
|
39
|
+
|
|
40
|
+
# Initialize generator
|
|
41
|
+
#
|
|
42
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
43
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
44
|
+
# @raise [Error] If font is not a variable font
|
|
45
|
+
def initialize(font, coordinates = {})
|
|
46
|
+
@font = font
|
|
47
|
+
@coordinates = coordinates || {}
|
|
48
|
+
|
|
49
|
+
validate_variable_font!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate SVG font at specified coordinates
|
|
53
|
+
#
|
|
54
|
+
# Creates a static instance at the given coordinates and converts
|
|
55
|
+
# it to SVG format. Returns the same format as SvgGenerator for
|
|
56
|
+
# consistency.
|
|
57
|
+
#
|
|
58
|
+
# @param options [Hash] SVG generation options
|
|
59
|
+
# @option options [Boolean] :pretty_print Pretty print XML (default: true)
|
|
60
|
+
# @option options [Array<Integer>] :glyph_ids Specific glyphs (default: all)
|
|
61
|
+
# @option options [Integer] :max_glyphs Maximum glyphs (default: all)
|
|
62
|
+
# @option options [String] :font_id Font ID for SVG
|
|
63
|
+
# @option options [Integer] :default_advance Default advance width
|
|
64
|
+
# @return [Hash] Hash with :svg_xml key containing SVG XML string
|
|
65
|
+
# @raise [Error] If generation fails
|
|
66
|
+
def generate(options = {})
|
|
67
|
+
# Generate static instance tables
|
|
68
|
+
instance_tables = generate_static_instance
|
|
69
|
+
|
|
70
|
+
# Build temporary font from instance tables
|
|
71
|
+
static_font = build_font_from_tables(instance_tables)
|
|
72
|
+
|
|
73
|
+
# Generate SVG using standard generator
|
|
74
|
+
svg_generator = Converters::SvgGenerator.new
|
|
75
|
+
result = svg_generator.convert(static_font, options)
|
|
76
|
+
|
|
77
|
+
# Add variation metadata to result
|
|
78
|
+
result[:variation_metadata] = {
|
|
79
|
+
coordinates: @coordinates,
|
|
80
|
+
source_font: extract_font_name,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Generate SVG for a named instance
|
|
87
|
+
#
|
|
88
|
+
# @param instance_index [Integer] Index of named instance in fvar
|
|
89
|
+
# @param options [Hash] SVG generation options
|
|
90
|
+
# @return [Hash] Hash with :svg_xml key
|
|
91
|
+
def generate_named_instance(instance_index, options = {})
|
|
92
|
+
instance_generator = InstanceGenerator.new(@font)
|
|
93
|
+
instance_tables = instance_generator.generate_named_instance(instance_index)
|
|
94
|
+
|
|
95
|
+
static_font = build_font_from_tables(instance_tables)
|
|
96
|
+
svg_generator = Converters::SvgGenerator.new
|
|
97
|
+
result = svg_generator.convert(static_font, options)
|
|
98
|
+
|
|
99
|
+
# Add instance metadata
|
|
100
|
+
result[:variation_metadata] = {
|
|
101
|
+
instance_index: instance_index,
|
|
102
|
+
source_font: extract_font_name,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get default coordinates for font
|
|
109
|
+
#
|
|
110
|
+
# Returns all axes at their default values.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash<String, Float>] Default coordinates
|
|
113
|
+
def default_coordinates
|
|
114
|
+
return {} unless @font.has_table?("fvar")
|
|
115
|
+
|
|
116
|
+
fvar = @font.table("fvar")
|
|
117
|
+
return {} unless fvar
|
|
118
|
+
|
|
119
|
+
coords = {}
|
|
120
|
+
fvar.axes.each do |axis|
|
|
121
|
+
coords[axis.axis_tag] = axis.default_value
|
|
122
|
+
end
|
|
123
|
+
coords
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get list of named instances
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<Hash>] Array of instance info
|
|
129
|
+
def named_instances
|
|
130
|
+
return [] unless @font.has_table?("fvar")
|
|
131
|
+
|
|
132
|
+
fvar = @font.table("fvar")
|
|
133
|
+
return [] unless fvar
|
|
134
|
+
|
|
135
|
+
fvar.instances.map.with_index do |instance, index|
|
|
136
|
+
{
|
|
137
|
+
index: index,
|
|
138
|
+
name: instance[:subfamily_name_id],
|
|
139
|
+
coordinates: build_instance_coordinates(instance, fvar.axes),
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Validate that font is a variable font
|
|
147
|
+
#
|
|
148
|
+
# @raise [Error] If not a variable font
|
|
149
|
+
def validate_variable_font!
|
|
150
|
+
unless @font.has_table?("fvar")
|
|
151
|
+
raise Fontisan::Error,
|
|
152
|
+
"Font must be a variable font (missing fvar table)"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check for variation data
|
|
156
|
+
has_gvar = @font.has_table?("gvar")
|
|
157
|
+
has_cff2 = @font.has_table?("CFF2")
|
|
158
|
+
|
|
159
|
+
unless has_gvar || has_cff2
|
|
160
|
+
raise Fontisan::Error,
|
|
161
|
+
"Variable font must have gvar (TrueType) or CFF2 (PostScript) table"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Generate static instance at current coordinates
|
|
166
|
+
#
|
|
167
|
+
# @return [Hash<String, String>] Instance tables
|
|
168
|
+
def generate_static_instance
|
|
169
|
+
# Use coordinates or defaults if none specified
|
|
170
|
+
coords = @coordinates.empty? ? default_coordinates : @coordinates
|
|
171
|
+
|
|
172
|
+
instance_generator = InstanceGenerator.new(@font, coords)
|
|
173
|
+
instance_generator.generate
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Build a font object from instance tables
|
|
177
|
+
#
|
|
178
|
+
# Creates a minimal font object that can be used by SvgGenerator.
|
|
179
|
+
# This is a lightweight wrapper around the table data.
|
|
180
|
+
#
|
|
181
|
+
# @param tables [Hash<String, String>] Font tables
|
|
182
|
+
# @return [Object] Font-like object
|
|
183
|
+
def build_font_from_tables(tables)
|
|
184
|
+
# Create a simple font wrapper that implements the minimal
|
|
185
|
+
# interface needed by SvgGenerator
|
|
186
|
+
InstanceFontWrapper.new(@font, tables)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Extract font name for metadata
|
|
190
|
+
#
|
|
191
|
+
# @return [String] Font name
|
|
192
|
+
def extract_font_name
|
|
193
|
+
name_table = @font.table("name")
|
|
194
|
+
return "Unknown" unless name_table
|
|
195
|
+
|
|
196
|
+
# Try font family name
|
|
197
|
+
family = name_table.font_family.first
|
|
198
|
+
return family if family && !family.empty?
|
|
199
|
+
|
|
200
|
+
"Unknown"
|
|
201
|
+
rescue StandardError
|
|
202
|
+
"Unknown"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Build coordinates from instance
|
|
206
|
+
#
|
|
207
|
+
# @param instance [Hash] Instance data
|
|
208
|
+
# @param axes [Array] Variation axes
|
|
209
|
+
# @return [Hash<String, Float>] Coordinates
|
|
210
|
+
def build_instance_coordinates(instance, axes)
|
|
211
|
+
coords = {}
|
|
212
|
+
instance[:coordinates].each_with_index do |value, index|
|
|
213
|
+
next if index >= axes.length
|
|
214
|
+
|
|
215
|
+
axis = axes[index]
|
|
216
|
+
coords[axis.axis_tag] = value
|
|
217
|
+
end
|
|
218
|
+
coords
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Wrapper class for instance font tables
|
|
222
|
+
#
|
|
223
|
+
# Provides minimal interface needed by SvgGenerator while using
|
|
224
|
+
# instance tables instead of original font tables.
|
|
225
|
+
class InstanceFontWrapper
|
|
226
|
+
# @return [Hash<String, String>] Font tables
|
|
227
|
+
attr_reader :table_data
|
|
228
|
+
|
|
229
|
+
# Initialize wrapper
|
|
230
|
+
#
|
|
231
|
+
# @param original_font [Object] Original variable font
|
|
232
|
+
# @param instance_tables [Hash<String, String>] Instance tables
|
|
233
|
+
def initialize(original_font, instance_tables)
|
|
234
|
+
@original_font = original_font
|
|
235
|
+
@table_data = instance_tables
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get table by tag
|
|
239
|
+
#
|
|
240
|
+
# @param tag [String] Table tag
|
|
241
|
+
# @return [Object, nil] Table or nil
|
|
242
|
+
def table(tag)
|
|
243
|
+
# Use instance table if available, otherwise fall back to original
|
|
244
|
+
if @table_data.key?(tag)
|
|
245
|
+
end
|
|
246
|
+
@original_font.table(tag)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Check if table exists
|
|
250
|
+
#
|
|
251
|
+
# @param tag [String] Table tag
|
|
252
|
+
# @return [Boolean] True if table exists
|
|
253
|
+
def has_table?(tag)
|
|
254
|
+
@table_data.key?(tag) || @original_font.has_table?(tag)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Forward other methods to original font
|
|
258
|
+
def method_missing(method, ...)
|
|
259
|
+
@original_font.send(method, ...)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def respond_to_missing?(method, include_private = false)
|
|
263
|
+
@original_font.respond_to?(method, include_private) || super
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "variation_context"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Preserves variation data when converting between compatible font formats
|
|
9
|
+
#
|
|
10
|
+
# [`VariationPreserver`](lib/fontisan/variation/variation_preserver.rb)
|
|
11
|
+
# copies variation tables from source to target font during format
|
|
12
|
+
# conversion. It handles:
|
|
13
|
+
# - Common variation tables (fvar, avar, STAT) - shared by all variable fonts
|
|
14
|
+
# - Format-specific tables (gvar for TTF, CFF2 for OTF)
|
|
15
|
+
# - Metrics variation tables (HVAR, VVAR, MVAR)
|
|
16
|
+
# - Table checksum updates
|
|
17
|
+
# - Validation of table consistency
|
|
18
|
+
#
|
|
19
|
+
# **Use Cases:**
|
|
20
|
+
#
|
|
21
|
+
# 1. **Variable TTF → Variable WOFF**: Preserve all gvar-based variation
|
|
22
|
+
# 2. **Variable OTF → Variable WOFF**: Preserve all CFF2-based variation
|
|
23
|
+
# 3. **Variable TTF → Variable OTF**: Copy common tables (fvar, avar, STAT)
|
|
24
|
+
# but variation data conversion handled by Converter
|
|
25
|
+
# 4. **Variable OTF → Variable TTF**: Copy common tables (fvar, avar, STAT)
|
|
26
|
+
# but variation data conversion handled by Converter
|
|
27
|
+
#
|
|
28
|
+
# **Preserved Tables:**
|
|
29
|
+
#
|
|
30
|
+
# Common (all variable fonts):
|
|
31
|
+
# - fvar (Font Variations)
|
|
32
|
+
# - avar (Axis Variations, optional)
|
|
33
|
+
# - STAT (Style Attributes)
|
|
34
|
+
#
|
|
35
|
+
# TrueType-specific:
|
|
36
|
+
# - gvar (Glyph Variations)
|
|
37
|
+
# - cvar (CVT Variations, optional)
|
|
38
|
+
#
|
|
39
|
+
# CFF2-specific:
|
|
40
|
+
# - CFF2 (with blend operators)
|
|
41
|
+
#
|
|
42
|
+
# Metrics (optional):
|
|
43
|
+
# - HVAR (Horizontal Metrics Variations)
|
|
44
|
+
# - VVAR (Vertical Metrics Variations)
|
|
45
|
+
# - MVAR (Metrics Variations)
|
|
46
|
+
#
|
|
47
|
+
# @example Preserve variation when converting TTF to WOFF
|
|
48
|
+
# preserver = VariationPreserver.new(ttf_font, woff_tables)
|
|
49
|
+
# preserved_tables = preserver.preserve
|
|
50
|
+
#
|
|
51
|
+
# @example Preserve only common tables for outline conversion
|
|
52
|
+
# preserver = VariationPreserver.new(ttf_font, otf_tables,
|
|
53
|
+
# preserve_format_specific: false)
|
|
54
|
+
# preserved_tables = preserver.preserve
|
|
55
|
+
class VariationPreserver
|
|
56
|
+
# Common variation tables present in all variable fonts
|
|
57
|
+
COMMON_TABLES = %w[fvar avar STAT].freeze
|
|
58
|
+
|
|
59
|
+
# TrueType-specific variation tables
|
|
60
|
+
TRUETYPE_TABLES = %w[gvar cvar].freeze
|
|
61
|
+
|
|
62
|
+
# CFF2-specific variation tables
|
|
63
|
+
CFF2_TABLES = %w[CFF2].freeze
|
|
64
|
+
|
|
65
|
+
# Metrics variation tables
|
|
66
|
+
METRICS_TABLES = %w[HVAR VVAR MVAR].freeze
|
|
67
|
+
|
|
68
|
+
# All variation-related tables
|
|
69
|
+
ALL_VARIATION_TABLES = (COMMON_TABLES + TRUETYPE_TABLES +
|
|
70
|
+
CFF2_TABLES + METRICS_TABLES).freeze
|
|
71
|
+
|
|
72
|
+
# Preserve variation data from source to target
|
|
73
|
+
#
|
|
74
|
+
# @param source_font [TrueTypeFont, OpenTypeFont] Variable font
|
|
75
|
+
# @param target_tables [Hash<String, String>] Target font tables
|
|
76
|
+
# @param options [Hash] Preservation options
|
|
77
|
+
# @return [Hash<String, String>] Tables with variation data preserved
|
|
78
|
+
def self.preserve(source_font, target_tables, options = {})
|
|
79
|
+
new(source_font, target_tables, options).preserve
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Object] Source font
|
|
83
|
+
attr_reader :source_font
|
|
84
|
+
|
|
85
|
+
# @return [Hash<String, String>] Target tables
|
|
86
|
+
attr_reader :target_tables
|
|
87
|
+
|
|
88
|
+
# @return [Hash] Preservation options
|
|
89
|
+
attr_reader :options
|
|
90
|
+
|
|
91
|
+
# Initialize preserver
|
|
92
|
+
#
|
|
93
|
+
# @param source_font [TrueTypeFont, OpenTypeFont] Variable font
|
|
94
|
+
# @param target_tables [Hash<String, String>] Target font tables
|
|
95
|
+
# @param options [Hash] Preservation options
|
|
96
|
+
# @option options [Boolean] :preserve_format_specific Preserve format-
|
|
97
|
+
# specific variation tables (default: true)
|
|
98
|
+
# @option options [Boolean] :preserve_metrics Preserve metrics variation
|
|
99
|
+
# tables (default: true)
|
|
100
|
+
# @option options [Boolean] :validate Validate table consistency
|
|
101
|
+
# (default: true)
|
|
102
|
+
def initialize(source_font, target_tables, options = {})
|
|
103
|
+
@source_font = source_font
|
|
104
|
+
@target_tables = target_tables.dup
|
|
105
|
+
@options = options
|
|
106
|
+
|
|
107
|
+
validate_source!
|
|
108
|
+
@context = VariationContext.new(source_font)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Preserve variation tables
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash<String, String>] Target tables with variation preserved
|
|
114
|
+
def preserve
|
|
115
|
+
# Copy common variation tables (fvar, avar, STAT)
|
|
116
|
+
copy_common_tables
|
|
117
|
+
|
|
118
|
+
# Copy format-specific variation tables if requested
|
|
119
|
+
if preserve_format_specific?
|
|
120
|
+
copy_format_specific_tables
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Copy metrics variation tables if requested
|
|
124
|
+
copy_metrics_tables if preserve_metrics?
|
|
125
|
+
|
|
126
|
+
# Validate consistency if requested
|
|
127
|
+
validate_consistency if validate?
|
|
128
|
+
|
|
129
|
+
@target_tables
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if source font is a variable font
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] True if source has fvar table
|
|
135
|
+
def variable_font?
|
|
136
|
+
@context.variable_font?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get variation type of source font
|
|
140
|
+
#
|
|
141
|
+
# @return [Symbol, nil] :truetype, :cff2, or nil
|
|
142
|
+
def variation_type
|
|
143
|
+
@context.variation_type
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Validate source font
|
|
149
|
+
#
|
|
150
|
+
# @raise [ArgumentError] If source is invalid
|
|
151
|
+
def validate_source!
|
|
152
|
+
raise ArgumentError, "Source font cannot be nil" if @source_font.nil?
|
|
153
|
+
|
|
154
|
+
unless @source_font.respond_to?(:has_table?) &&
|
|
155
|
+
@source_font.respond_to?(:table_data)
|
|
156
|
+
raise ArgumentError,
|
|
157
|
+
"Source font must respond to :has_table? and :table_data"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if @target_tables.nil?
|
|
161
|
+
raise ArgumentError,
|
|
162
|
+
"Target tables cannot be nil"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
unless @target_tables.is_a?(Hash)
|
|
166
|
+
raise ArgumentError,
|
|
167
|
+
"Target tables must be a Hash, got: #{@target_tables.class}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Copy common variation tables (fvar, avar, STAT)
|
|
172
|
+
#
|
|
173
|
+
# These tables are independent of outline format and can always be copied
|
|
174
|
+
def copy_common_tables
|
|
175
|
+
COMMON_TABLES.each do |tag|
|
|
176
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Copy format-specific variation tables
|
|
181
|
+
#
|
|
182
|
+
# For TrueType: gvar, cvar
|
|
183
|
+
# For CFF2: CFF2 table
|
|
184
|
+
def copy_format_specific_tables
|
|
185
|
+
case variation_type
|
|
186
|
+
when :truetype
|
|
187
|
+
copy_truetype_variation_tables
|
|
188
|
+
when :postscript
|
|
189
|
+
copy_cff2_variation_tables
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Copy TrueType variation tables
|
|
194
|
+
def copy_truetype_variation_tables
|
|
195
|
+
TRUETYPE_TABLES.each do |tag|
|
|
196
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Copy CFF2 variation tables
|
|
201
|
+
def copy_cff2_variation_tables
|
|
202
|
+
# CFF2 table contains both outlines and variation data
|
|
203
|
+
# Only copy if target doesn't already have CFF2 and source has it
|
|
204
|
+
return unless @source_font.has_table?("CFF2")
|
|
205
|
+
return if @target_tables.key?("CFF2")
|
|
206
|
+
|
|
207
|
+
copy_table("CFF2")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Copy metrics variation tables (HVAR, VVAR, MVAR)
|
|
211
|
+
def copy_metrics_tables
|
|
212
|
+
METRICS_TABLES.each do |tag|
|
|
213
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Copy a single table from source to target
|
|
218
|
+
#
|
|
219
|
+
# @param tag [String] Table tag
|
|
220
|
+
def copy_table(tag)
|
|
221
|
+
return unless @source_font.has_table?(tag)
|
|
222
|
+
|
|
223
|
+
table_data = @source_font.table_data[tag]
|
|
224
|
+
return unless table_data
|
|
225
|
+
|
|
226
|
+
@target_tables[tag] = table_data.dup
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Validate table consistency
|
|
230
|
+
#
|
|
231
|
+
# Ensures that copied variation tables are consistent with target font
|
|
232
|
+
# @raise [Error] If validation fails
|
|
233
|
+
def validate_consistency
|
|
234
|
+
# Must have fvar if we're preserving variations
|
|
235
|
+
unless @target_tables.key?("fvar")
|
|
236
|
+
raise Fontisan::Error,
|
|
237
|
+
"Cannot preserve variations: fvar table missing"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# If we have gvar, we must have glyf (TrueType outlines)
|
|
241
|
+
if @target_tables.key?("gvar") && !@target_tables.key?("glyf")
|
|
242
|
+
raise Fontisan::Error,
|
|
243
|
+
"Invalid variation preservation: gvar present without glyf"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# If we have CFF2, we shouldn't have glyf (CFF2 has CFF outlines)
|
|
247
|
+
# Check both source and target to catch conflicts
|
|
248
|
+
has_cff2 = @target_tables.key?("CFF2") ||
|
|
249
|
+
(@source_font.has_table?("CFF2") && preserve_format_specific?)
|
|
250
|
+
if has_cff2 && @target_tables.key?("glyf")
|
|
251
|
+
raise Fontisan::Error,
|
|
252
|
+
"Invalid variation preservation: CFF2 and glyf both present"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Metrics variation tables require fvar
|
|
256
|
+
if metrics_tables_present? && !@target_tables.key?("fvar")
|
|
257
|
+
raise Fontisan::Error,
|
|
258
|
+
"Metrics variation tables require fvar table"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Check if any metrics variation tables are present
|
|
263
|
+
#
|
|
264
|
+
# @return [Boolean] True if HVAR, VVAR, or MVAR present
|
|
265
|
+
def metrics_tables_present?
|
|
266
|
+
METRICS_TABLES.any? { |tag| @target_tables.key?(tag) }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Get preserve_format_specific option
|
|
270
|
+
#
|
|
271
|
+
# @return [Boolean] True if format-specific tables should be preserved
|
|
272
|
+
def preserve_format_specific?
|
|
273
|
+
@options.fetch(:preserve_format_specific, true)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get preserve_metrics option
|
|
277
|
+
#
|
|
278
|
+
# @return [Boolean] True if metrics tables should be preserved
|
|
279
|
+
def preserve_metrics?
|
|
280
|
+
@options.fetch(:preserve_metrics, true)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Get validate option
|
|
284
|
+
#
|
|
285
|
+
# @return [Boolean] True if consistency should be validated
|
|
286
|
+
def validate?
|
|
287
|
+
@options.fetch(:validate, true)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
data/lib/fontisan/version.rb
CHANGED