fontisan 0.4.4 → 0.4.5
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/ufo/compile/gvar.rb +222 -0
- data/lib/fontisan/ufo/compile.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 999173c7fe3caa0568c7bc29b87184b1f167c5885b5e66c9c7c7f0f884d2523e
|
|
4
|
+
data.tar.gz: a9bf5e401b1b08672ced56f71b7ff5d6ab6b60e83b4ba1932ef43582c5d3a528
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b770784dbac538d4d3998b1e908eedd5cb010f72897a0dc94b28e32f1150c6fdb99d384703760360dd1b01855de725ef786c34b173854ca8fd869b23ad6a8d6
|
|
7
|
+
data.tar.gz: 5b64092d77f6c19fc2e40fe40114722652000c49a36b22ae0dacccb86236d5529fe7d9c183bc4bd3141998d44a59b44ae2d5954bf675bb86fb489ff380a7b490
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Ufo
|
|
5
|
+
module Compile
|
|
6
|
+
# Builds the OpenType `gvar` (Glyph Variation Data) table from
|
|
7
|
+
# per-glyph deltas computed between a default master and one or
|
|
8
|
+
# more extreme masters.
|
|
9
|
+
#
|
|
10
|
+
# This is the hardest table in the variable-font specification.
|
|
11
|
+
# It stores, for every glyph, how each outline point moves
|
|
12
|
+
# between the default master and each extreme master.
|
|
13
|
+
#
|
|
14
|
+
# This builder produces a minimal-but-valid gvar:
|
|
15
|
+
# - One tuple per master (no shared tuples)
|
|
16
|
+
# - Explicit per-point deltas (no IUP compression)
|
|
17
|
+
# - Delta values encoded as int8 when possible, int16 otherwise
|
|
18
|
+
#
|
|
19
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/gvar
|
|
20
|
+
module Gvar
|
|
21
|
+
VERSION_MAJOR = 1
|
|
22
|
+
VERSION_MINOR = 0
|
|
23
|
+
|
|
24
|
+
# TupleIndex flags
|
|
25
|
+
TUPLE_SHARED = 0x8000
|
|
26
|
+
TUPLE_PRIVATE = 0x4000
|
|
27
|
+
TUPLE_AXIS_COUNT_MASK = 0x0FFF
|
|
28
|
+
|
|
29
|
+
# Delta encoding flags (in the delta data header)
|
|
30
|
+
DELTAS_ARE_ZERO = 0x80
|
|
31
|
+
DELTAS_ARE_INT8 = 0x40
|
|
32
|
+
POINT_COUNT_MASK = 0x3FFF
|
|
33
|
+
|
|
34
|
+
# @param default_glyphs [Array<Fontisan::Ufo::Glyph>] default master
|
|
35
|
+
# @param masters [Array<Hash>] extreme masters
|
|
36
|
+
# Each hash: { axes: { tag: peak_value }, glyphs: [Glyph, ...] }
|
|
37
|
+
# @param axis_count [Integer] number of axes
|
|
38
|
+
# @return [String] gvar table bytes
|
|
39
|
+
def self.build(default_glyphs:, masters:, axis_count:)
|
|
40
|
+
glyph_count = default_glyphs.size
|
|
41
|
+
return build_empty(glyph_count) if masters.empty? || glyph_count.zero?
|
|
42
|
+
|
|
43
|
+
# Compute per-glyph variation data
|
|
44
|
+
glyph_data = Array.new(glyph_count) do |gid|
|
|
45
|
+
build_glyph_variation(default_glyphs[gid], masters, gid, axis_count)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
assemble(glyph_count, axis_count, glyph_data)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ---------- per-glyph variation ----------
|
|
52
|
+
|
|
53
|
+
# Build the tuple variation data for a single glyph.
|
|
54
|
+
# Returns the serialized bytes (tuple headers + delta data).
|
|
55
|
+
# Returns empty string if the glyph has no variation.
|
|
56
|
+
def self.build_glyph_variation(default_glyph, masters, gid, axis_count)
|
|
57
|
+
return +"" unless default_glyph
|
|
58
|
+
|
|
59
|
+
tuples = []
|
|
60
|
+
masters.each do |master|
|
|
61
|
+
master_glyph = master[:glyphs]&.dig(gid)
|
|
62
|
+
next unless master_glyph
|
|
63
|
+
|
|
64
|
+
deltas = compute_deltas(default_glyph, master_glyph)
|
|
65
|
+
next if deltas.all? { |d| d[0].zero? && d[1].zero? }
|
|
66
|
+
|
|
67
|
+
tuples << {
|
|
68
|
+
peak: master[:axes] || {},
|
|
69
|
+
deltas: deltas,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
return +"" if tuples.empty?
|
|
74
|
+
|
|
75
|
+
serialize_tuples(tuples, axis_count)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Compute per-point deltas (dx, dy) between default and master.
|
|
79
|
+
# @return [Array<[Integer, Integer]>] deltas for each point
|
|
80
|
+
def self.compute_deltas(default_glyph, master_glyph)
|
|
81
|
+
default_points = default_glyph.contours.flat_map(&:points)
|
|
82
|
+
master_points = master_glyph.contours.flat_map(&:points)
|
|
83
|
+
|
|
84
|
+
count = [default_points.size, master_points.size].min
|
|
85
|
+
Array.new(count) do |i|
|
|
86
|
+
dx = master_points[i].x.to_i - default_points[i].x.to_i
|
|
87
|
+
dy = master_points[i].y.to_i - default_points[i].y.to_i
|
|
88
|
+
[dx, dy]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# ---------- tuple serialization ----------
|
|
93
|
+
|
|
94
|
+
def self.serialize_tuples(tuples, axis_count)
|
|
95
|
+
tuple_entries = tuples.map { |t| serialize_tuple(t, axis_count) }
|
|
96
|
+
tuple_count = tuple_entries.size
|
|
97
|
+
|
|
98
|
+
io = +""
|
|
99
|
+
io << [tuple_count].pack("n") # tupleVariationCount
|
|
100
|
+
io << [4 + tuple_count * 4].pack("n") # dataOffset (after header + tuple headers)
|
|
101
|
+
# Wait — dataOffset is from start of per-glyph data to the delta data area.
|
|
102
|
+
# The tuple headers come first, then the delta data.
|
|
103
|
+
# Let me recalculate:
|
|
104
|
+
tuple_headers_size = tuple_entries.sum { |e| e[:header].bytesize }
|
|
105
|
+
data_offset = 4 + tuple_headers_size
|
|
106
|
+
|
|
107
|
+
io = +""
|
|
108
|
+
io << [tuple_count].pack("n")
|
|
109
|
+
io << [data_offset].pack("n")
|
|
110
|
+
|
|
111
|
+
tuple_entries.each do |e|
|
|
112
|
+
io << e[:header]
|
|
113
|
+
io << e[:data]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
io
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.serialize_tuple(tuple, axis_count)
|
|
120
|
+
peak = tuple[:peak]
|
|
121
|
+
deltas = tuple[:deltas]
|
|
122
|
+
|
|
123
|
+
# Encode delta data
|
|
124
|
+
all_fit_int8 = deltas.all? { |dx, dy| dx.between?(-127, 127) && dy.between?(-127, 127) }
|
|
125
|
+
|
|
126
|
+
point_count = deltas.size
|
|
127
|
+
flags = point_count & POINT_COUNT_MASK
|
|
128
|
+
flags |= DELTAS_ARE_INT8 if all_fit_int8
|
|
129
|
+
|
|
130
|
+
data = +""
|
|
131
|
+
data << [flags].pack("n")
|
|
132
|
+
|
|
133
|
+
if all_fit_int8
|
|
134
|
+
deltas.each do |dx, dy|
|
|
135
|
+
data << [dx & 0xFF, dy & 0xFF].pack("CC")
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
deltas.each do |dx, dy|
|
|
139
|
+
data << [dx & 0xFFFF, dy & 0xFFFF].pack("nn")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Encode tuple header
|
|
144
|
+
tuple_index = TUPLE_PRIVATE | (axis_count & TUPLE_AXIS_COUNT_MASK)
|
|
145
|
+
|
|
146
|
+
header = +""
|
|
147
|
+
header << [data.bytesize].pack("n") # variationDataSize
|
|
148
|
+
header << [tuple_index].pack("n") # tupleIndex
|
|
149
|
+
|
|
150
|
+
# Peak axis coordinates (F2DOT14 per axis)
|
|
151
|
+
axis_tags = peak.keys.sort
|
|
152
|
+
axis_tags.first(axis_count).each do |tag|
|
|
153
|
+
header << [f2dot14(peak[tag] || 0)].pack("n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
{ header: header, data: data }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ---------- gvar table assembly ----------
|
|
160
|
+
|
|
161
|
+
def self.assemble(glyph_count, axis_count, glyph_data)
|
|
162
|
+
# Build the glyph variation data offset array
|
|
163
|
+
offsets = [0]
|
|
164
|
+
current = 0
|
|
165
|
+
glyph_data.each do |data|
|
|
166
|
+
current += data.bytesize
|
|
167
|
+
offsets << current
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Offset size: uint16 (flags bit 0 = 0) or uint32 (bit 0 = 1)
|
|
171
|
+
use_long = offsets.last > 0xFFFF
|
|
172
|
+
flags = use_long ? 1 : 0
|
|
173
|
+
|
|
174
|
+
# Build header (20 bytes for v1)
|
|
175
|
+
header = +""
|
|
176
|
+
header << [VERSION_MAJOR, VERSION_MINOR].pack("nn") # version
|
|
177
|
+
header << [axis_count].pack("n") # axisCount
|
|
178
|
+
header << [0].pack("n") # sharedTupleCount
|
|
179
|
+
header << [0].pack("N") # offsetToSharedTuples
|
|
180
|
+
header << [glyph_count].pack("n") # glyphCount
|
|
181
|
+
header << [flags].pack("n") # flags (bit 0 = offset size)
|
|
182
|
+
|
|
183
|
+
header_size = header.bytesize
|
|
184
|
+
offset_entry_size = use_long ? 4 : 2
|
|
185
|
+
offset_array_size = (glyph_count + 1) * offset_entry_size
|
|
186
|
+
header_size + offset_array_size
|
|
187
|
+
|
|
188
|
+
# Offset array (relative to data_start)
|
|
189
|
+
if use_long
|
|
190
|
+
header + offsets.pack("N*") + glyph_data.join
|
|
191
|
+
else
|
|
192
|
+
header + offsets.pack("n*") + glyph_data.join
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.build_empty(glyph_count)
|
|
197
|
+
# Minimal gvar with no variation data
|
|
198
|
+
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count * 1 + 16].pack("nnnnnnNN")
|
|
199
|
+
offsets = Array.new(glyph_count + 1, 0).pack("C*")
|
|
200
|
+
header + offsets
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ---------- helpers ----------
|
|
204
|
+
|
|
205
|
+
def self.f2dot14(value)
|
|
206
|
+
(value.to_f * 16384).to_i
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.byte_size_for(max_value)
|
|
210
|
+
return 1 if max_value <= 0xFF
|
|
211
|
+
return 2 if max_value <= 0xFFFF
|
|
212
|
+
return 3 if max_value <= 0xFFFFFF
|
|
213
|
+
|
|
214
|
+
4
|
|
215
|
+
end
|
|
216
|
+
private_class_method :build_glyph_variation, :compute_deltas,
|
|
217
|
+
:serialize_tuples, :serialize_tuple,
|
|
218
|
+
:assemble, :build_empty, :f2dot14, :byte_size_for
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/lib/fontisan/ufo/compile.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Fontisan
|
|
|
35
35
|
autoload :Filters, "fontisan/ufo/compile/filters"
|
|
36
36
|
autoload :Fvar, "fontisan/ufo/compile/fvar"
|
|
37
37
|
autoload :Gpos, "fontisan/ufo/compile/gpos"
|
|
38
|
+
autoload :Gvar, "fontisan/ufo/compile/gvar"
|
|
38
39
|
autoload :Head, "fontisan/ufo/compile/head"
|
|
39
40
|
autoload :Hhea, "fontisan/ufo/compile/hhea"
|
|
40
41
|
autoload :Maxp, "fontisan/ufo/compile/maxp"
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -566,6 +566,7 @@ files:
|
|
|
566
566
|
- lib/fontisan/ufo/compile/fvar.rb
|
|
567
567
|
- lib/fontisan/ufo/compile/glyf_loca.rb
|
|
568
568
|
- lib/fontisan/ufo/compile/gpos.rb
|
|
569
|
+
- lib/fontisan/ufo/compile/gvar.rb
|
|
569
570
|
- lib/fontisan/ufo/compile/head.rb
|
|
570
571
|
- lib/fontisan/ufo/compile/hhea.rb
|
|
571
572
|
- lib/fontisan/ufo/compile/hmtx.rb
|