fontisan 0.4.46 → 0.4.47
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/lib/fontisan/pipeline/transformation_pipeline.rb +10 -8
- data/lib/fontisan/pipeline.rb +1 -1
- data/lib/fontisan/tables/glyf.rb +4 -6
- data/lib/fontisan/validators/opentype_validator.rb +13 -3
- data/lib/fontisan/variable/instancer.rb +22 -192
- data/lib/fontisan/variation/instance_generator.rb +3 -149
- data/lib/fontisan/variation/instancing/axis_space.rb +136 -0
- data/lib/fontisan/variation/instancing/glyph_outline.rb +204 -0
- data/lib/fontisan/variation/instancing/gvar_reader.rb +146 -0
- data/lib/fontisan/variation/instancing/item_variation_store.rb +92 -0
- data/lib/fontisan/variation/instancing/name_rewriter.rb +90 -0
- data/lib/fontisan/variation/instancing/true_type_instancer.rb +318 -0
- data/lib/fontisan/variation/instancing.rb +20 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +19 -1
- data/lib/fontisan/variation.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 999790a5a5e57d822deee3a86f540b3dac9b8bef06c234c883cfaacb87e9f0b1
|
|
4
|
+
data.tar.gz: c376a3129b02bec87f5754656a66bef27695e0f445ef5740a60dc8a60013b71e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 481c4a053c4aece81d321e5bc6f76abb7633f6f2d987c901bcba4587dbed9a40462dd192a9f9cd71166114b0a5c0fc6bbc628e7cb71b1c043423cf321bca19ec
|
|
7
|
+
data.tar.gz: b367817e918c9ec6de0e346180c9e45f2a2afa73d4c223997357e7b2d38ed445ce04aff98b05da36e1d297d63a71c29728c0e8e9807a79c2567a2c47b9544645
|
|
@@ -216,7 +216,7 @@ module Fontisan
|
|
|
216
216
|
return tables if source_format == target
|
|
217
217
|
|
|
218
218
|
# Use FormatConverter for outline conversion
|
|
219
|
-
if needs_outline_conversion?(
|
|
219
|
+
if needs_outline_conversion?(tables, target) || target == :svg
|
|
220
220
|
converter = Converters::FormatConverter.new
|
|
221
221
|
# Create temporary font object from tables
|
|
222
222
|
font = build_font_from_tables(tables, source_format)
|
|
@@ -229,16 +229,18 @@ module Fontisan
|
|
|
229
229
|
|
|
230
230
|
# Check if outline conversion is needed
|
|
231
231
|
#
|
|
232
|
-
#
|
|
232
|
+
# TTF <-> OTF changes the outline format. WOFF/WOFF2 targets only
|
|
233
|
+
# repackage whichever outlines the font already has.
|
|
234
|
+
#
|
|
235
|
+
# @param tables [Hash] Font tables (their outlines decide the source)
|
|
233
236
|
# @param target [Symbol] Target format
|
|
234
237
|
# @return [Boolean] True if outline conversion needed
|
|
235
|
-
def needs_outline_conversion?(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
otf_formats = %i[otf otc]
|
|
238
|
+
def needs_outline_conversion?(tables, target)
|
|
239
|
+
target_outline = { ttf: :glyf, ttc: :glyf, otf: :cff, otc: :cff }[target]
|
|
240
|
+
return false unless target_outline
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
source_outline = tables.key?("glyf") ? :glyf : :cff
|
|
243
|
+
source_outline != target_outline
|
|
242
244
|
end
|
|
243
245
|
|
|
244
246
|
# Write output font file
|
data/lib/fontisan/pipeline.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Fontisan
|
|
|
6
6
|
module Pipeline
|
|
7
7
|
autoload :FormatDetector, "fontisan/pipeline/format_detector"
|
|
8
8
|
autoload :OutputWriter, "fontisan/pipeline/output_writer"
|
|
9
|
-
autoload :Strategies, "fontisan/pipeline/strategies
|
|
9
|
+
autoload :Strategies, "fontisan/pipeline/strategies"
|
|
10
10
|
autoload :TransformationPipeline,
|
|
11
11
|
"fontisan/pipeline/transformation_pipeline"
|
|
12
12
|
autoload :VariationResolver, "fontisan/pipeline/variation_resolver"
|
data/lib/fontisan/tables/glyf.rb
CHANGED
|
@@ -241,13 +241,11 @@ module Fontisan
|
|
|
241
241
|
glyph = glyph_for(glyph_id, loca, head)
|
|
242
242
|
return true if glyph.nil? # Empty glyphs are OK
|
|
243
243
|
|
|
244
|
-
# Simple glyphs: contours
|
|
245
|
-
#
|
|
244
|
+
# Simple glyphs: contours must be >= 0. Compound glyphs always have
|
|
245
|
+
# numberOfContours = -1; CompoundGlyph.parse rejects anything else.
|
|
246
246
|
case glyph
|
|
247
|
-
when SimpleGlyph
|
|
248
|
-
|
|
249
|
-
else
|
|
250
|
-
true
|
|
247
|
+
when SimpleGlyph then glyph.num_contours >= 0
|
|
248
|
+
else true
|
|
251
249
|
end
|
|
252
250
|
rescue StandardError
|
|
253
251
|
false
|
|
@@ -44,7 +44,7 @@ module Fontisan
|
|
|
44
44
|
glyf = font.table("glyf")
|
|
45
45
|
next true unless glyf # Skip if CFF font
|
|
46
46
|
|
|
47
|
-
loca = font
|
|
47
|
+
loca = parsed_loca(font)
|
|
48
48
|
head = font.table("head")
|
|
49
49
|
maxp = font.table("maxp")
|
|
50
50
|
glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
|
|
@@ -55,7 +55,7 @@ module Fontisan
|
|
|
55
55
|
glyf = font.table("glyf")
|
|
56
56
|
next true unless glyf
|
|
57
57
|
|
|
58
|
-
loca = font
|
|
58
|
+
loca = parsed_loca(font)
|
|
59
59
|
head = font.table("head")
|
|
60
60
|
maxp = font.table("maxp")
|
|
61
61
|
glyf.no_clipped_glyphs?(loca, head, maxp.num_glyphs)
|
|
@@ -66,7 +66,7 @@ module Fontisan
|
|
|
66
66
|
glyf = font.table("glyf")
|
|
67
67
|
next true unless glyf
|
|
68
68
|
|
|
69
|
-
loca = font
|
|
69
|
+
loca = parsed_loca(font)
|
|
70
70
|
head = font.table("head")
|
|
71
71
|
maxp = font.table("maxp")
|
|
72
72
|
|
|
@@ -101,6 +101,16 @@ module Fontisan
|
|
|
101
101
|
true # Placeholder - actual checksum validation if desired
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
|
+
|
|
105
|
+
# loca must be parsed with head/maxp context before glyf can read it.
|
|
106
|
+
def parsed_loca(font)
|
|
107
|
+
loca = font.table("loca")
|
|
108
|
+
return loca if loca.parsed?
|
|
109
|
+
|
|
110
|
+
loca.parse_with_context(font.table("head").index_to_loc_format,
|
|
111
|
+
font.table("maxp").num_glyphs)
|
|
112
|
+
loca
|
|
113
|
+
end
|
|
104
114
|
end
|
|
105
115
|
end
|
|
106
116
|
end
|
|
@@ -25,12 +25,6 @@ module Fontisan
|
|
|
25
25
|
# @return [Object] The variable font object
|
|
26
26
|
attr_reader :font
|
|
27
27
|
|
|
28
|
-
# @return [DeltaApplicator] Delta applicator
|
|
29
|
-
attr_reader :delta_applicator
|
|
30
|
-
|
|
31
|
-
# @return [StaticFontBuilder] Static font builder
|
|
32
|
-
attr_reader :static_font_builder
|
|
33
|
-
|
|
34
28
|
# Initialize the instancer
|
|
35
29
|
#
|
|
36
30
|
# @param font [TrueTypeFont, OpenTypeFont] Variable font object
|
|
@@ -38,9 +32,6 @@ module Fontisan
|
|
|
38
32
|
def initialize(font)
|
|
39
33
|
@font = font
|
|
40
34
|
validate_variable_font!
|
|
41
|
-
|
|
42
|
-
@delta_applicator = DeltaApplicator.new(font)
|
|
43
|
-
@static_font_builder = StaticFontBuilder.new(font)
|
|
44
35
|
end
|
|
45
36
|
|
|
46
37
|
# Generate static font instance at specified coordinates
|
|
@@ -53,21 +44,10 @@ module Fontisan
|
|
|
53
44
|
#
|
|
54
45
|
# @example
|
|
55
46
|
# binary = instancer.instance({ "wght" => 700, "wdth" => 100 })
|
|
56
|
-
def instance(user_coords,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Collect varied metrics for all glyphs
|
|
61
|
-
varied_metrics = collect_varied_glyph_metrics(
|
|
62
|
-
delta_result[:normalized_coords],
|
|
63
|
-
delta_result[:region_scalars],
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Extract font-level metrics
|
|
67
|
-
font_metrics = delta_result[:font_metrics]
|
|
68
|
-
|
|
69
|
-
# Build static font
|
|
70
|
-
@static_font_builder.build(varied_metrics, font_metrics, options)
|
|
47
|
+
def instance(user_coords, _options = {})
|
|
48
|
+
tables = Variation::Instancing::TrueTypeInstancer
|
|
49
|
+
.new(@font.table_data, user_coords).call
|
|
50
|
+
FontWriter.write_font(tables)
|
|
71
51
|
end
|
|
72
52
|
|
|
73
53
|
# Generate static font instance and write to file
|
|
@@ -117,23 +97,16 @@ module Fontisan
|
|
|
117
97
|
# @return [Array<Hash>] Array of named instance information
|
|
118
98
|
# [{ name: "Bold", coords: { "wght" => 700 } }, ...]
|
|
119
99
|
def named_instances
|
|
120
|
-
fvar =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
fvar.instances.map do |instance|
|
|
127
|
-
coords = extract_instance_coords(instance, fvar)
|
|
128
|
-
name = if name_table
|
|
129
|
-
get_instance_name(instance[:name_id],
|
|
130
|
-
name_table)
|
|
131
|
-
end
|
|
132
|
-
|
|
100
|
+
fvar = @font.table_data["fvar"]
|
|
101
|
+
names = if @font.table_data["name"]
|
|
102
|
+
Variation::Instancing::NameRewriter.new(@font.table_data["name"])
|
|
103
|
+
end
|
|
104
|
+
tags = axis_tags
|
|
105
|
+
Variation::Instancing::AxisSpace.named_instances(fvar).map do |inst|
|
|
133
106
|
{
|
|
134
|
-
name_id:
|
|
135
|
-
name:
|
|
136
|
-
coordinates:
|
|
107
|
+
name_id: inst[:name_id],
|
|
108
|
+
name: names&.lookup(inst[:name_id]) || "Instance #{inst[:name_id]}",
|
|
109
|
+
coordinates: tags.zip(inst[:values]).to_h,
|
|
137
110
|
}
|
|
138
111
|
end
|
|
139
112
|
end
|
|
@@ -142,21 +115,21 @@ module Fontisan
|
|
|
142
115
|
#
|
|
143
116
|
# @return [Boolean] True if variable font
|
|
144
117
|
def variable_font?
|
|
145
|
-
|
|
118
|
+
true
|
|
146
119
|
end
|
|
147
120
|
|
|
148
121
|
# Get available axis information
|
|
149
122
|
#
|
|
150
|
-
# @return [
|
|
123
|
+
# @return [Array<Variation::Instancing::AxisSpace::Axis>]
|
|
151
124
|
def axes
|
|
152
|
-
|
|
125
|
+
axis_space.axes
|
|
153
126
|
end
|
|
154
127
|
|
|
155
128
|
# Get available axis tags
|
|
156
129
|
#
|
|
157
130
|
# @return [Array<String>] Array of axis tags
|
|
158
131
|
def axis_tags
|
|
159
|
-
|
|
132
|
+
axis_space.tags
|
|
160
133
|
end
|
|
161
134
|
|
|
162
135
|
private
|
|
@@ -165,60 +138,16 @@ module Fontisan
|
|
|
165
138
|
#
|
|
166
139
|
# @raise [ArgumentError] If not a variable font
|
|
167
140
|
def validate_variable_font!
|
|
168
|
-
fvar_data = @font.table_data
|
|
141
|
+
fvar_data = @font.table_data["fvar"]
|
|
169
142
|
return unless fvar_data.nil? || fvar_data.empty?
|
|
170
143
|
|
|
171
144
|
raise ArgumentError, "Font is not a variable font (missing fvar table)"
|
|
172
145
|
end
|
|
173
146
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# @return [Hash<Integer, Hash>] Varied metrics by glyph ID
|
|
179
|
-
def collect_varied_glyph_metrics(normalized_coords, _region_scalars)
|
|
180
|
-
varied_metrics = {}
|
|
181
|
-
|
|
182
|
-
# Get number of glyphs
|
|
183
|
-
maxp = load_maxp_table
|
|
184
|
-
return varied_metrics unless maxp
|
|
185
|
-
|
|
186
|
-
num_glyphs = maxp.num_glyphs
|
|
187
|
-
|
|
188
|
-
# Get original hmtx metrics
|
|
189
|
-
hmtx = load_hmtx_table
|
|
190
|
-
return varied_metrics unless hmtx
|
|
191
|
-
|
|
192
|
-
# For each glyph, calculate varied metrics
|
|
193
|
-
num_glyphs.times do |glyph_id|
|
|
194
|
-
original_metric = hmtx.metric_for(glyph_id)
|
|
195
|
-
next unless original_metric
|
|
196
|
-
|
|
197
|
-
# Get deltas from delta applicator
|
|
198
|
-
deltas = @delta_applicator.apply_glyph(glyph_id, normalized_coords)
|
|
199
|
-
metric_deltas = deltas[:metric_deltas]
|
|
200
|
-
|
|
201
|
-
# Calculate varied values
|
|
202
|
-
varied_metric = {
|
|
203
|
-
advance_width: original_metric[:advance_width],
|
|
204
|
-
lsb: original_metric[:lsb],
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
# Apply horizontal deltas if present
|
|
208
|
-
if metric_deltas[:horizontal]
|
|
209
|
-
if metric_deltas[:horizontal][:advance_width]
|
|
210
|
-
varied_metric[:advance_width] += metric_deltas[:horizontal][:advance_width]
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
if metric_deltas[:horizontal][:lsb]
|
|
214
|
-
varied_metric[:lsb] += metric_deltas[:horizontal][:lsb]
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
varied_metrics[glyph_id] = varied_metric
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
varied_metrics
|
|
147
|
+
def axis_space
|
|
148
|
+
@axis_space ||= Variation::Instancing::AxisSpace.new(
|
|
149
|
+
@font.table_data["fvar"], @font.table_data["avar"]
|
|
150
|
+
)
|
|
222
151
|
end
|
|
223
152
|
|
|
224
153
|
# Find coordinates for a named instance
|
|
@@ -237,105 +166,6 @@ module Fontisan
|
|
|
237
166
|
|
|
238
167
|
instance[:coordinates]
|
|
239
168
|
end
|
|
240
|
-
|
|
241
|
-
# Extract coordinates from instance record
|
|
242
|
-
#
|
|
243
|
-
# @param instance [Hash] Instance record
|
|
244
|
-
# @param fvar [Fvar] fvar table
|
|
245
|
-
# @return [Hash<String, Float>] Coordinates by axis tag
|
|
246
|
-
def extract_instance_coords(instance, fvar)
|
|
247
|
-
coords = {}
|
|
248
|
-
instance[:coordinates].each_with_index do |value, index|
|
|
249
|
-
axis = fvar.axes[index]
|
|
250
|
-
coords[axis.axis_tag.to_s] = value if axis
|
|
251
|
-
end
|
|
252
|
-
coords
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Get instance name from name table
|
|
256
|
-
#
|
|
257
|
-
# @param name_id [Integer] Name table ID
|
|
258
|
-
# @param name_table [Name] name table
|
|
259
|
-
# @return [String, nil] Instance name
|
|
260
|
-
def get_instance_name(name_id, name_table)
|
|
261
|
-
# Try to get English name
|
|
262
|
-
record = name_table.records.find do |r|
|
|
263
|
-
r.name_id == name_id && r.language_id == 0x0409 # English (US)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
record ||= name_table.records.find { |r| r.name_id == name_id }
|
|
267
|
-
record&.value
|
|
268
|
-
rescue StandardError
|
|
269
|
-
nil
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Load fvar table
|
|
273
|
-
#
|
|
274
|
-
# @return [Fvar, nil] fvar table or nil
|
|
275
|
-
def load_fvar_table
|
|
276
|
-
data = @font.table_data("fvar")
|
|
277
|
-
return nil if data.nil? || data.empty?
|
|
278
|
-
|
|
279
|
-
Tables::Fvar.read(data)
|
|
280
|
-
rescue StandardError
|
|
281
|
-
nil
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# Load name table
|
|
285
|
-
#
|
|
286
|
-
# @return [Name, nil] name table or nil
|
|
287
|
-
def load_name_table
|
|
288
|
-
data = @font.table_data("name")
|
|
289
|
-
return nil if data.nil? || data.empty?
|
|
290
|
-
|
|
291
|
-
Tables::Name.read(data)
|
|
292
|
-
rescue StandardError
|
|
293
|
-
nil
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Load maxp table
|
|
297
|
-
#
|
|
298
|
-
# @return [Maxp, nil] maxp table or nil
|
|
299
|
-
def load_maxp_table
|
|
300
|
-
data = @font.table_data("maxp")
|
|
301
|
-
return nil if data.nil? || data.empty?
|
|
302
|
-
|
|
303
|
-
Tables::Maxp.read(data)
|
|
304
|
-
rescue StandardError
|
|
305
|
-
nil
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Load hmtx table
|
|
309
|
-
#
|
|
310
|
-
# @return [Hmtx, nil] hmtx table or nil
|
|
311
|
-
def load_hmtx_table
|
|
312
|
-
data = @font.table_data("hmtx")
|
|
313
|
-
return nil if data.nil? || data.empty?
|
|
314
|
-
|
|
315
|
-
hmtx = Tables::Hmtx.read(data)
|
|
316
|
-
|
|
317
|
-
# Parse with context
|
|
318
|
-
hhea = load_hhea_table
|
|
319
|
-
maxp = load_maxp_table
|
|
320
|
-
return nil unless hhea && maxp
|
|
321
|
-
|
|
322
|
-
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
323
|
-
hmtx
|
|
324
|
-
rescue StandardError
|
|
325
|
-
nil
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Load hhea table
|
|
329
|
-
#
|
|
330
|
-
# @return [Hhea, nil] hhea table or nil
|
|
331
|
-
def load_hhea_table
|
|
332
|
-
data = @font.table_data("hhea")
|
|
333
|
-
return nil if data.nil? || data.empty?
|
|
334
|
-
|
|
335
|
-
Tables::Hhea.read(data)
|
|
336
|
-
rescue StandardError
|
|
337
|
-
nil
|
|
338
|
-
end
|
|
339
169
|
end
|
|
340
170
|
end
|
|
341
171
|
end
|
|
@@ -54,29 +54,12 @@ module Fontisan
|
|
|
54
54
|
|
|
55
55
|
# Generate static font instance
|
|
56
56
|
#
|
|
57
|
-
# Applies
|
|
57
|
+
# Applies gvar/HVAR deltas at the coordinates to the outlines and
|
|
58
|
+
# metrics and drops the variation tables. TrueType (gvar) fonts only.
|
|
58
59
|
#
|
|
59
60
|
# @return [Hash<String, String>] Map of table tags to binary data
|
|
60
61
|
def generate
|
|
61
|
-
|
|
62
|
-
tables = @font.table_data.dup
|
|
63
|
-
|
|
64
|
-
# Determine variation type
|
|
65
|
-
if has_variation_table?("gvar")
|
|
66
|
-
# TrueType outlines with gvar
|
|
67
|
-
apply_gvar_deltas(tables)
|
|
68
|
-
elsif has_variation_table?("CFF2")
|
|
69
|
-
# PostScript outlines with CFF2 blend
|
|
70
|
-
apply_cff2_blend(tables)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Apply metrics variations if present
|
|
74
|
-
apply_metrics_deltas(tables) if @context.has_metrics_variations?
|
|
75
|
-
|
|
76
|
-
# Remove variation-specific tables to create static font
|
|
77
|
-
remove_variation_tables(tables)
|
|
78
|
-
|
|
79
|
-
tables
|
|
62
|
+
Instancing::TrueTypeInstancer.new(@font.table_data, @coordinates).call
|
|
80
63
|
end
|
|
81
64
|
|
|
82
65
|
# Generate a named instance
|
|
@@ -96,135 +79,6 @@ module Fontisan
|
|
|
96
79
|
generate
|
|
97
80
|
end
|
|
98
81
|
|
|
99
|
-
# Apply gvar deltas to TrueType outlines
|
|
100
|
-
#
|
|
101
|
-
# @param tables [Hash<String, String>] Font tables
|
|
102
|
-
def apply_gvar_deltas(_tables)
|
|
103
|
-
gvar = variation_table("gvar")
|
|
104
|
-
glyf = @font.table("glyf")
|
|
105
|
-
return unless gvar && glyf
|
|
106
|
-
|
|
107
|
-
# Get glyph count
|
|
108
|
-
maxp = @font.table("maxp")
|
|
109
|
-
glyph_count = maxp ? maxp.num_glyphs : gvar.glyph_count
|
|
110
|
-
|
|
111
|
-
# Process each glyph
|
|
112
|
-
glyph_count.times do |glyph_id|
|
|
113
|
-
apply_glyph_deltas(glyph_id, gvar, glyf)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Rebuild glyf and loca tables with adjusted outlines
|
|
117
|
-
# This is a placeholder - full implementation would reconstruct tables
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Apply deltas to a specific glyph
|
|
121
|
-
#
|
|
122
|
-
# @param glyph_id [Integer] Glyph ID
|
|
123
|
-
# @param gvar [Gvar] Gvar table
|
|
124
|
-
# @param glyf [Glyf] Glyf table
|
|
125
|
-
def apply_glyph_deltas(glyph_id, gvar, _glyf)
|
|
126
|
-
# Get tuple variations for this glyph
|
|
127
|
-
tuple_data = gvar.glyph_tuple_variations(glyph_id)
|
|
128
|
-
return unless tuple_data
|
|
129
|
-
|
|
130
|
-
# Match tuples to current coordinates
|
|
131
|
-
matches = @context.region_matcher.match_tuples(
|
|
132
|
-
coordinates: @coordinates,
|
|
133
|
-
tuples: tuple_data[:tuples],
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
nil if matches.empty?
|
|
137
|
-
|
|
138
|
-
# Get base glyph outline
|
|
139
|
-
# Apply matched deltas with their scalars
|
|
140
|
-
# This is a placeholder - full implementation would:
|
|
141
|
-
# 1. Parse glyph outline points
|
|
142
|
-
# 2. Parse delta data for each tuple
|
|
143
|
-
# 3. Apply: new_point = base_point + Σ(delta * scalar)
|
|
144
|
-
# 4. Update glyph outline
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Apply CFF2 blend operators
|
|
148
|
-
#
|
|
149
|
-
# @param tables [Hash<String, String>] Font tables
|
|
150
|
-
def apply_cff2_blend(_tables)
|
|
151
|
-
cff2 = variation_table("CFF2")
|
|
152
|
-
return unless cff2
|
|
153
|
-
|
|
154
|
-
# Set number of axes for CFF2
|
|
155
|
-
cff2.num_axes = @context.axis_count
|
|
156
|
-
|
|
157
|
-
# Process each glyph's CharString
|
|
158
|
-
glyph_count = cff2.glyph_count
|
|
159
|
-
return if glyph_count.zero?
|
|
160
|
-
|
|
161
|
-
# Calculate variation scalars once
|
|
162
|
-
calculate_variation_scalars
|
|
163
|
-
|
|
164
|
-
# Apply blend to each glyph
|
|
165
|
-
# This is a placeholder - full implementation would:
|
|
166
|
-
# 1. Parse CharString with blend operators
|
|
167
|
-
# 2. Apply scalars to blend operands
|
|
168
|
-
# 3. Rebuild CharStrings without blend operators
|
|
169
|
-
# 4. Update CFF2 table
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Calculate variation scalars for current coordinates
|
|
173
|
-
#
|
|
174
|
-
# @return [Array<Float>] Scalars for each axis
|
|
175
|
-
def calculate_variation_scalars
|
|
176
|
-
@context.axes.map do |axis|
|
|
177
|
-
coord = @coordinates[axis.axis_tag] || axis.default_value
|
|
178
|
-
@context.interpolator.normalize_coordinate(coord, axis.axis_tag)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Apply metrics variations
|
|
183
|
-
#
|
|
184
|
-
# @param tables [Hash<String, String>] Font tables
|
|
185
|
-
def apply_metrics_deltas(tables)
|
|
186
|
-
# Apply HVAR (horizontal metrics)
|
|
187
|
-
apply_hvar_deltas(tables) if has_variation_table?("HVAR")
|
|
188
|
-
|
|
189
|
-
# Apply VVAR (vertical metrics)
|
|
190
|
-
apply_vvar_deltas(tables) if has_variation_table?("VVAR")
|
|
191
|
-
|
|
192
|
-
# Apply MVAR (font-wide metrics)
|
|
193
|
-
apply_mvar_deltas(tables) if has_variation_table?("MVAR")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Apply HVAR deltas to horizontal metrics
|
|
197
|
-
#
|
|
198
|
-
# @param tables [Hash<String, String>] Font tables
|
|
199
|
-
def apply_hvar_deltas(_tables)
|
|
200
|
-
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
201
|
-
adjuster.apply_hvar_deltas(@coordinates)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Apply VVAR deltas to vertical metrics
|
|
205
|
-
#
|
|
206
|
-
# @param tables [Hash<String, String>] Font tables
|
|
207
|
-
def apply_vvar_deltas(_tables)
|
|
208
|
-
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
209
|
-
adjuster.apply_vvar_deltas(@coordinates)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Apply MVAR deltas to font-wide metrics
|
|
213
|
-
#
|
|
214
|
-
# @param tables [Hash<String, String>] Font tables
|
|
215
|
-
def apply_mvar_deltas(_tables)
|
|
216
|
-
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
217
|
-
adjuster.apply_mvar_deltas(@coordinates)
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Remove variation tables from static font
|
|
221
|
-
#
|
|
222
|
-
# @param tables [Hash<String, String>] Font tables
|
|
223
|
-
def remove_variation_tables(tables)
|
|
224
|
-
variation_tables = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
|
|
225
|
-
variation_tables.each { |tag| tables.delete(tag) }
|
|
226
|
-
end
|
|
227
|
-
|
|
228
82
|
# Interpolate a single value
|
|
229
83
|
#
|
|
230
84
|
# @param base_value [Numeric] Base value
|