Skip to main content

max / quasi-type

15.6 KB · 401 lines History Blame Raw
1 //! Making a drawn mark answer the base's axis.
2 //!
3 //! A static base is cut once per weight, so a mark is drawn once per weight and
4 //! that is the end of it. A variable base is one file covering `wght` 200 to
5 //! 800, and a glyph spliced into it with no variation data holds still across
6 //! the whole range: correct at the default instance, and progressively wrong
7 //! everywhere else. A bold table border would draw at the light stroke weight.
8 //!
9 //! So the pipeline **keeps the axis** rather than instancing it away. That was
10 //! the open question and it is settled here: instancing is cheaper and throws
11 //! away the thing the base was chosen for, and the set's `weight` term already
12 //! describes a relationship the axis makes continuous instead of sampled at two
13 //! points. The recipes are parametric in the base's own measurements, so a
14 //! master is just the same recipe measured at another location — no second
15 //! drawing, no second source of truth.
16 //!
17 //! What that costs is `gvar`, which this module writes. The base's own entries
18 //! are copied as bytes for the same reason `glyf` is spliced rather than
19 //! recompiled: round-tripping a font's variation data to append to it risks
20 //! changing a face nobody asked us to change.
21
22 use read_fonts::types::GlyphId;
23 use write_fonts::tables::glyf::SimpleGlyph;
24 use write_fonts::tables::gvar::{GlyphDelta, GlyphDeltas, GlyphVariations, Gvar, Tent};
25 use write_fonts::types::F2Dot14;
26
27 use crate::Error;
28
29 /// The four points every glyph carries past its outline: the two side bearings
30 /// and the two vertical ones. `gvar` counts them, and a tuple that varies the
31 /// outline has to account for them even when they do not move.
32 const PHANTOM_POINTS: usize = 4;
33
34 /// One mark, drawn at every master.
35 pub struct Varied {
36 /// The drawing at the axis default, which is what goes into `glyf`.
37 pub default: SimpleGlyph,
38 /// `(peak, drawing)` for each master that gets deltas.
39 pub masters: Vec<(f32, SimpleGlyph)>,
40 }
41
42 impl Varied {
43 /// The per-point offsets from the default drawing, one tuple per master.
44 ///
45 /// Returns `None` when nothing moves anywhere, which is a real answer: a
46 /// mark whose recipe ignores the base's weight has no variation data and
47 /// should not be given an empty tuple that says so at length.
48 pub fn deltas(&self, name: &str) -> Result<Option<Vec<GlyphDeltas>>, Error> {
49 let default = points(&self.default);
50 let mut out = Vec::new();
51 for (peak, drawing) in &self.masters {
52 let master = points(drawing);
53 if master.len() != default.len() {
54 // The recipes are parametric, so this cannot happen from a
55 // weight change alone; it would take a recipe that adds a
56 // contour past some threshold. Catching it here beats shipping
57 // a font whose glyph interpolates into a different shape.
58 return Err(Error::Draw(format!(
59 "{name} has {} points at the axis default and {} at {peak}, so the \
60 two cannot interpolate. A mark's point count has to be the same at \
61 every master.",
62 default.len(),
63 master.len()
64 )));
65 }
66 let mut deltas: Vec<GlyphDelta> = default
67 .iter()
68 .zip(&master)
69 .map(|(from, to)| GlyphDelta::required(to.0 - from.0, to.1 - from.1))
70 .collect();
71 // The phantom points do not move: a mark takes the cell at every
72 // weight, which is what `hmtx` already says.
73 deltas.extend(std::iter::repeat_n(
74 GlyphDelta::optional(0, 0),
75 PHANTOM_POINTS,
76 ));
77 if deltas.iter().all(|d| d.x == 0 && d.y == 0) {
78 continue;
79 }
80 out.push(GlyphDeltas::new(
81 vec![Tent::new(F2Dot14::from_f32(*peak), None)],
82 deltas,
83 ));
84 }
85 Ok((!out.is_empty()).then_some(out))
86 }
87 }
88
89 fn points(glyph: &SimpleGlyph) -> Vec<(i16, i16)> {
90 glyph
91 .contours
92 .iter()
93 .flat_map(|contour| contour.iter().map(|point| (point.x, point.y)))
94 .collect()
95 }
96
97 /// Compile one glyph's variation data into the bytes `gvar` stores for it.
98 ///
99 /// Done a glyph at a time on purpose. `Gvar::new` pulls any peak tuple used by
100 /// more than one glyph into the table's shared-tuple array and leaves an index
101 /// behind, and an index into *our* shared array means nothing inside the base's
102 /// table. One glyph per call cannot share with anything, so what comes back is
103 /// self-contained and safe to splice.
104 pub fn compile(variations: Vec<GlyphDeltas>) -> Result<Vec<u8>, Error> {
105 let gvar = Gvar::new(vec![GlyphVariations::new(GlyphId::new(0), variations)], 1)
106 .map_err(|e| Error::Font(format!("gvar: {e}")))?;
107 let bytes = write_fonts::dump_table(&gvar).map_err(|e| Error::Font(format!("gvar: {e}")))?;
108 let table = Table::read(&bytes)?;
109 let (start, end) = table.entry(0)?;
110 Ok(table.bytes[start..end].to_vec())
111 }
112
113 /// Splice per-glyph variation data onto the end of the base's `gvar`.
114 ///
115 /// The base's glyphs keep the bytes they arrived with, exactly as they keep
116 /// their `glyf` entries. Offsets are rewritten in the long format
117 /// unconditionally: it is one code path, and the short format stores offsets
118 /// halved, so a table that outgrows it mid-splice would need rewriting anyway.
119 pub fn splice(base: &[u8], appended: &[Vec<u8>], axis_count: u16) -> Result<Vec<u8>, Error> {
120 let table = Table::read(base)?;
121 if table.axis_count != axis_count {
122 return Err(Error::Font(format!(
123 "the base's `gvar` varies on {} axes and its `fvar` names {axis_count}",
124 table.axis_count
125 )));
126 }
127
128 let mut data = table.bytes[table.data_start..table.data_end()].to_vec();
129 let mut offsets: Vec<u32> = (0..=table.glyph_count)
130 .map(|i| table.offset(i))
131 .collect::<Result<_, _>>()?;
132 for entry in appended {
133 data.extend_from_slice(entry);
134 // Every entry starts on an even boundary, which the short offset format
135 // requires and the long one is written to match.
136 if data.len() % 2 != 0 {
137 data.push(0);
138 }
139 offsets.push(data.len() as u32);
140 }
141
142 let glyph_count = table.glyph_count + appended.len() as u32;
143 let shared = &table.bytes[table.shared_start..table.shared_start + table.shared_len];
144 let header = 20;
145 let offsets_len = (glyph_count as usize + 1) * 4;
146 let shared_offset = header + offsets_len;
147 let data_offset = shared_offset + shared.len();
148
149 let mut out = Vec::with_capacity(data_offset + data.len());
150 out.extend_from_slice(&0x0001_0000u32.to_be_bytes());
151 out.extend_from_slice(&axis_count.to_be_bytes());
152 out.extend_from_slice(&table.shared_count.to_be_bytes());
153 out.extend_from_slice(&(shared_offset as u32).to_be_bytes());
154 out.extend_from_slice(&u16::try_from(glyph_count).map_err(too_many)?.to_be_bytes());
155 // Flag 1: the offsets below are `u32` rather than halved `u16`.
156 out.extend_from_slice(&1u16.to_be_bytes());
157 out.extend_from_slice(&(data_offset as u32).to_be_bytes());
158 for offset in &offsets {
159 out.extend_from_slice(&offset.to_be_bytes());
160 }
161 out.extend_from_slice(shared);
162 out.extend_from_slice(&data);
163 Ok(out)
164 }
165
166 fn too_many(_: std::num::TryFromIntError) -> Error {
167 Error::Font("a face cannot carry more than 65,535 glyphs".into())
168 }
169
170 /// Just enough of `gvar` to copy the parts the pipeline does not rewrite.
171 ///
172 /// read-fonts parses this table into deltas, which is the wrong shape here: the
173 /// point is to move the base's bytes across without decoding them.
174 struct Table<'a> {
175 bytes: &'a [u8],
176 axis_count: u16,
177 shared_count: u16,
178 shared_start: usize,
179 shared_len: usize,
180 glyph_count: u32,
181 long_offsets: bool,
182 offsets_start: usize,
183 data_start: usize,
184 }
185
186 impl<'a> Table<'a> {
187 fn read(bytes: &'a [u8]) -> Result<Self, Error> {
188 let short = || Error::Font("the base's `gvar` is truncated".into());
189 let u16_at = |at: usize| -> Result<u16, Error> {
190 bytes
191 .get(at..at + 2)
192 .map(|b| u16::from_be_bytes([b[0], b[1]]))
193 .ok_or_else(short)
194 };
195 let u32_at = |at: usize| -> Result<u32, Error> {
196 bytes
197 .get(at..at + 4)
198 .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
199 .ok_or_else(short)
200 };
201
202 let axis_count = u16_at(4)?;
203 let shared_count = u16_at(6)?;
204 let shared_start = u32_at(8)? as usize;
205 let glyph_count = u32::from(u16_at(12)?);
206 let long_offsets = u16_at(14)? & 1 == 1;
207 let data_start = u32_at(16)? as usize;
208 Ok(Table {
209 bytes,
210 axis_count,
211 shared_count,
212 shared_start,
213 shared_len: shared_count as usize * axis_count as usize * 2,
214 glyph_count,
215 long_offsets,
216 offsets_start: 20,
217 data_start,
218 })
219 }
220
221 /// Offsets are stored halved in the short format, which is what makes a
222 /// misread here silently produce a face whose glyphs are someone else's.
223 fn offset(&self, index: u32) -> Result<u32, Error> {
224 let short = || Error::Font("the base's `gvar` offset array is truncated".into());
225 if self.long_offsets {
226 let at = self.offsets_start + index as usize * 4;
227 self.bytes
228 .get(at..at + 4)
229 .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
230 .ok_or_else(short)
231 } else {
232 let at = self.offsets_start + index as usize * 2;
233 self.bytes
234 .get(at..at + 2)
235 .map(|b| u32::from(u16::from_be_bytes([b[0], b[1]])) * 2)
236 .ok_or_else(short)
237 }
238 }
239
240 fn data_end(&self) -> usize {
241 self.data_start + self.offset(self.glyph_count).unwrap_or(0) as usize
242 }
243
244 /// The byte range one glyph's variation data occupies, relative to the
245 /// start of the data array.
246 fn entry(&self, index: u32) -> Result<(usize, usize), Error> {
247 Ok((
248 self.data_start + self.offset(index)? as usize,
249 self.data_start + self.offset(index + 1)? as usize,
250 ))
251 }
252
253 #[cfg(test)]
254 fn glyph_data(&self, index: u32) -> Result<&'a [u8], Error> {
255 let (start, end) = self.entry(index)?;
256 self.bytes
257 .get(start..end)
258 .ok_or_else(|| Error::Font("the base's `gvar` is truncated".into()))
259 }
260 }
261
262 #[cfg(test)]
263 mod tests {
264 use super::*;
265 use read_fonts::tables::glyf::CurvePoint;
266 use write_fonts::tables::glyf::Contour;
267
268 fn glyph(points: &[(i16, i16)]) -> SimpleGlyph {
269 let contour: Contour = points
270 .iter()
271 .map(|&(x, y)| CurvePoint::on_curve(x, y))
272 .collect::<Vec<_>>()
273 .into();
274 SimpleGlyph {
275 contours: vec![contour],
276 ..Default::default()
277 }
278 }
279
280 fn varied(default: &[(i16, i16)], bold: &[(i16, i16)]) -> Varied {
281 Varied {
282 default: glyph(default),
283 masters: vec![(1.0, glyph(bold))],
284 }
285 }
286
287 #[test]
288 fn a_mark_that_moves_gets_a_tuple_per_master() {
289 let varied = varied(
290 &[(0, 0), (100, 0), (50, 90)],
291 &[(0, 0), (120, 0), (60, 108)],
292 );
293 let deltas = varied.deltas("uni25B2").unwrap().expect("it moves");
294 assert_eq!(deltas.len(), 1);
295 }
296
297 /// A recipe with no weight term draws the same mark everywhere, and an
298 /// empty tuple would only cost bytes to say so.
299 #[test]
300 fn a_mark_that_holds_still_gets_no_variation_data() {
301 let still = [(0, 0), (100, 0), (50, 90)];
302 assert!(varied(&still, &still).deltas("uni2588").unwrap().is_none());
303 }
304
305 #[test]
306 fn a_mark_that_changes_point_count_is_refused() {
307 let varied = varied(&[(0, 0), (100, 0), (50, 90)], &[(0, 0), (120, 0)]);
308 let err = varied.deltas("uni25B2").unwrap_err().to_string();
309 assert!(err.contains("interpolate"), "{err}");
310 }
311
312 /// The deltas are offsets from the default drawing, and the phantom points
313 /// ride along without moving.
314 #[test]
315 fn the_deltas_are_offsets_from_the_default_drawing() {
316 let varied = varied(&[(10, 10)], &[(30, 4)]);
317 let deltas = varied.deltas("m").unwrap().unwrap();
318 let first = &deltas[0];
319 assert_eq!(first.deltas.len(), 1 + PHANTOM_POINTS);
320 assert_eq!((first.deltas[0].x, first.deltas[0].y), (20, -6));
321 assert!(first.deltas[1..].iter().all(|d| d.x == 0 && d.y == 0));
322 }
323
324 /// Compiled data has to stand on its own: a peak tuple embedded in the
325 /// entry rather than an index into a shared array the base does not have.
326 #[test]
327 fn compiled_data_embeds_its_peak_rather_than_sharing_it() {
328 let varied = Varied {
329 default: glyph(&[(0, 0), (100, 0), (50, 90)]),
330 masters: vec![
331 (1.0, glyph(&[(0, 0), (120, 0), (60, 108)])),
332 (-1.0, glyph(&[(0, 0), (90, 0), (45, 81)])),
333 ],
334 };
335 let entry = compile(varied.deltas("uni25B2").unwrap().unwrap()).unwrap();
336 let count = u16::from_be_bytes([entry[0], entry[1]]);
337 // Bit 15 of tupleVariationCount is the shared-point-numbers flag; the
338 // low twelve bits are the count.
339 assert_eq!(count & 0x0FFF, 2, "one tuple per master");
340 // The first tuple header sits past the count and the data offset; its
341 // `tupleIndex` is the second field of that header.
342 let flags = u16::from_be_bytes([entry[6], entry[7]]);
343 assert_eq!(flags & 0x8000, 0x8000, "EMBEDDED_PEAK_TUPLE");
344 }
345
346 /// The point of splicing: the base's own entries survive byte for byte.
347 #[test]
348 fn splicing_leaves_every_base_entry_where_it_was() {
349 let base = Gvar::new(
350 (0..3)
351 .map(|gid| {
352 GlyphVariations::new(
353 GlyphId::new(gid),
354 vec![GlyphDeltas::new(
355 vec![Tent::new(F2Dot14::from_f32(1.0), None)],
356 vec![
357 GlyphDelta::required(gid as i16 + 1, 0),
358 GlyphDelta::required(0, gid as i16 + 2),
359 ],
360 )],
361 )
362 })
363 .collect(),
364 1,
365 )
366 .unwrap();
367 let base = write_fonts::dump_table(&base).unwrap();
368 let mark = compile(
369 varied(
370 &[(0, 0), (100, 0), (50, 90)],
371 &[(0, 0), (120, 0), (60, 108)],
372 )
373 .deltas("m")
374 .unwrap()
375 .unwrap(),
376 )
377 .unwrap();
378
379 let out = splice(&base, std::slice::from_ref(&mark), 1).unwrap();
380 let before = Table::read(&base).unwrap();
381 let after = Table::read(&out).unwrap();
382 assert_eq!(after.glyph_count, before.glyph_count + 1);
383 assert!(after.long_offsets);
384 for gid in 0..before.glyph_count {
385 assert_eq!(
386 after.glyph_data(gid).unwrap(),
387 before.glyph_data(gid).unwrap(),
388 "glyph {gid}'s variation data changed"
389 );
390 }
391 assert_eq!(after.glyph_data(before.glyph_count).unwrap(), mark);
392 }
393
394 #[test]
395 fn a_gvar_whose_axis_count_disagrees_with_fvar_is_refused() {
396 let base = Gvar::new(vec![GlyphVariations::new(GlyphId::new(0), vec![])], 1).unwrap();
397 let base = write_fonts::dump_table(&base).unwrap();
398 assert!(splice(&base, &[], 2).is_err());
399 }
400 }
401