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
|
@@ -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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
module Instancing
|
|
6
|
+
# Evaluates HVAR advance-width deltas (ItemVariationStore plus the
|
|
7
|
+
# optional DeltaSetIndexMap) at a normalized location.
|
|
8
|
+
class ItemVariationStore
|
|
9
|
+
# @param hvar [String] raw HVAR table
|
|
10
|
+
def initialize(hvar)
|
|
11
|
+
@hvar = hvar
|
|
12
|
+
store_offset, @advance_map_offset = hvar[4, 8].unpack("NN")
|
|
13
|
+
@store = hvar.byteslice(store_offset..)
|
|
14
|
+
region_list_offset, data_count = @store[2, 6].unpack("Nn")
|
|
15
|
+
@regions = parse_regions(region_list_offset)
|
|
16
|
+
@data_offsets = @store[8, data_count * 4].unpack("N*")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Float] advance-width delta for +gid+
|
|
20
|
+
def advance_delta(gid, coords)
|
|
21
|
+
outer, inner = delta_index(gid)
|
|
22
|
+
delta(outer, inner, coords)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def parse_regions(offset)
|
|
28
|
+
axis_count, region_count = @store[offset, 4].unpack("nn")
|
|
29
|
+
Array.new(region_count) do |r|
|
|
30
|
+
pos = offset + 4 + (r * axis_count * 6)
|
|
31
|
+
@store[pos, axis_count * 6].unpack("s>*").map { |v| v / 16384.0 }
|
|
32
|
+
.each_slice(3).to_a
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delta_index(gid)
|
|
37
|
+
return [0, gid] if @advance_map_offset.zero?
|
|
38
|
+
|
|
39
|
+
format, entry_format = @hvar[@advance_map_offset, 2].unpack("CC")
|
|
40
|
+
if format.zero?
|
|
41
|
+
count = @hvar[@advance_map_offset + 2, 2].unpack1("n")
|
|
42
|
+
pos = @advance_map_offset + 4
|
|
43
|
+
else
|
|
44
|
+
count = @hvar[@advance_map_offset + 2, 4].unpack1("N")
|
|
45
|
+
pos = @advance_map_offset + 6
|
|
46
|
+
end
|
|
47
|
+
inner_bits = (entry_format & 0x0F) + 1
|
|
48
|
+
size = ((entry_format & 0x30) >> 4) + 1
|
|
49
|
+
entry_gid = [gid, count - 1].min
|
|
50
|
+
entry = @hvar[pos + (entry_gid * size), size].bytes.inject(0) { |a, b| (a << 8) | b }
|
|
51
|
+
[entry >> inner_bits, entry & ((1 << inner_bits) - 1)]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def delta(outer, inner, coords)
|
|
55
|
+
offset = @data_offsets[outer]
|
|
56
|
+
return 0.0 unless offset
|
|
57
|
+
|
|
58
|
+
item_count, word_field, region_count = @store[offset, 6].unpack("nnn")
|
|
59
|
+
return 0.0 if inner >= item_count
|
|
60
|
+
|
|
61
|
+
long = word_field.anybits?(0x8000)
|
|
62
|
+
word_count = word_field & 0x7FFF
|
|
63
|
+
indexes = @store[offset + 6, region_count * 2].unpack("n*")
|
|
64
|
+
big = long ? 4 : 2
|
|
65
|
+
small = long ? 2 : 1
|
|
66
|
+
row_size = (word_count * big) + ((region_count - word_count) * small)
|
|
67
|
+
pos = offset + 6 + (region_count * 2) + (inner * row_size)
|
|
68
|
+
words = @store[pos, word_count * big].unpack(long ? "l>*" : "s>*")
|
|
69
|
+
rest = @store[pos + (word_count * big), (region_count - word_count) * small]
|
|
70
|
+
.unpack(long ? "s>*" : "c*")
|
|
71
|
+
(words + rest).each_with_index.sum do |d, i|
|
|
72
|
+
d * region_scalar(@regions[indexes[i]], coords)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def region_scalar(region, coords)
|
|
77
|
+
scalar = 1.0
|
|
78
|
+
region.each_with_index do |(lower, peak, upper), i|
|
|
79
|
+
v = coords[i]
|
|
80
|
+
next if peak.zero? || lower > peak || peak > upper
|
|
81
|
+
next if lower.negative? && upper.positive?
|
|
82
|
+
next if v == peak
|
|
83
|
+
return 0.0 if v <= lower || upper <= v
|
|
84
|
+
|
|
85
|
+
scalar *= v < peak ? (v - lower) / (peak - lower) : (upper - v) / (upper - peak)
|
|
86
|
+
end
|
|
87
|
+
scalar
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Variation
|
|
5
|
+
module Instancing
|
|
6
|
+
# Rewrites a name table so it describes one instance of a family.
|
|
7
|
+
class NameRewriter
|
|
8
|
+
Record = Struct.new(:platform, :encoding, :language, :name_id, :text)
|
|
9
|
+
|
|
10
|
+
# @param name_data [String] raw name table
|
|
11
|
+
def initialize(name_data)
|
|
12
|
+
@records = parse(name_data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def lookup(name_id)
|
|
16
|
+
same = @records.select { |r| r.name_id == name_id }
|
|
17
|
+
rec = same.find { |r| r.platform == 3 && r.language == 0x409 } ||
|
|
18
|
+
same.find { |r| r.platform == 3 } || same.first
|
|
19
|
+
rec&.text
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Names the instance +label+ (e.g. "Light") of the family.
|
|
23
|
+
def rename(label)
|
|
24
|
+
family = lookup(16) || lookup(1) || "Font"
|
|
25
|
+
legacy = ["Regular", "Bold", "Italic", "Bold Italic"].include?(label)
|
|
26
|
+
full = "#{family} #{label}".strip
|
|
27
|
+
ps = "#{family}-#{label}".delete(" ")
|
|
28
|
+
set(1, legacy ? family : full)
|
|
29
|
+
set(2, legacy ? label : "Regular")
|
|
30
|
+
set(3, "#{lookup(5) || ''};#{ps}".delete_prefix(";"))
|
|
31
|
+
set(4, full)
|
|
32
|
+
set(6, ps)
|
|
33
|
+
set(16, family)
|
|
34
|
+
set(17, label)
|
|
35
|
+
@records.reject! { |r| r.name_id == 25 }
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_binary
|
|
40
|
+
recs = @records.sort_by { |r| [r.platform, r.encoding, r.language, r.name_id] }
|
|
41
|
+
strings = +"".b
|
|
42
|
+
entries = recs.map do |r|
|
|
43
|
+
bin = encode(r)
|
|
44
|
+
off = strings.bytesize
|
|
45
|
+
strings << bin
|
|
46
|
+
[r.platform, r.encoding, r.language, r.name_id, bin.bytesize, off]
|
|
47
|
+
end
|
|
48
|
+
header = [0, recs.size, 6 + (recs.size * 12)].pack("nnn")
|
|
49
|
+
header + entries.map { |e| e.pack("n6") }.join + strings
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def set(name_id, text)
|
|
55
|
+
same = @records.select { |r| r.name_id == name_id }
|
|
56
|
+
same.each { |r| r.text = text }
|
|
57
|
+
return if same.any? { |r| r.platform == 3 }
|
|
58
|
+
|
|
59
|
+
@records << Record.new(3, 1, 0x409, name_id, text)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse(data)
|
|
63
|
+
count, str_off = data[2, 4].unpack("nn")
|
|
64
|
+
recs = Array.new(count) do |i|
|
|
65
|
+
plat, enc, lang, nid, len, off = data[6 + (i * 12), 12].unpack("n6")
|
|
66
|
+
raw = data[str_off + off, len]
|
|
67
|
+
Record.new(plat, enc, lang, nid, decode(plat, raw))
|
|
68
|
+
end
|
|
69
|
+
recs.reject { |r| r.language >= 0x8000 }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def decode(platform, raw)
|
|
73
|
+
if [0, 3].include?(platform)
|
|
74
|
+
raw.dup.force_encoding("UTF-16BE").encode("UTF-8", invalid: :replace)
|
|
75
|
+
else
|
|
76
|
+
raw.dup.force_encoding("ISO-8859-1").encode("UTF-8")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def encode(rec)
|
|
81
|
+
if [0, 3].include?(rec.platform)
|
|
82
|
+
rec.text.encode("UTF-16BE").b
|
|
83
|
+
else
|
|
84
|
+
rec.text.encode("ISO-8859-1", invalid: :replace, undef: :replace).b
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|