fontisan 0.4.45 → 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/Rakefile +1 -2
- data/lib/fontisan/cli.rb +1 -1
- data/lib/fontisan/commands/export_command.rb +3 -3
- data/lib/fontisan/export/exporter.rb +1 -1
- data/lib/fontisan/export/table_serializer.rb +36 -38
- data/lib/fontisan/pipeline/transformation_pipeline.rb +10 -8
- data/lib/fontisan/pipeline.rb +1 -1
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +1 -2
- data/lib/fontisan/tables/cff/standard_strings_data.rb +1 -2
- data/lib/fontisan/tables/glyf.rb +4 -6
- data/lib/fontisan/tables/gpos/glyph_filter.rb +87 -0
- data/lib/fontisan/tables/gpos/kerning_pairs.rb +215 -0
- data/lib/fontisan/tables/gpos/pair_position.rb +274 -0
- data/lib/fontisan/tables/gpos.rb +23 -0
- data/lib/fontisan/tables/post.rb +2 -4
- 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 +12 -3
- data/fontisan.gemspec +0 -48
|
@@ -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
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
module Instancing
|
|
6
|
+
# The design space of a variable font: fvar axes plus avar mapping.
|
|
7
|
+
# Validates user coordinates and normalizes them to -1..1.
|
|
8
|
+
class AxisSpace
|
|
9
|
+
Axis = Struct.new(:tag, :minimum, :default, :maximum, :name_id)
|
|
10
|
+
|
|
11
|
+
attr_reader :axes
|
|
12
|
+
|
|
13
|
+
# @param fvar [String] raw fvar table
|
|
14
|
+
# @param avar [String, nil] raw avar table
|
|
15
|
+
def initialize(fvar, avar = nil)
|
|
16
|
+
@axes = parse_fvar(fvar)
|
|
17
|
+
@segments = avar ? parse_avar(avar) : {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tags
|
|
21
|
+
@axes.map(&:tag)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Named instances: [{ name_id:, coordinates: { tag => value } }]
|
|
25
|
+
def self.named_instances(fvar)
|
|
26
|
+
offset, axis_count, axis_size, inst_count, inst_size = header(fvar)
|
|
27
|
+
base = offset + (axis_count * axis_size)
|
|
28
|
+
Array.new(inst_count) do |i|
|
|
29
|
+
pos = base + (i * inst_size)
|
|
30
|
+
name_id = fvar[pos, 2].unpack1("n")
|
|
31
|
+
coords = Array.new(axis_count) do |a|
|
|
32
|
+
fixed(fvar, pos + 4 + (a * 4))
|
|
33
|
+
end
|
|
34
|
+
{ name_id: name_id, values: coords }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.header(fvar)
|
|
39
|
+
offset, _res, axis_count, axis_size, inst_count, inst_size =
|
|
40
|
+
fvar[4, 12].unpack("nnnnnn")
|
|
41
|
+
[offset, axis_count, axis_size, inst_count, inst_size]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.fixed(data, pos)
|
|
45
|
+
data[pos, 4].unpack1("l>") / 65536.0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param requested [Hash{String=>Numeric}] user coordinates
|
|
49
|
+
# @return [Hash{String=>Float}] full user location (defaults filled)
|
|
50
|
+
# @raise [InvalidCoordinatesError, InvalidAxisError]
|
|
51
|
+
def validate(requested)
|
|
52
|
+
known = tags
|
|
53
|
+
requested.each_key do |tag|
|
|
54
|
+
next if known.include?(tag.to_s)
|
|
55
|
+
|
|
56
|
+
raise InvalidAxisError.new(
|
|
57
|
+
axis: tag.to_s,
|
|
58
|
+
reason: "font has axes #{known.join(', ')}",
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
@axes.to_h do |axis|
|
|
62
|
+
value = requested.transform_keys(&:to_s)[axis.tag]
|
|
63
|
+
value = axis.default if value.nil?
|
|
64
|
+
value = Float(value)
|
|
65
|
+
unless value.between?(axis.minimum, axis.maximum)
|
|
66
|
+
raise InvalidCoordinatesError.new(
|
|
67
|
+
axis: axis.tag, value: value, range: [axis.minimum, axis.maximum],
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
[axis.tag, value]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Array<Float>] normalized (avar-mapped) coordinates, fvar order
|
|
75
|
+
def normalize(location)
|
|
76
|
+
@axes.map do |axis|
|
|
77
|
+
value = location.fetch(axis.tag)
|
|
78
|
+
n = if value < axis.default
|
|
79
|
+
(value - axis.default) / (axis.default - axis.minimum)
|
|
80
|
+
elsif value > axis.default
|
|
81
|
+
(value - axis.default) / (axis.maximum - axis.default)
|
|
82
|
+
else
|
|
83
|
+
0.0
|
|
84
|
+
end
|
|
85
|
+
map_segment(axis.tag, n.clamp(-1.0, 1.0))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def parse_fvar(fvar)
|
|
92
|
+
offset, axis_count, axis_size, = self.class.header(fvar)
|
|
93
|
+
Array.new(axis_count) do |i|
|
|
94
|
+
pos = offset + (i * axis_size)
|
|
95
|
+
Axis.new(fvar[pos, 4], self.class.fixed(fvar, pos + 4),
|
|
96
|
+
self.class.fixed(fvar, pos + 8),
|
|
97
|
+
self.class.fixed(fvar, pos + 12),
|
|
98
|
+
fvar[pos + 18, 2].unpack1("n"))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# avar v1: per axis, array of [from, to] pairs (F2Dot14)
|
|
103
|
+
def parse_avar(avar)
|
|
104
|
+
axis_count = avar[6, 2].unpack1("n")
|
|
105
|
+
pos = 8
|
|
106
|
+
segments = {}
|
|
107
|
+
axis_count.times do |i|
|
|
108
|
+
count = avar[pos, 2].unpack1("n")
|
|
109
|
+
pos += 2
|
|
110
|
+
pairs = Array.new(count) do |j|
|
|
111
|
+
from, to = avar[pos + (j * 4), 4].unpack("s>s>")
|
|
112
|
+
[from / 16384.0, to / 16384.0]
|
|
113
|
+
end
|
|
114
|
+
pos += count * 4
|
|
115
|
+
segments[@axes[i].tag] = pairs if @axes[i]
|
|
116
|
+
end
|
|
117
|
+
segments
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def map_segment(tag, value)
|
|
121
|
+
pairs = @segments[tag]
|
|
122
|
+
return value if pairs.nil? || pairs.empty?
|
|
123
|
+
|
|
124
|
+
exact = pairs.find { |from, _| from == value }
|
|
125
|
+
return exact[1] if exact
|
|
126
|
+
|
|
127
|
+
idx = pairs.index { |from, _| from > value }
|
|
128
|
+
return value if idx.nil? || idx.zero?
|
|
129
|
+
|
|
130
|
+
(f0, t0), (f1, t1) = pairs[idx - 1], pairs[idx]
|
|
131
|
+
t0 + ((t1 - t0) * (value - f0) / (f1 - f0))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
module Instancing
|
|
6
|
+
# A parsed glyf entry that can be moved by deltas and re-serialized.
|
|
7
|
+
# Points are the simple-glyph points, or for composites one point per
|
|
8
|
+
# component (its x/y offset), matching gvar's point numbering.
|
|
9
|
+
class GlyphOutline
|
|
10
|
+
ON_CURVE = 0x01
|
|
11
|
+
X_SHORT = 0x02
|
|
12
|
+
Y_SHORT = 0x04
|
|
13
|
+
REPEAT = 0x08
|
|
14
|
+
X_SAME = 0x10
|
|
15
|
+
Y_SAME = 0x20
|
|
16
|
+
|
|
17
|
+
ARGS_ARE_WORDS = 0x0001
|
|
18
|
+
ARGS_ARE_XY = 0x0002
|
|
19
|
+
HAVE_SCALE = 0x0008
|
|
20
|
+
MORE_COMPONENTS = 0x0020
|
|
21
|
+
HAVE_XY_SCALE = 0x0040
|
|
22
|
+
HAVE_TWO_BY_TWO = 0x0080
|
|
23
|
+
|
|
24
|
+
Component = Struct.new(:flags, :gid, :arg1, :arg2, :transform)
|
|
25
|
+
|
|
26
|
+
attr_reader :end_pts, :on_curve, :overlap, :instructions, :components,
|
|
27
|
+
:trailer, :header_contours
|
|
28
|
+
|
|
29
|
+
def self.parse(bytes)
|
|
30
|
+
new(bytes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(bytes)
|
|
34
|
+
@header_contours = 0
|
|
35
|
+
@end_pts = []
|
|
36
|
+
@on_curve = []
|
|
37
|
+
@components = []
|
|
38
|
+
@points = []
|
|
39
|
+
return if bytes.nil? || bytes.empty?
|
|
40
|
+
|
|
41
|
+
@header_contours = bytes[0, 2].unpack1("s>")
|
|
42
|
+
if @header_contours >= 0
|
|
43
|
+
parse_simple(bytes)
|
|
44
|
+
else
|
|
45
|
+
parse_composite(bytes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def empty?
|
|
50
|
+
@header_contours.zero? && @points.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def composite?
|
|
54
|
+
@header_contours.negative?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<Array<Float>>] [[x, y], ...]
|
|
58
|
+
def points
|
|
59
|
+
@points.map(&:dup)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def point_count
|
|
63
|
+
@points.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def bounds_of(points)
|
|
67
|
+
return nil if points.empty?
|
|
68
|
+
|
|
69
|
+
xs = points.map(&:first)
|
|
70
|
+
ys = points.map(&:last)
|
|
71
|
+
[xs.min, ys.min, xs.max, ys.max]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Serialize with the given (already rounded) points. +bbox+ is the
|
|
75
|
+
# [xMin, yMin, xMax, yMax] to record in the header.
|
|
76
|
+
def serialize(new_points, bbox)
|
|
77
|
+
return "".b if empty?
|
|
78
|
+
|
|
79
|
+
composite? ? serialize_composite(new_points, bbox) : serialize_simple(new_points, bbox)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def parse_simple(bytes)
|
|
85
|
+
n = @header_contours
|
|
86
|
+
@end_pts = bytes[10, n * 2].unpack("n*")
|
|
87
|
+
pos = 10 + (n * 2)
|
|
88
|
+
ilen = bytes[pos, 2].unpack1("n")
|
|
89
|
+
@instructions = bytes[pos + 2, ilen]
|
|
90
|
+
pos += 2 + ilen
|
|
91
|
+
count = n.zero? ? 0 : @end_pts.last + 1
|
|
92
|
+
flags = []
|
|
93
|
+
while flags.size < count
|
|
94
|
+
f = bytes.getbyte(pos)
|
|
95
|
+
pos += 1
|
|
96
|
+
flags << f
|
|
97
|
+
if f.anybits?(REPEAT)
|
|
98
|
+
bytes.getbyte(pos).times { flags << f }
|
|
99
|
+
pos += 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
xs = read_coords(bytes, pos, flags, X_SHORT, X_SAME)
|
|
103
|
+
pos = xs.pop
|
|
104
|
+
ys = read_coords(bytes, pos, flags, Y_SHORT, Y_SAME)
|
|
105
|
+
ys.pop
|
|
106
|
+
@on_curve = flags.map { |f| f.anybits?(ON_CURVE) }
|
|
107
|
+
@overlap = flags.first&.anybits?(0x40) || false
|
|
108
|
+
@points = xs.zip(ys)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read_coords(bytes, pos, flags, short, same)
|
|
112
|
+
val = 0
|
|
113
|
+
out = flags.map do |f|
|
|
114
|
+
if f.anybits?(short)
|
|
115
|
+
d = bytes.getbyte(pos)
|
|
116
|
+
pos += 1
|
|
117
|
+
val += f.anybits?(same) ? d : -d
|
|
118
|
+
elsif !f.anybits?(same)
|
|
119
|
+
val += bytes[pos, 2].unpack1("s>")
|
|
120
|
+
pos += 2
|
|
121
|
+
end
|
|
122
|
+
val
|
|
123
|
+
end
|
|
124
|
+
out << pos
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_composite(bytes)
|
|
128
|
+
pos = 10
|
|
129
|
+
loop do
|
|
130
|
+
flags, gid = bytes[pos, 4].unpack("nn")
|
|
131
|
+
pos += 4
|
|
132
|
+
if flags.anybits?(ARGS_ARE_WORDS)
|
|
133
|
+
fmt = flags.anybits?(ARGS_ARE_XY) ? "s>s>" : "nn"
|
|
134
|
+
a1, a2 = bytes[pos, 4].unpack(fmt)
|
|
135
|
+
pos += 4
|
|
136
|
+
else
|
|
137
|
+
fmt = flags.anybits?(ARGS_ARE_XY) ? "cc" : "CC"
|
|
138
|
+
a1, a2 = bytes[pos, 2].unpack(fmt)
|
|
139
|
+
pos += 2
|
|
140
|
+
end
|
|
141
|
+
tlen = if flags.anybits?(HAVE_SCALE) then 2
|
|
142
|
+
elsif flags.anybits?(HAVE_XY_SCALE) then 4
|
|
143
|
+
elsif flags.anybits?(HAVE_TWO_BY_TWO) then 8
|
|
144
|
+
else 0
|
|
145
|
+
end
|
|
146
|
+
@components << Component.new(flags, gid, a1, a2, bytes[pos, tlen])
|
|
147
|
+
pos += tlen
|
|
148
|
+
break unless flags.anybits?(MORE_COMPONENTS)
|
|
149
|
+
end
|
|
150
|
+
@trailer = bytes[pos..] || "".b
|
|
151
|
+
@points = @components.map do |c|
|
|
152
|
+
c.flags.anybits?(ARGS_ARE_XY) ? [c.arg1.to_f, c.arg2.to_f] : [0.0, 0.0]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def serialize_simple(pts, bbox)
|
|
157
|
+
out = [@header_contours, *bbox].pack("s>5")
|
|
158
|
+
out << @end_pts.pack("n*")
|
|
159
|
+
out << [@instructions.bytesize].pack("n") << @instructions
|
|
160
|
+
flags = +"".b
|
|
161
|
+
xs = +"".b
|
|
162
|
+
ys = +"".b
|
|
163
|
+
px = py = 0
|
|
164
|
+
pts.each_with_index do |(x, y), i|
|
|
165
|
+
f = @on_curve[i] ? ON_CURVE : 0
|
|
166
|
+
f |= 0x40 if i.zero? && @overlap
|
|
167
|
+
dx = x - px
|
|
168
|
+
dy = y - py
|
|
169
|
+
if dx.zero? then f |= X_SAME
|
|
170
|
+
else xs << [dx].pack("s>")
|
|
171
|
+
end
|
|
172
|
+
if dy.zero? then f |= Y_SAME
|
|
173
|
+
else ys << [dy].pack("s>")
|
|
174
|
+
end
|
|
175
|
+
flags << f.chr
|
|
176
|
+
px = x
|
|
177
|
+
py = y
|
|
178
|
+
end
|
|
179
|
+
out << flags << xs << ys
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def serialize_composite(pts, bbox)
|
|
183
|
+
out = [@header_contours, *bbox].pack("s>5")
|
|
184
|
+
@components.each_with_index do |c, i|
|
|
185
|
+
flags = c.flags
|
|
186
|
+
a1 = c.arg1
|
|
187
|
+
a2 = c.arg2
|
|
188
|
+
if flags.anybits?(ARGS_ARE_XY)
|
|
189
|
+
a1, a2 = pts[i]
|
|
190
|
+
flags |= ARGS_ARE_WORDS
|
|
191
|
+
out << [flags, c.gid, a1, a2].pack("nns>s>")
|
|
192
|
+
elsif flags.anybits?(ARGS_ARE_WORDS)
|
|
193
|
+
out << [flags, c.gid, a1, a2].pack("nnnn")
|
|
194
|
+
else
|
|
195
|
+
out << [flags, c.gid, a1, a2].pack("nnCC")
|
|
196
|
+
end
|
|
197
|
+
out << c.transform
|
|
198
|
+
end
|
|
199
|
+
out << @trailer
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
module Instancing
|
|
6
|
+
# Reads per-glyph tuple variations from a gvar table.
|
|
7
|
+
class GvarReader
|
|
8
|
+
SHARED_POINTS = 0x8000
|
|
9
|
+
EMBEDDED_PEAK = 0x8000
|
|
10
|
+
INTERMEDIATE = 0x4000
|
|
11
|
+
PRIVATE_POINTS = 0x2000
|
|
12
|
+
INDEX_MASK = 0x0FFF
|
|
13
|
+
|
|
14
|
+
# points == nil means "every point of the glyph, phantoms included"
|
|
15
|
+
Tuple = Struct.new(:peak, :start, :end, :points, :dx, :dy) do
|
|
16
|
+
def intermediate?
|
|
17
|
+
!start.nil?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(data)
|
|
22
|
+
@data = data
|
|
23
|
+
_major, _minor, @axis_count, shared_count, shared_offset,
|
|
24
|
+
@glyph_count, flags, @array_offset = data[0, 20].unpack("nnnnNnnN")
|
|
25
|
+
@long_offsets = flags.anybits?(1)
|
|
26
|
+
@shared = Array.new(shared_count) do |i|
|
|
27
|
+
read_tuple(shared_offset + (i * @axis_count * 2))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param gid [Integer]
|
|
32
|
+
# @param point_count [Integer] glyph points including 4 phantoms
|
|
33
|
+
# @return [Array<Tuple>]
|
|
34
|
+
def tuples_for(gid, point_count)
|
|
35
|
+
return [] if gid >= @glyph_count
|
|
36
|
+
|
|
37
|
+
first, last = glyph_range(gid)
|
|
38
|
+
return [] if last <= first
|
|
39
|
+
|
|
40
|
+
parse_glyph(@data[@array_offset + first, last - first], point_count)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def read_tuple(pos)
|
|
46
|
+
@data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def glyph_range(gid)
|
|
50
|
+
if @long_offsets
|
|
51
|
+
@data[20 + (gid * 4), 8].unpack("NN")
|
|
52
|
+
else
|
|
53
|
+
@data[20 + (gid * 2), 4].unpack("nn").map { |v| v * 2 }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_glyph(glyph_data, point_count)
|
|
58
|
+
count_field, data_offset = glyph_data[0, 4].unpack("nn")
|
|
59
|
+
count = count_field & INDEX_MASK
|
|
60
|
+
headers = []
|
|
61
|
+
pos = 4
|
|
62
|
+
count.times do
|
|
63
|
+
size, index = glyph_data[pos, 4].unpack("nn")
|
|
64
|
+
pos += 4
|
|
65
|
+
peak = start = finish = nil
|
|
66
|
+
if index.anybits?(EMBEDDED_PEAK)
|
|
67
|
+
peak = glyph_data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
|
|
68
|
+
pos += @axis_count * 2
|
|
69
|
+
else
|
|
70
|
+
peak = @shared.fetch(index & INDEX_MASK)
|
|
71
|
+
end
|
|
72
|
+
if index.anybits?(INTERMEDIATE)
|
|
73
|
+
start = glyph_data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
|
|
74
|
+
finish = glyph_data[pos + (@axis_count * 2), @axis_count * 2]
|
|
75
|
+
.unpack("s>*").map { |v| v / 16384.0 }
|
|
76
|
+
pos += @axis_count * 4
|
|
77
|
+
end
|
|
78
|
+
headers << [size, index, peak, start, finish]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
dpos = data_offset
|
|
82
|
+
shared_points = nil
|
|
83
|
+
if count_field.anybits?(SHARED_POINTS)
|
|
84
|
+
shared_points, dpos = read_points(glyph_data, dpos)
|
|
85
|
+
end
|
|
86
|
+
headers.map do |size, index, peak, start, finish|
|
|
87
|
+
tuple_end = dpos + size
|
|
88
|
+
points = shared_points
|
|
89
|
+
points, dpos = read_points(glyph_data, dpos) if index.anybits?(PRIVATE_POINTS)
|
|
90
|
+
n = points ? points.size : point_count
|
|
91
|
+
dx, dpos = read_deltas(glyph_data, dpos, n)
|
|
92
|
+
dy, = read_deltas(glyph_data, dpos, n)
|
|
93
|
+
dpos = tuple_end
|
|
94
|
+
Tuple.new(peak, start, finish, points, dx, dy)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read_points(data, pos)
|
|
99
|
+
count = data.getbyte(pos)
|
|
100
|
+
pos += 1
|
|
101
|
+
if count.anybits?(0x80)
|
|
102
|
+
count = ((count & 0x7F) << 8) | data.getbyte(pos)
|
|
103
|
+
pos += 1
|
|
104
|
+
end
|
|
105
|
+
return [nil, pos] if count.zero?
|
|
106
|
+
|
|
107
|
+
points = []
|
|
108
|
+
last = 0
|
|
109
|
+
while points.size < count
|
|
110
|
+
ctl = data.getbyte(pos)
|
|
111
|
+
pos += 1
|
|
112
|
+
run = (ctl & 0x7F) + 1
|
|
113
|
+
if ctl.anybits?(0x80)
|
|
114
|
+
vals = data[pos, run * 2].unpack("n*")
|
|
115
|
+
pos += run * 2
|
|
116
|
+
else
|
|
117
|
+
vals = data[pos, run].unpack("C*")
|
|
118
|
+
pos += run
|
|
119
|
+
end
|
|
120
|
+
vals.each { |v| points << (last += v) }
|
|
121
|
+
end
|
|
122
|
+
[points, pos]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read_deltas(data, pos, count)
|
|
126
|
+
out = []
|
|
127
|
+
while out.size < count
|
|
128
|
+
ctl = data.getbyte(pos)
|
|
129
|
+
pos += 1
|
|
130
|
+
run = (ctl & 0x3F) + 1
|
|
131
|
+
if ctl.anybits?(0x80)
|
|
132
|
+
out.concat(Array.new(run, 0))
|
|
133
|
+
elsif ctl.anybits?(0x40)
|
|
134
|
+
out.concat(data[pos, run * 2].unpack("s>*"))
|
|
135
|
+
pos += run * 2
|
|
136
|
+
else
|
|
137
|
+
out.concat(data[pos, run].unpack("c*"))
|
|
138
|
+
pos += run
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
[out.first(count), pos]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|