open-standards/wai/wai-vector/tests/stroke_sets.rs
2026-10-11 17:46:35 -04:00

221 lines
10 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The in-house sets (`graphics-vector.md` §4, Annexes A and B): the default stroke set's
//! digest equals Annex B; Annex A states the expansion's table; the expansion is
//! deterministic and re-derives both committed sets from their committed sources; the
//! sets' notes carry their licence; and the test repertoire's joining table picks the
//! forms of the test words.
//!
//! Every leg runs these: natively, and under WASI with the `wai` directory preopened.
use std::path::PathBuf;
use wai_vector::builtin;
use wai_vector::json::hex;
use wai_vector::sha256::sha256;
use wai_vector::vectors::{repertoire, stroke, text};
use wai_vector::wsf1;
fn wai() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn read(rel: &str) -> Vec<u8> {
std::fs::read(wai().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
}
/// The text of the extension from `heading` up to the next heading of the same level.
fn section(heading: &str) -> String {
let doc = String::from_utf8(read("extensions/graphics-vector.md")).unwrap();
let start = doc.find(heading).unwrap_or_else(|| panic!("no {heading}"));
let level = heading.split(' ').next().unwrap();
let rest = &doc[start + heading.len()..];
let end = rest.find(&format!("\n{level} ")).unwrap_or(rest.len());
rest[..end].to_string()
}
/// The first run of 64 lowercase hexadecimal digits on the line holding `label`.
fn digest_on(text: &str, label: &str) -> String {
let line = text.lines().find(|l| l.contains(label)).unwrap_or_else(|| panic!("no line with {label}"));
line.split(|c: char| !c.is_ascii_hexdigit()).find(|w| w.len() == 64).unwrap_or_else(|| panic!("no digest on {line}")).to_string()
}
/// The number before " bytes" on the line holding `label`, commas removed.
fn length_on(text: &str, label: &str) -> usize {
let line = text.lines().find(|l| l.contains(label)).unwrap();
let before = &line[..line.find(" bytes").unwrap()];
before.rsplit("length ").next().unwrap().replace(',', "").parse().unwrap()
}
#[test]
fn the_stroke_sets_digest_equals_annex_b() {
let b = section("## Annex B.");
let (set_digest, src_digest) = (digest_on(&b, "the set (`WOS1`)"), digest_on(&b, "its source (`WSF1`)"));
// The bundled bytes, their constant and the annex agree.
assert_eq!(hex(&sha256(builtin::STROKE_ASCII)), set_digest, "the bundled set's SHA-256 is Annex B's");
assert_eq!(hex(&builtin::STROKE_ASCII_SHA256), set_digest, "the digest constant is Annex B's");
assert_eq!(builtin::STROKE_ASCII.len(), length_on(&b, "the set (`WOS1`)"));
assert_eq!(builtin::STROKE_ASCII_LEN as usize, builtin::STROKE_ASCII.len());
// The committed source is Annex B's, and expanding it by Annex A gives the set.
let src = read("vector-conformance/sources/stroke-ascii.wsf1");
assert_eq!(hex(&sha256(&src)), src_digest, "the committed source's SHA-256 is Annex B's");
assert_eq!(src.len(), length_on(&b, "its source (`WSF1`)"));
let set = wsf1::expand(&src).unwrap().write();
assert_eq!(hex(&sha256(&set)), set_digest, "Annex A's expansion of the source gives Annex B's set");
assert!(set == builtin::STROKE_ASCII);
// Coverage: printable ASCII, each glyph ID its code point.
let w = wai_vector::wos1::parse(builtin::STROKE_ASCII).unwrap();
assert_eq!(w.glyphs.iter().map(|g| g.gid).collect::<Vec<u16>>(), (0x20..=0x7E).collect::<Vec<u16>>());
}
#[test]
fn annex_a_states_the_table() {
let a = section("### A.2 ");
let block = &a[a.find("```text").unwrap() + 7..];
let block = &block[..block.find("```").unwrap()];
let hexs: String = block.split_whitespace().collect();
assert_eq!(hexs, hex(&wsf1::unit_table_bytes()), "Annex A's bytes are the table the expansion reads");
assert_eq!(digest_on(&a, "Their SHA-256"), hex(&sha256(&wsf1::unit_table_bytes())));
}
#[test]
fn the_expansion_is_deterministic() {
// Offsets worked by hand. (300, 400) has an exact length; (100, 200) does not:
// isqrt(50000) = 223, and −18000/223 = −80.72 rounds to −81, 9000/223 = 40.36 to 40.
assert_eq!(wsf1::normal((300, 400), 80), (-64, 48));
assert_eq!(wsf1::normal((100, 200), 90), (-81, 40));
assert_eq!(wsf1::normal((-7, 3), 80), (-34, -80), "isqrt(58) = 7: (R_rat(−240, 7), R_rat(−560, 7))");
// Both committed sets re-derive from their committed sources, twice, byte for byte.
for name in ["stroke-ascii", "test-repertoire"] {
let src = read(&format!("vector-conformance/sources/{name}.wsf1"));
let set = read(&format!("vector-conformance/sets/{name}.wos1"));
let a = wsf1::expand(&src).unwrap().write();
let b = wsf1::expand(&src).unwrap().write();
assert!(a == b, "{name}: two expansions agree");
assert!(a == set, "{name}: the expansion reproduces the committed set");
// The source written by the generator is the committed one.
let generated = if name == "stroke-ascii" { stroke::ascii_source_bytes() } else { repertoire::repertoire_source_bytes() };
assert!(generated == src, "{name}: the generator writes the committed source");
// The set's notes name its source by SHA-256, and the expansion.
let w = wai_vector::wos1::parse(&set).unwrap();
let notes = String::from_utf8(w.notes.clone()).unwrap();
assert!(notes.starts_with(&format!("parent_sha256={}\nconverter=wsf1-expansion/1\n", hex(&sha256(&src)))), "{name}");
// A polygon-only set: every point on-curve, overlaps not removed.
assert!(w.points.iter().all(|p| p.tag == 0) && w.flags == 0, "{name}");
}
}
#[test]
fn each_set_carries_its_licence() {
for name in ["stroke-ascii", "test-repertoire"] {
let set = read(&format!("vector-conformance/sets/{name}.wos1"));
let w = wai_vector::wos1::parse(&set).unwrap();
let notes = String::from_utf8(w.notes).unwrap();
let (keys, licence) = notes.split_once("\n\n").expect("key lines, a blank line, the licence text");
let key = |k: &str| keys.lines().find_map(|l| l.strip_prefix(&format!("{k}="))).unwrap_or_else(|| panic!("{name}: no {k}"));
assert_eq!(key("licence"), "Apache-2.0", "{name}");
assert_eq!(key("licence_sha256"), hex(&sha256(licence.as_bytes())), "{name}: the verifier's licence check");
assert_eq!(key("rfn"), "none", "{name}");
assert!(key("source").contains("no outline is traced from any font"), "{name}");
assert!(licence.contains("Apache License") && licence.contains("Version 2.0"), "{name}");
}
}
#[test]
fn the_joining_table_picks_the_forms_of_the_test_words() {
for (word, want) in text::test_words() {
let got: Vec<u16> = repertoire::shape_arabic(word).0.iter().map(|g| g.gid).collect();
assert_eq!(got, want, "{word}");
}
// Every form the table names is drawn in the test repertoire.
let src = repertoire::repertoire_source();
for (_, _, forms) in repertoire::JOINING {
for f in forms {
assert!(src.glyph(f).is_some(), "form {f:04X} is drawn");
}
}
}
/// The winding number of the closed polygon contours of a glyph about `(px, py)`.
fn winding(contours: &[Vec<(i64, i64)>], px: i64, py: i64) -> i64 {
let mut w = 0;
for c in contours {
for i in 0..c.len() {
let (a, b) = (c[i], c[(i + 1) % c.len()]);
let side = (b.0 - a.0) * (py - a.1) - (b.1 - a.1) * (px - a.0);
if a.1 <= py && py < b.1 && side > 0 {
w += 1;
} else if b.1 <= py && py < a.1 && side < 0 {
w -= 1;
}
}
}
w
}
/// Whether `(px, py)` lies within `r` of a centre-line polyline, exactly in integers.
fn within(stroke: &[(i16, i16)], px: i64, py: i64, r: i64) -> bool {
let p: Vec<(i64, i64)> = stroke.iter().map(|&(x, y)| (i64::from(x), i64::from(y))).collect();
let d2 = |a: (i64, i64)| (px - a.0).pow(2) + (py - a.1).pow(2) <= r * r;
if p.len() == 1 {
return d2(p[0]);
}
p.windows(2).any(|s| {
let (a, b) = (s[0], s[1]);
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let l = dx * dx + dy * dy;
let t = (px - a.0) * dx + (py - a.1) * dy;
if t <= 0 {
d2(a)
} else if t >= l {
d2(b)
} else {
let c = i128::from((px - a.0) * dy - (py - a.1) * dx);
c * c <= i128::from(r * r) * i128::from(l)
}
})
}
#[test]
fn the_expansion_covers_the_pens_path() {
// Sampled every 20 outline units: a point within ρ − 4 of a centre line is inside
// the glyph's fill (non-zero winding), and a point farther than ρ + 8 from every
// centre line is outside it. The bands absorb the 64-gon's chords and the offsets'
// rounding; the inner loops of joins must not open a hole, and no outer join may
// cut its corner.
for src in [stroke::ascii_source(), repertoire::repertoire_source()] {
let rho = i64::from(src.radius);
let set = wsf1::expand(&src.write()).unwrap();
for g in &src.glyphs {
let rec = set.glyph(g.gid).unwrap();
let pts = set.glyph_points(rec);
let mut contours = Vec::new();
let mut start = 0usize;
for &e in set.glyph_ends(rec) {
contours.push(pts[start..=e as usize].iter().map(|p| (i64::from(p.x), i64::from(p.y))).collect::<Vec<_>>());
start = e as usize + 1;
}
let (x0, y0, x1, y1) = (i64::from(rec.xmin) - 20, i64::from(rec.ymin) - 20, i64::from(rec.xmax) + 20, i64::from(rec.ymax) + 20);
let (mut inside, mut outside) = (0, 0);
let mut y = y0 + 3;
while y <= y1 {
let mut x = x0 + 7;
while x <= x1 {
let near = g.strokes.iter().any(|s| within(s, x, y, rho - 4));
let far = !g.strokes.iter().any(|s| within(s, x, y, rho + 8));
let w = winding(&contours, x, y);
if near {
inside += 1;
assert!(w != 0, "glyph {:04X}: ({x}, {y}) is within the pen's path but not filled", g.gid);
}
if far {
outside += 1;
assert!(w == 0, "glyph {:04X}: ({x}, {y}) is outside the pen's path but winds {w}", g.gid);
}
x += 20;
}
y += 20;
}
assert!(g.strokes.is_empty() || inside > 0, "glyph {:04X}: the samples reach its strokes", g.gid);
assert!(outside > 0);
}
}
}