Skip to main content

max / quasi-type

19.0 KB · 524 lines History Blame Raw
1 //! Cutting a face from a variable base, which is the capability itself.
2 //!
3 //! The base is Atkinson Hyperlegible Mono: `wght` 200-800 in one file, and the
4 //! base `quasi-mono` has been cut from since. What is asserted here is what
5 //! the axis adds — that it survives the cut, that the marks ride it, and that
6 //! a mark calibrated at rest is still the right weight at the far end, which
7 //! is the failure mode a static base could not have.
8 //!
9 //! Skipped rather than failed when the base is not cached, same as the static
10 //! cut. `cargo run -- params --base atkinson-mono` once and it runs for good.
11
12 use quasi_type::base::{self, BaseFace};
13 use quasi_type::compose::{self, Identity};
14 use quasi_type::manifest::WeightResponse;
15 use quasi_type::manifest::{GlyphSpec, Manifest};
16 use quasi_type::pins::{Base, Pins};
17 use quasi_type::{HOUSE_SET, PINS, assert as coverage, woff2};
18
19 use read_fonts::types::{GlyphId, Tag};
20 use read_fonts::{FontRef, TableProvider};
21 use skrifa::MetadataProvider;
22 use skrifa::instance::Size;
23
24 const BASE: &str = "atkinson-mono";
25
26 /// The family the slot names. Read here rather than through `pins.slot` because
27 /// this file is about the base and cuts from it directly.
28 const FAMILY: &str = "Quasi Mono";
29
30 fn root() -> std::path::PathBuf {
31 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
32 }
33
34 fn pins() -> Pins {
35 Pins::parse(PINS).unwrap()
36 }
37
38 fn base_face() -> Option<(BaseFace, &'static Base)> {
39 let pins: &'static Pins = Box::leak(Box::new(pins()));
40 let base = pins.base(BASE).unwrap();
41 match base::load(base, &base::cache_dir(&root()), true) {
42 Ok(faces) => faces.into_iter().next().map(|face| (face, base)),
43 Err(_) => {
44 eprintln!("skipped: {BASE} is not cached. Run `cargo run -- params --base {BASE}`");
45 None
46 }
47 }
48 }
49
50 fn cut_as(style: &str) -> Option<Result<(Vec<u8>, Vec<GlyphSpec>), String>> {
51 let (face, base) = base_face()?;
52 let manifest = Manifest::parse(HOUSE_SET).unwrap();
53 let specs = manifest.glyphs;
54 let refs: Vec<&GlyphSpec> = specs.iter().collect();
55 let id = Identity {
56 family: FAMILY,
57 style,
58 version: "2.2.001",
59 set_version: manifest.set.version,
60 base,
61 };
62 Some(
63 compose::build(&face.bytes, &refs, &id)
64 .map(|built| (built.bytes, specs))
65 .map_err(|e| e.to_string()),
66 )
67 }
68
69 fn cut() -> Option<(Vec<u8>, Vec<GlyphSpec>)> {
70 Some(cut_as("ExtraLight")?.expect("the face cuts"))
71 }
72
73 /// The whole point: the cut face still varies, and so do the marks in it.
74 #[test]
75 fn the_axis_survives_the_cut_and_the_marks_ride_it() {
76 let Some((bytes, specs)) = cut() else { return };
77 let font = FontRef::new(&bytes).unwrap();
78
79 for tag in [b"fvar", b"gvar", b"avar", b"STAT", b"HVAR"] {
80 assert!(
81 font.table_data(Tag::new(tag)).is_some(),
82 "{} was dropped, so the face no longer varies the way its base did",
83 String::from_utf8_lossy(tag)
84 );
85 }
86
87 let axes = font.axes();
88 assert_eq!(axes.len(), 1);
89 let axis = axes.get(0).unwrap();
90 assert_eq!(axis.tag(), Tag::new(b"wght"));
91 assert_eq!((axis.min_value(), axis.max_value()), (200.0, 800.0));
92 assert_eq!(
93 font.named_instances().len(),
94 7,
95 "the base's seven named weights"
96 );
97
98 // Every mark that answers weight has variation data, and it is data the
99 // base did not have. A cell fill is exactly half the cell at every weight,
100 // so it correctly carries none: `gvar` says "does not vary" with an empty
101 // entry, and that is the honest record rather than a gap.
102 let mappings = compose::coverage(&bytes).unwrap();
103 let gvar = font.gvar().unwrap();
104 for spec in &specs {
105 if spec.shape.weight_response() == WeightResponse::Holds {
106 continue;
107 }
108 let gid = mappings[&spec.codepoint];
109 let data = gvar
110 .glyph_variation_data(gid)
111 .unwrap_or_else(|e| panic!("{} has no readable variation data: {e:?}", spec.name));
112 assert!(
113 data.is_some_and(|d| d.tuples().count() > 0),
114 "{} carries no variation, so it would draw at wght 200 across the whole axis",
115 spec.name
116 );
117 }
118 }
119
120 /// The done condition, in the form it was written in: a mark drawn at the bold
121 /// end of the axis is drawn at the bold stroke weight.
122 ///
123 /// Read back through the axis rather than off the deltas, so what is measured is
124 /// what a rasteriser would actually put on a screen.
125 #[test]
126 fn a_mark_at_the_heavy_end_is_heavier_than_at_the_light_end() {
127 let Some((bytes, specs)) = cut() else { return };
128 for spec in &specs {
129 // Except the cell fills, which are the one thing in the set that
130 // deliberately does not answer weight: `▀` is exactly half the cell at
131 // wght 200 and at 800, and a half that grew would stop tiling.
132 if spec.shape.weight_response() == WeightResponse::Holds {
133 continue;
134 }
135 let light = ink(&bytes, spec.codepoint, 200.0);
136 let heavy = ink(&bytes, spec.codepoint, 800.0);
137 assert!(
138 heavy > light * 1.10,
139 "{} puts {light:.0} units of ink down at wght 200 and {heavy:.0} at 800, \
140 which is not a face answering its own axis",
141 spec.name
142 );
143 }
144 }
145
146 /// The two kinds of mark answer the axis differently, and the discriminator is
147 /// how much of its own box a mark inks rather than how wide it gets.
148 ///
149 /// Extent alone does not separate them here and should not be expected to. The
150 /// static cut compares Regular to Bold, 400 to 700; this axis runs 200 to 800
151 /// and Atkinson's stroke goes 55 units to 139, so a stroked mark's band term
152 /// carries it 1.15x wider on its own. Fill separates them cleanly, because it is
153 /// what the two responses actually mean: a solid mark scales, so it inks the
154 /// same share of a larger box, and a stroked mark thickens inside a box that
155 /// barely moves.
156 #[test]
157 fn solid_marks_scale_along_the_axis_and_stroked_marks_thicken() {
158 use quasi_type::manifest::WeightResponse;
159
160 let Some((bytes, specs)) = cut() else { return };
161 for spec in &specs {
162 // Cell furniture is measured by the test below instead. A bar's
163 // bounding box *is* the bar, so it inks all of it at every weight, and
164 // a fill fraction says nothing about whether it thickened.
165 if spec.shape.is_cell_furniture() {
166 continue;
167 }
168 let light = fill(&bytes, spec.codepoint, 200.0);
169 let heavy = fill(&bytes, spec.codepoint, 800.0);
170 match spec.shape.weight_response() {
171 // Cell fills are exact fractions of the cell at every weight, so
172 // they neither grow nor thicken. Nothing to assert but that.
173 WeightResponse::Holds => {
174 assert!(
175 (width(&bytes, spec.codepoint, 800.0) - width(&bytes, spec.codepoint, 200.0))
176 .abs()
177 < 1.0,
178 "{} is a cell fill and must not move with the axis",
179 spec.name
180 );
181 }
182 WeightResponse::Grows => {
183 assert!(
184 (heavy - light).abs() < 0.01,
185 "{} inks {light:.3} of its box at wght 200 and {heavy:.3} at 800. \
186 A solid mark scales, so its fill is the shape's own constant.",
187 spec.name
188 );
189 let growth =
190 width(&bytes, spec.codepoint, 800.0) / width(&bytes, spec.codepoint, 200.0);
191 assert!(
192 growth > 1.10,
193 "{} grew {growth:.2}x across the whole axis, which is not a mark \
194 following a base whose stroke goes 55 units to 139",
195 spec.name
196 );
197 }
198 WeightResponse::Thickens => assert!(
199 heavy > light * 1.4,
200 "{} inks {light:.3} of its box at wght 200 and {heavy:.3} at 800, so its \
201 stroke is not following the base's",
202 spec.name
203 ),
204 }
205 }
206 }
207
208 /// The calibration holds at the far end of the axis, which is where adopting
209 /// this base broke it.
210 ///
211 /// Both numbers here are read off the base at each end rather than written
212 /// down as constants, because that is what the coefficients in the manifest
213 /// are: a relationship to the base, refitted when the base moves.
214 ///
215 /// - the ballot X is heavier than the base's own `×` and not far heavier. A
216 /// coefficient carried over from another base can leave it *lighter*.
217 /// - the space render keeps its counter open. A coefficient that is too heavy
218 /// at `wght` 800 inks most of its own bounding box, which rasterises as a
219 /// blob rather than a box.
220 #[test]
221 fn the_marks_are_still_calibrated_at_the_heavy_end() {
222 let Some((bytes, _)) = cut() else { return };
223 for wght in [200.0, 800.0] {
224 let base_cross = base_fill(0x00D7, wght);
225 let ours = fill(&bytes, 0x2718, wght);
226 assert!(
227 ours > base_cross && ours < base_cross + 0.04,
228 "at wght {wght} the ballot X inks {ours:.3} of its box against the base's own \
229 cross at {base_cross:.3}. HEAVY BALLOT X is heavier than `×`, and not far \
230 heavier: a flat multiple of the base's bar read as a blob."
231 );
232
233 // The counter is what makes an open box a box. Measured off the drawing
234 // rather than off a fill fraction, since fill cannot tell a thick box
235 // from a closed one.
236 let bar = base_stroke(wght);
237 let across = width(&bytes, 0x2423, wght) - 2.0 * bar;
238 let up = height(&bytes, 0x2423, wght) - bar;
239 assert!(
240 across / width(&bytes, 0x2423, wght) > 0.45 && up / height(&bytes, 0x2423, wght) > 0.45,
241 "at wght {wght} the space render's counter is {across:.0} x {up:.0} units inside \
242 a {:.0} x {:.0} box, which closes up rather than reading as a box",
243 width(&bytes, 0x2423, wght),
244 height(&bytes, 0x2423, wght),
245 );
246 }
247 }
248
249 /// The base's own fill for a codepoint, so a calibration is stated against the
250 /// face it was fitted to rather than against a number somebody copied.
251 fn base_fill(codepoint: u32, wght: f32) -> f32 {
252 let (face, _) = base_face().expect("the base is cached, since the cut above needed it");
253 fill(&face.bytes, codepoint, wght)
254 }
255
256 /// The base's horizontal stroke at a location, read off `-` the way the recipes
257 /// read it.
258 fn base_stroke(wght: f32) -> f32 {
259 height(&base_face().expect("cached").0.bytes, 0x002D, wght)
260 }
261
262 /// The done condition of the variable work, in the words it was written in: a
263 /// bold table border draws at the bold stroke weight.
264 ///
265 /// Measured on the bar itself rather than on a fill fraction. A box-drawing
266 /// stem is a rectangle, so its bounding box is the bar and it inks all of it at
267 /// every weight; what has to move is the bar's own thickness, and it has to
268 /// move by what the base moved — `-` goes 55 units to 139 across this axis.
269 #[test]
270 fn a_bold_border_draws_at_the_bold_stroke_weight() {
271 let Some((bytes, _)) = cut() else { return };
272 // A horizontal rule's thickness is its height; a vertical's is its width.
273 let thin = height(&bytes, 0x2500, 200.0);
274 let thick = height(&bytes, 0x2500, 800.0);
275 let ratio = thick / thin;
276 let base_ratio = 139.0 / 55.0;
277 assert!(
278 (ratio - base_ratio).abs() < 0.15,
279 "the light horizontal goes {thin:.0} to {thick:.0} ({ratio:.2}x) while the base's \
280 own bar goes 55 to 139 ({base_ratio:.2}x)"
281 );
282 let thin = width(&bytes, 0x2502, 200.0);
283 let thick = width(&bytes, 0x2502, 800.0);
284 assert!(
285 thick > thin * 2.0,
286 "the light vertical goes {thin:.0} to {thick:.0}, and the base's stem goes 54 to 158"
287 );
288 // And heavy stays twice light at both ends of the axis, or the two weights
289 // of border stop being distinguishable at one end.
290 for wght in [200.0, 800.0] {
291 let light = height(&bytes, 0x2500, wght);
292 let heavy = height(&bytes, 0x2501, wght);
293 assert!(
294 (heavy / light - 2.0).abs() < 0.1,
295 "at wght {wght} heavy is {:.2}x light",
296 heavy / light
297 );
298 }
299 // A cell fill holds: `█` is the whole cell at every weight.
300 assert!(
301 (height(&bytes, 0x2588, 800.0) - height(&bytes, 0x2588, 200.0)).abs() < 1.0,
302 "the full block moved with the axis"
303 );
304 }
305
306 /// The ExtraLight trap, as a gate rather than as a warning in a comment.
307 ///
308 /// Keeping the axis means keeping the base's default instance, and this base's
309 /// default is `wght` 200. A cut that called itself Regular would be a file every
310 /// naive `@font-face` and every `fc-match` believes.
311 #[test]
312 fn a_face_that_misnames_the_default_instance_is_refused() {
313 let Some(result) = cut_as("Regular") else {
314 return;
315 };
316 let err = result.expect_err("Regular is not this base's default instance");
317 assert!(err.contains("ExtraLight"), "{err}");
318 assert!(err.contains("wght 200"), "{err}");
319 }
320
321 /// The names other tables point at by number survive, which is what makes the
322 /// seven named instances still have names.
323 #[test]
324 fn the_names_the_axis_points_at_come_across() {
325 let Some((bytes, _)) = cut() else { return };
326 let font = FontRef::new(&bytes).unwrap();
327 let instances: Vec<String> = font
328 .named_instances()
329 .iter()
330 .map(|instance| {
331 font.localized_strings(instance.subfamily_name_id())
332 .english_or_first()
333 .map(|s| s.chars().collect())
334 .unwrap_or_default()
335 })
336 .collect();
337 assert!(
338 instances.iter().any(|n| n == "Bold") && instances.iter().any(|n| n == "ExtraLight"),
339 "the instances lost their names: {instances:?}"
340 );
341
342 // The house name reaches the records a menu reads, and the typographic pair
343 // states what the file really is.
344 let names = |id: u16| -> String {
345 font.localized_strings(skrifa::string::StringId::new(id))
346 .english_or_first()
347 .map(|s| s.chars().collect())
348 .unwrap_or_default()
349 };
350 assert_eq!(names(1), "Quasi Mono ExtraLight");
351 assert_eq!(names(2), "Regular");
352 assert_eq!(names(16), "Quasi Mono");
353 assert_eq!(names(17), "ExtraLight");
354 assert_eq!(names(25), "QuasiMono");
355 assert!(names(10).contains("Atkinson Hyperlegible Mono 2.001"));
356 }
357
358 /// Atkinson ships no box drawing, no block elements and none of the four
359 /// arrows; a face cut from it covers the whole floor a consumer is entitled to
360 /// assume, which is what lets the mono slot point at this base.
361 #[test]
362 fn the_cut_face_is_fit_for_the_mono_slot() {
363 let Some((bytes, specs)) = cut() else { return };
364 let house: Vec<u32> = specs.iter().map(|g| g.codepoint).collect();
365 let result = coverage::check(
366 &coverage::ALLOY_SURFACE,
367 &compose::coverage(&bytes).unwrap(),
368 &house,
369 );
370 assert!(
371 result.ok(),
372 "missing {:?}",
373 result
374 .missing
375 .iter()
376 .map(|c| format!("U+{c:04X}"))
377 .collect::<Vec<_>>()
378 );
379 // The four the base has none of, one from each tier that had to be built:
380 // a box-drawing stem, a fill, an eighth, and a shade.
381 for cell_furniture in [0x2502, 0x2588, 0x258F, 0x2591] {
382 assert!(
383 house.contains(&cell_furniture),
384 "U+{cell_furniture:04X} is not in the set, so the base must have it \
385 and Atkinson does not"
386 );
387 }
388 }
389
390 #[test]
391 fn the_base_keeps_every_glyph_and_every_codepoint_it_had() {
392 let Some((face, _)) = base_face() else { return };
393 let before = base::mappings(&face.bytes).unwrap();
394 let base_glyphs = FontRef::new(&face.bytes)
395 .unwrap()
396 .maxp()
397 .unwrap()
398 .num_glyphs();
399 let (bytes, specs) = cut().unwrap();
400 let after = compose::coverage(&bytes).unwrap();
401 for (codepoint, gid) in &before {
402 assert_eq!(after.get(codepoint), Some(gid), "U+{codepoint:04X} moved");
403 }
404 assert_eq!(
405 FontRef::new(&bytes).unwrap().maxp().unwrap().num_glyphs(),
406 base_glyphs + specs.len() as u16
407 );
408 }
409
410 #[test]
411 fn the_same_checkout_cuts_the_same_bytes() {
412 let Some((first, _)) = cut() else { return };
413 let (second, _) = cut().unwrap();
414 assert_eq!(first, second, "the build is not reproducible");
415 }
416
417 #[test]
418 fn the_woff2_still_encodes() {
419 let Some((bytes, _)) = cut() else { return };
420 let web = woff2::encode(&bytes).unwrap();
421 assert_eq!(&web[0..4], b"wOF2");
422 assert!(web.len() < bytes.len());
423 }
424
425 /// A glyph's outline at one point on the axis, as a `(bbox width, ink area)`.
426 fn outline(bytes: &[u8], codepoint: u32, wght: f32) -> (f32, f32, f32) {
427 let font = FontRef::new(bytes).unwrap();
428 let gid: GlyphId = compose::coverage(bytes).unwrap()[&codepoint];
429 let location = font.axes().location([("wght", wght)]);
430 let mut pen = Trace::default();
431 font.outline_glyphs()
432 .get(gid)
433 .unwrap()
434 .draw(
435 skrifa::outline::DrawSettings::unhinted(Size::unscaled(), &location),
436 &mut pen,
437 )
438 .unwrap();
439 (pen.x1 - pen.x0, pen.y1 - pen.y0, pen.area.abs() / 2.0)
440 }
441
442 fn width(bytes: &[u8], codepoint: u32, wght: f32) -> f32 {
443 outline(bytes, codepoint, wght).0
444 }
445
446 fn height(bytes: &[u8], codepoint: u32, wght: f32) -> f32 {
447 outline(bytes, codepoint, wght).1
448 }
449
450 fn ink(bytes: &[u8], codepoint: u32, wght: f32) -> f32 {
451 outline(bytes, codepoint, wght).2
452 }
453
454 /// How much of its own bounding box a mark inks in.
455 fn fill(bytes: &[u8], codepoint: u32, wght: f32) -> f32 {
456 let (width, height, ink) = outline(bytes, codepoint, wght);
457 ink / (width * height)
458 }
459
460 /// Accumulates a drawn outline's extent and its shoelace area. Every mark in the
461 /// set is straight-edged, so summing the segments is exact rather than an
462 /// approximation of a curve.
463 #[derive(Default)]
464 struct Trace {
465 x0: f32,
466 x1: f32,
467 y0: f32,
468 y1: f32,
469 area: f32,
470 start: (f32, f32),
471 at: (f32, f32),
472 started: bool,
473 }
474
475 impl Trace {
476 fn point(&mut self, x: f32, y: f32) {
477 if !self.started {
478 self.x0 = x;
479 self.x1 = x;
480 self.y0 = y;
481 self.y1 = y;
482 self.started = true;
483 }
484 self.x0 = self.x0.min(x);
485 self.x1 = self.x1.max(x);
486 self.y0 = self.y0.min(y);
487 self.y1 = self.y1.max(y);
488 }
489
490 fn edge(&mut self, x: f32, y: f32) {
491 self.area += self.at.0 * y - x * self.at.1;
492 self.at = (x, y);
493 self.point(x, y);
494 }
495 }
496
497 impl skrifa::outline::OutlinePen for Trace {
498 fn move_to(&mut self, x: f32, y: f32) {
499 self.point(x, y);
500 self.start = (x, y);
501 self.at = (x, y);
502 }
503
504 fn line_to(&mut self, x: f32, y: f32) {
505 self.edge(x, y);
506 }
507
508 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
509 self.edge(cx0, cy0);
510 self.edge(x, y);
511 }
512
513 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
514 self.edge(cx0, cy0);
515 self.edge(cx1, cy1);
516 self.edge(x, y);
517 }
518
519 fn close(&mut self) {
520 let (x, y) = self.start;
521 self.edge(x, y);
522 }
523 }
524