- WSF1 (feature author): the stroke source format, its checks, the pinned Q15 table of 64 unit vectors and the integer expansion into a polygon-only WOS1. - The ASCII stroke set, U+0020-U+007E, drawn here as integer centre lines; bundled as builtin::STROKE_ASCII with its SHA-256, which Annex B states. - The test repertoire: Arabic, Hebrew and Devanagari glyphs and a U+1F600 face, drawn here, with the joining table and the small shaper the corpus generator uses. - graphics-vector.md section 4, Annex A (WSF1, the table as bytes, the expansion) and Annex B (the digest). - vector-conformance: sources/, the two sets, the text group, and stroke-glyph members of runs and motion; the README's provenance line names the in-house sets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
543 lines
20 KiB
Rust
543 lines
20 KiB
Rust
//! `WSF1`, the stroke source format, and its integer expansion into a `WOS1`
|
||
//! (`graphics-vector.md` Annex A). Authoring only: a sink never reads a `WSF1` and
|
||
//! has no stroker (§3.14), so this module is behind the feature `author`.
|
||
//!
|
||
//! A source holds, for each glyph, stroke paths: polylines of integer centre-line
|
||
//! points in outline units, drawn with one round pen of radius `ρ`. The expansion
|
||
//! turns each stroke path into one closed polygon contour:
|
||
//! - each segment `(dx, dy)` is offset by `n = (R_rat(−dy·ρ, len), R_rat(dx·ρ, len))`
|
||
//! with `len = isqrt(dx² + dy²)`;
|
||
//! - caps and the outer side of joins take their vertices from [`UNIT`], the pinned
|
||
//! table of 64 unit vectors in Q15, each placed at `R_half_up(ρ·u, 15)`, chosen by
|
||
//! exact cross-product sign tests;
|
||
//! - the inner side of a join takes the two offset points.
|
||
//!
|
||
//! No trigonometry and no floating point run anywhere: the same source gives the same
|
||
//! `WOS1` bytes on every machine.
|
||
|
||
use alloc::vec::Vec;
|
||
use core::cmp::Ordering;
|
||
|
||
use crate::arith::{r_half_up, r_rat};
|
||
use crate::json::hex;
|
||
use crate::sha256::sha256;
|
||
use crate::wos1::{Point, Wos1};
|
||
|
||
/// The header's size.
|
||
pub const HEADER: usize = 32;
|
||
/// A glyph record's size.
|
||
pub const GLYPH: usize = 8;
|
||
/// The caps of Annex A. They make every expansion fit `WOS1`'s caps (§2.1): a stroke
|
||
/// of `m ≥ 2` points expands to at most `36·m − 4` points and a dot to 64, so a glyph
|
||
/// expands to at most `36·256 + 64·64 = 13,312 ≤ 16,383` points, and a source to at
|
||
/// most `36·2^16 + 64·2^14 ≤ 2^22`.
|
||
pub const GLYPHS_MAX: u64 = 65_536;
|
||
pub const STROKES_TOTAL_MAX: u64 = 1 << 14;
|
||
pub const POINTS_TOTAL_MAX: u64 = 1 << 16;
|
||
pub const NOTES_MAX: u64 = 1 << 19;
|
||
pub const GLYPH_STROKES_MAX: u16 = 64;
|
||
pub const GLYPH_POINTS_MAX: u16 = 256;
|
||
pub const RADIUS_MAX: u16 = 4096;
|
||
|
||
/// The expansion's name and version, written into the notes of every set it makes.
|
||
pub const CONVERTER: &str = "wsf1-expansion/1";
|
||
|
||
/// The pinned table (Annex A): `U[k] = (round(2^15·cos(2πk/64)), round(2^15·sin(2πk/64)))`
|
||
/// for `k = 0 … 63`, rounded once to the nearest integer (no value lies within 0.02 of
|
||
/// a tie) and frozen. With y down, `k` increases from +x toward +y. The bytes of
|
||
/// Annex A, each component an `i32` little-endian in this order, are normative.
|
||
pub const UNIT: [(i32, i32); 64] = [
|
||
(32768, 0),
|
||
(32610, 3212),
|
||
(32138, 6393),
|
||
(31357, 9512),
|
||
(30274, 12540),
|
||
(28899, 15447),
|
||
(27246, 18205),
|
||
(25330, 20788),
|
||
(23170, 23170),
|
||
(20788, 25330),
|
||
(18205, 27246),
|
||
(15447, 28899),
|
||
(12540, 30274),
|
||
(9512, 31357),
|
||
(6393, 32138),
|
||
(3212, 32610),
|
||
(0, 32768),
|
||
(-3212, 32610),
|
||
(-6393, 32138),
|
||
(-9512, 31357),
|
||
(-12540, 30274),
|
||
(-15447, 28899),
|
||
(-18205, 27246),
|
||
(-20788, 25330),
|
||
(-23170, 23170),
|
||
(-25330, 20788),
|
||
(-27246, 18205),
|
||
(-28899, 15447),
|
||
(-30274, 12540),
|
||
(-31357, 9512),
|
||
(-32138, 6393),
|
||
(-32610, 3212),
|
||
(-32768, 0),
|
||
(-32610, -3212),
|
||
(-32138, -6393),
|
||
(-31357, -9512),
|
||
(-30274, -12540),
|
||
(-28899, -15447),
|
||
(-27246, -18205),
|
||
(-25330, -20788),
|
||
(-23170, -23170),
|
||
(-20788, -25330),
|
||
(-18205, -27246),
|
||
(-15447, -28899),
|
||
(-12540, -30274),
|
||
(-9512, -31357),
|
||
(-6393, -32138),
|
||
(-3212, -32610),
|
||
(0, -32768),
|
||
(3212, -32610),
|
||
(6393, -32138),
|
||
(9512, -31357),
|
||
(12540, -30274),
|
||
(15447, -28899),
|
||
(18205, -27246),
|
||
(20788, -25330),
|
||
(23170, -23170),
|
||
(25330, -20788),
|
||
(27246, -18205),
|
||
(28899, -15447),
|
||
(30274, -12540),
|
||
(31357, -9512),
|
||
(32138, -6393),
|
||
(32610, -3212),
|
||
];
|
||
|
||
/// The table as Annex A states it: 512 bytes, `cos` then `sin` of each entry, each an
|
||
/// `i32` little-endian.
|
||
pub fn unit_table_bytes() -> Vec<u8> {
|
||
let mut o = Vec::with_capacity(512);
|
||
for (c, s) in UNIT {
|
||
o.extend_from_slice(&c.to_le_bytes());
|
||
o.extend_from_slice(&s.to_le_bytes());
|
||
}
|
||
o
|
||
}
|
||
|
||
/// A glyph of a source: its ID, its advance (read by the authoring tool's layout, never
|
||
/// by a sink), and its stroke paths.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Glyph {
|
||
pub gid: u16,
|
||
pub advance: u16,
|
||
pub strokes: Vec<Vec<(i16, i16)>>,
|
||
}
|
||
|
||
/// A parsed `WSF1`.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Wsf1 {
|
||
pub upem2: u16,
|
||
pub radius: u16,
|
||
pub glyphs: Vec<Glyph>,
|
||
pub notes: Vec<u8>,
|
||
}
|
||
|
||
/// Why a source is refused: the Annex A check that failed, and where.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct SourceError {
|
||
pub check: u8,
|
||
pub rule: &'static str,
|
||
pub index: u64,
|
||
}
|
||
|
||
fn err<T>(check: u8, rule: &'static str, index: u64) -> Result<T, SourceError> {
|
||
Err(SourceError { check, rule, index })
|
||
}
|
||
|
||
fn rd16(b: &[u8], o: usize) -> u16 {
|
||
u16::from_le_bytes([b[o], b[o + 1]])
|
||
}
|
||
fn rd32(b: &[u8], o: usize) -> u32 {
|
||
u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||
}
|
||
|
||
/// The exact length a header's counts give.
|
||
pub fn exact_len(n_glyphs: u64, strokes_total: u64, points_total: u64, notes_len: u64) -> u64 {
|
||
HEADER as u64 + GLYPH as u64 * n_glyphs + 2 * strokes_total + 4 * points_total + notes_len
|
||
}
|
||
|
||
/// `cross(a, b) = a.x·b.y − a.y·b.x`.
|
||
#[inline]
|
||
fn cross(a: (i64, i64), b: (i64, i64)) -> i64 {
|
||
a.0 * b.1 - a.1 * b.0
|
||
}
|
||
|
||
/// Parse a `WSF1`, running Annex A's checks in their order and reporting the first
|
||
/// failure.
|
||
pub fn parse(b: &[u8]) -> Result<Wsf1, SourceError> {
|
||
// Check 1.
|
||
if b.len() < HEADER {
|
||
return err(1, "the header is incomplete", 0);
|
||
}
|
||
if &b[0..4] != b"WSF1" {
|
||
return err(1, "magic", 0);
|
||
}
|
||
if rd32(b, 4) != 1 {
|
||
return err(1, "format", 0);
|
||
}
|
||
if rd32(b, 28) != 0 {
|
||
return err(1, "zero", 0);
|
||
}
|
||
let upem2 = rd16(b, 8);
|
||
let radius = rd16(b, 10);
|
||
// Check 2, in field order.
|
||
if radius == 0 || radius > RADIUS_MAX {
|
||
return err(2, "radius", 0);
|
||
}
|
||
let n_glyphs = u64::from(rd32(b, 12));
|
||
let strokes_total = u64::from(rd32(b, 16));
|
||
let points_total = u64::from(rd32(b, 20));
|
||
let notes_len = u64::from(rd32(b, 24));
|
||
if n_glyphs == 0 || n_glyphs > GLYPHS_MAX {
|
||
return err(2, "n_glyphs", 0);
|
||
}
|
||
if strokes_total > STROKES_TOTAL_MAX {
|
||
return err(2, "strokes_total", 0);
|
||
}
|
||
if points_total > POINTS_TOTAL_MAX {
|
||
return err(2, "points_total", 0);
|
||
}
|
||
if notes_len == 0 || notes_len > NOTES_MAX {
|
||
return err(2, "notes_len", 0);
|
||
}
|
||
// Check 3.
|
||
if exact_len(n_glyphs, strokes_total, points_total, notes_len) != b.len() as u64 {
|
||
return err(3, "exact length", 0);
|
||
}
|
||
let (ng, ns, np) = (n_glyphs as usize, strokes_total as usize, points_total as usize);
|
||
let e0 = HEADER + GLYPH * ng;
|
||
let p0 = e0 + 2 * ns;
|
||
let n0 = p0 + 4 * np;
|
||
// Check 4: each glyph record.
|
||
let mut recs: Vec<(u16, u16, u16, u16)> = Vec::with_capacity(ng);
|
||
for i in 0..ng {
|
||
let o = HEADER + GLYPH * i;
|
||
let (gid, n_strokes, n_points, advance) = (rd16(b, o), rd16(b, o + 2), rd16(b, o + 4), rd16(b, o + 6));
|
||
if n_strokes > GLYPH_STROKES_MAX || n_points > GLYPH_POINTS_MAX {
|
||
return err(4, "a glyph's strokes or points above its cap", i as u64);
|
||
}
|
||
if n_strokes > n_points || (n_points >= 1 && n_strokes == 0) {
|
||
return err(4, "n_strokes against n_points", i as u64);
|
||
}
|
||
recs.push((gid, n_strokes, n_points, advance));
|
||
}
|
||
// Check 5.
|
||
for i in 1..ng {
|
||
if recs[i].0 <= recs[i - 1].0 {
|
||
return err(5, "gids strictly increase", i as u64);
|
||
}
|
||
}
|
||
// Check 6.
|
||
let ss: u64 = recs.iter().map(|r| u64::from(r.1)).sum();
|
||
let sp: u64 = recs.iter().map(|r| u64::from(r.2)).sum();
|
||
if ss != strokes_total || sp != points_total {
|
||
return err(6, "sums", 0);
|
||
}
|
||
// Check 7: stroke ends.
|
||
let ends: Vec<u16> = (0..ns).map(|i| rd16(b, e0 + 2 * i)).collect();
|
||
let pts: Vec<(i16, i16)> = (0..np).map(|i| (rd16(b, p0 + 4 * i) as i16, rd16(b, p0 + 4 * i + 2) as i16)).collect();
|
||
let (mut fe, mut fp) = (0usize, 0usize);
|
||
let mut glyphs: Vec<Glyph> = Vec::with_capacity(ng);
|
||
for (i, &(gid, n_strokes, n_points, advance)) in recs.iter().enumerate() {
|
||
let ge = &ends[fe..fe + n_strokes as usize];
|
||
if ge.windows(2).any(|w| w[1] <= w[0]) || ge.last().is_some_and(|&l| u32::from(l) + 1 != u32::from(n_points)) {
|
||
return err(7, "stroke ends", i as u64);
|
||
}
|
||
let gp = &pts[fp..fp + n_points as usize];
|
||
let mut strokes = Vec::with_capacity(ge.len());
|
||
let mut start = 0usize;
|
||
for &e in ge {
|
||
strokes.push(gp[start..=e as usize].to_vec());
|
||
start = e as usize + 1;
|
||
}
|
||
glyphs.push(Glyph { gid, advance, strokes });
|
||
fe += n_strokes as usize;
|
||
fp += n_points as usize;
|
||
}
|
||
// Check 8: every point leaves room for the pen inside i16.
|
||
let r = i32::from(radius);
|
||
if let Some(i) = pts.iter().position(|&(x, y)| {
|
||
let ok = |v: i16| i32::from(v) - r >= i32::from(i16::MIN) && i32::from(v) + r <= i32::from(i16::MAX);
|
||
!(ok(x) && ok(y))
|
||
}) {
|
||
return err(8, "a point within the radius of the i16 range's edge", i as u64);
|
||
}
|
||
// Check 9: consecutive points differ, and no interior vertex reverses.
|
||
for (i, g) in glyphs.iter().enumerate() {
|
||
for s in &g.strokes {
|
||
let d: Vec<(i64, i64)> = s.windows(2).map(|w| (i64::from(w[1].0) - i64::from(w[0].0), i64::from(w[1].1) - i64::from(w[0].1))).collect();
|
||
if d.contains(&(0, 0)) {
|
||
return err(9, "two consecutive points of a stroke are equal", i as u64);
|
||
}
|
||
if d.windows(2).any(|w| cross(w[0], w[1]) == 0 && w[0].0 * w[1].0 + w[0].1 * w[1].1 < 0) {
|
||
return err(9, "a stroke reverses on itself", i as u64);
|
||
}
|
||
}
|
||
}
|
||
// Check 10.
|
||
if core::str::from_utf8(&b[n0..]).is_err() {
|
||
return err(10, "notes UTF-8", 0);
|
||
}
|
||
Ok(Wsf1 { upem2, radius, glyphs, notes: b[n0..].to_vec() })
|
||
}
|
||
|
||
impl Wsf1 {
|
||
/// The canonical bytes.
|
||
pub fn write(&self) -> Vec<u8> {
|
||
let ns: usize = self.glyphs.iter().map(|g| g.strokes.len()).sum();
|
||
let np: usize = self.glyphs.iter().flat_map(|g| g.strokes.iter()).map(Vec::len).sum();
|
||
let mut o = Vec::with_capacity(exact_len(self.glyphs.len() as u64, ns as u64, np as u64, self.notes.len() as u64) as usize);
|
||
o.extend_from_slice(b"WSF1");
|
||
o.extend_from_slice(&1u32.to_le_bytes());
|
||
o.extend_from_slice(&self.upem2.to_le_bytes());
|
||
o.extend_from_slice(&self.radius.to_le_bytes());
|
||
o.extend_from_slice(&(self.glyphs.len() as u32).to_le_bytes());
|
||
o.extend_from_slice(&(ns as u32).to_le_bytes());
|
||
o.extend_from_slice(&(np as u32).to_le_bytes());
|
||
o.extend_from_slice(&(self.notes.len() as u32).to_le_bytes());
|
||
o.extend_from_slice(&0u32.to_le_bytes());
|
||
for g in &self.glyphs {
|
||
let n: usize = g.strokes.iter().map(Vec::len).sum();
|
||
for v in [g.gid, g.strokes.len() as u16, n as u16, g.advance] {
|
||
o.extend_from_slice(&v.to_le_bytes());
|
||
}
|
||
}
|
||
for g in &self.glyphs {
|
||
let mut end = 0usize;
|
||
for s in &g.strokes {
|
||
end += s.len();
|
||
o.extend_from_slice(&((end - 1) as u16).to_le_bytes());
|
||
}
|
||
}
|
||
for g in &self.glyphs {
|
||
for &(x, y) in g.strokes.iter().flatten() {
|
||
o.extend_from_slice(&x.to_le_bytes());
|
||
o.extend_from_slice(&y.to_le_bytes());
|
||
}
|
||
}
|
||
o.extend_from_slice(&self.notes);
|
||
o
|
||
}
|
||
|
||
/// The glyph of `gid`.
|
||
pub fn glyph(&self, gid: u16) -> Option<&Glyph> {
|
||
self.glyphs.binary_search_by_key(&gid, |g| g.gid).ok().map(|i| &self.glyphs[i])
|
||
}
|
||
}
|
||
|
||
/// `isqrt(n)`: the largest `r ≥ 0` with `r² ≤ n` ([integer-operators] §2), by Newton's
|
||
/// iteration from above, which is exact on integers.
|
||
pub fn isqrt(n: u64) -> u64 {
|
||
if n < 2 {
|
||
return n;
|
||
}
|
||
let mut x = n;
|
||
let mut y = x.div_ceil(2);
|
||
while y < x {
|
||
x = y;
|
||
y = (x + n / x) / 2;
|
||
}
|
||
x
|
||
}
|
||
|
||
/// The offset of a segment `d` for pen radius `ρ`: `(R_rat(−dy·ρ, len), R_rat(dx·ρ, len))`
|
||
/// with `len = isqrt(dx² + dy²)`.
|
||
pub fn normal(d: (i64, i64), rho: i64) -> (i64, i64) {
|
||
let len = isqrt((d.0 * d.0 + d.1 * d.1) as u64) as i64;
|
||
(r_rat(-d.1 * rho, len), r_rat(d.0 * rho, len))
|
||
}
|
||
|
||
fn add(p: (i64, i64), n: (i64, i64)) -> (i64, i64) {
|
||
(p.0 + n.0, p.1 + n.1)
|
||
}
|
||
fn sub(p: (i64, i64), n: (i64, i64)) -> (i64, i64) {
|
||
(p.0 - n.0, p.1 - n.1)
|
||
}
|
||
fn neg(n: (i64, i64)) -> (i64, i64) {
|
||
(-n.0, -n.1)
|
||
}
|
||
|
||
fn unit(k: usize) -> (i64, i64) {
|
||
(i64::from(UNIT[k].0), i64::from(UNIT[k].1))
|
||
}
|
||
|
||
/// The arc vertices at `p` from the offset `a` toward `b`, in the negative sense (the
|
||
/// sense of decreasing `k`): every table direction strictly between them, in order
|
||
/// from `a`. With `b = None` the arc is a cap, from `a` to `−a`: the directions `u`
|
||
/// with `cross(a, u) < 0`. With `b = Some(b)` it is a join, and is empty unless
|
||
/// `cross(a, b) < 0`: the directions with `cross(a, u) < 0` and `cross(u, b) < 0`.
|
||
fn arc(out: &mut Vec<(i64, i64)>, p: (i64, i64), a: (i64, i64), b: Option<(i64, i64)>, rho: i64) {
|
||
if let Some(b) = b
|
||
&& cross(a, b) >= 0
|
||
{
|
||
return;
|
||
}
|
||
let mut ks: Vec<usize> = (0..64).filter(|&k| cross(a, unit(k)) < 0 && b.is_none_or(|b| cross(unit(k), b) < 0)).collect();
|
||
// All selected directions lie in one open half-plane, so `cross` orders them: `u`
|
||
// comes before `v` when `v` lies in the negative sense from `u`.
|
||
ks.sort_by(|&i, &j| match cross(unit(i), unit(j)) {
|
||
c if c < 0 => Ordering::Less,
|
||
c if c > 0 => Ordering::Greater,
|
||
_ => Ordering::Equal,
|
||
});
|
||
for k in ks {
|
||
let u = unit(k);
|
||
out.push((p.0 + r_half_up(rho * u.0, 15), p.1 + r_half_up(rho * u.1, 15)));
|
||
}
|
||
}
|
||
|
||
/// One stroke path's closed contour (Annex A): the left offsets forward, the end cap,
|
||
/// the right offsets backward, then the start cap; a single point is a dot, every
|
||
/// table direction from `k = 0` in the negative sense. A vertex equal to the one before
|
||
/// it is written once, and a last vertex equal to the first is dropped.
|
||
pub fn stroke_contour(stroke: &[(i16, i16)], rho: i64) -> Vec<Point> {
|
||
let p: Vec<(i64, i64)> = stroke.iter().map(|&(x, y)| (i64::from(x), i64::from(y))).collect();
|
||
let m = p.len();
|
||
let mut v: Vec<(i64, i64)> = Vec::new();
|
||
if m == 1 {
|
||
for t in 0..64 {
|
||
let u = unit((64 - t) % 64);
|
||
v.push((p[0].0 + r_half_up(rho * u.0, 15), p[0].1 + r_half_up(rho * u.1, 15)));
|
||
}
|
||
} else {
|
||
let d: Vec<(i64, i64)> = p.windows(2).map(|w| (w[1].0 - w[0].0, w[1].1 - w[0].1)).collect();
|
||
let n: Vec<(i64, i64)> = d.iter().map(|&d| normal(d, rho)).collect();
|
||
// The left side, forward. Turning in the negative sense, it is the outer side.
|
||
v.push(add(p[0], n[0]));
|
||
for i in 1..m - 1 {
|
||
v.push(add(p[i], n[i - 1]));
|
||
if cross(d[i - 1], d[i]) < 0 {
|
||
arc(&mut v, p[i], n[i - 1], Some(n[i]), rho);
|
||
}
|
||
v.push(add(p[i], n[i]));
|
||
}
|
||
v.push(add(p[m - 1], n[m - 2]));
|
||
// The end cap, from n to −n through the last direction.
|
||
arc(&mut v, p[m - 1], n[m - 2], None, rho);
|
||
// The right side, backward. Turning in the positive sense, it is the outer side.
|
||
v.push(sub(p[m - 1], n[m - 2]));
|
||
for i in (1..m - 1).rev() {
|
||
v.push(sub(p[i], n[i]));
|
||
if cross(d[i - 1], d[i]) > 0 {
|
||
arc(&mut v, p[i], neg(n[i]), Some(neg(n[i - 1])), rho);
|
||
}
|
||
v.push(sub(p[i], n[i - 1]));
|
||
}
|
||
v.push(sub(p[0], n[0]));
|
||
// The start cap, from −n to n through the reverse of the first direction.
|
||
arc(&mut v, p[0], neg(n[0]), None, rho);
|
||
}
|
||
let mut out: Vec<Point> = Vec::with_capacity(v.len());
|
||
for (x, y) in v {
|
||
let q = Point { x: x as i16, y: y as i16, tag: 0 };
|
||
if out.last() != Some(&q) {
|
||
out.push(q);
|
||
}
|
||
}
|
||
while out.len() > 1 && out.last() == out.first() {
|
||
out.pop();
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The notes of the set a source expands to: the line `parent_sha256=` with the
|
||
/// source's SHA-256 in lowercase hex, the line `converter=wsf1-expansion/1`, then the
|
||
/// source's notes, byte for byte.
|
||
pub fn expanded_notes(src: &[u8], notes: &[u8]) -> Vec<u8> {
|
||
let mut o = Vec::new();
|
||
o.extend_from_slice(b"parent_sha256=");
|
||
o.extend_from_slice(hex(&sha256(src)).as_bytes());
|
||
o.extend_from_slice(b"\nconverter=");
|
||
o.extend_from_slice(CONVERTER.as_bytes());
|
||
o.push(b'\n');
|
||
o.extend_from_slice(notes);
|
||
o
|
||
}
|
||
|
||
/// Expand a source into its `WOS1` (Annex A): one glyph record per source glyph, with
|
||
/// the same `gid`; one contour per stroke path; `upem2` copied; `flags` 0, since
|
||
/// overlaps are not removed; and the notes of [`expanded_notes`].
|
||
pub fn expand(src: &[u8]) -> Result<Wos1, SourceError> {
|
||
let s = parse(src)?;
|
||
let rho = i64::from(s.radius);
|
||
let glyphs: Vec<(u16, Vec<Vec<Point>>)> =
|
||
s.glyphs.iter().map(|g| (g.gid, g.strokes.iter().map(|st| stroke_contour(st, rho)).collect())).collect();
|
||
Ok(Wos1::from_outlines(s.upem2, 0, &glyphs, &expanded_notes(src, &s.notes)))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use alloc::vec;
|
||
|
||
#[test]
|
||
fn isqrt_is_the_floor() {
|
||
for n in 0u64..20_000 {
|
||
let r = isqrt(n);
|
||
assert!(r * r <= n && (r + 1) * (r + 1) > n, "{n}");
|
||
}
|
||
for n in [u64::from(u32::MAX), (1u64 << 33) - 1, 2 * 65_535 * 65_535] {
|
||
let r = isqrt(n);
|
||
assert!(r * r <= n && (r + 1) * (r + 1) > n, "{n}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_table_is_symmetric_and_unit() {
|
||
for k in 0..64 {
|
||
let (c, s) = UNIT[k];
|
||
assert_eq!(UNIT[(k + 16) % 64], (-s, c), "a quarter turn of {k}");
|
||
assert_eq!(UNIT[(64 - k) % 64], (c, -s), "the reflection of {k}");
|
||
let r2 = i64::from(c) * i64::from(c) + i64::from(s) * i64::from(s);
|
||
assert!((r2 - (1 << 30)).abs() < 1 << 17, "{k}: |u|² = {r2}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_horizontal_stroke_is_a_stadium() {
|
||
let c = stroke_contour(&[(0, 0), (1000, 0)], 100);
|
||
// Left offsets (y + 100: n = (0, 100)), the cap round +x, the right offsets,
|
||
// the cap round −x.
|
||
assert_eq!(c[0], Point { x: 0, y: 100, tag: 0 });
|
||
assert_eq!(c[1], Point { x: 1000, y: 100, tag: 0 });
|
||
assert!(c.iter().all(|p| p.x >= -100 && p.x <= 1100 && p.y >= -100 && p.y <= 100));
|
||
assert!(c.contains(&Point { x: 1100, y: 0, tag: 0 }) && c.contains(&Point { x: -100, y: 0, tag: 0 }));
|
||
// Each cap holds the 31 directions strictly inside its half-plane.
|
||
assert_eq!(c.len(), 4 + 31 + 31);
|
||
}
|
||
|
||
#[test]
|
||
fn a_dot_is_a_polygon_of_the_table() {
|
||
let c = stroke_contour(&[(500, -500)], 80);
|
||
assert_eq!(c.len(), 64);
|
||
assert_eq!(c[0], Point { x: 580, y: -500, tag: 0 });
|
||
assert_eq!(c[16], Point { x: 500, y: -580, tag: 0 }, "the negative sense: up after right");
|
||
}
|
||
|
||
#[test]
|
||
fn round_trip() {
|
||
let s = Wsf1 {
|
||
upem2: 2000,
|
||
radius: 80,
|
||
glyphs: vec![
|
||
Glyph { gid: 32, advance: 600, strokes: vec![] },
|
||
Glyph { gid: 65, advance: 1200, strokes: vec![vec![(0, 0), (500, -1400), (1000, 0)], vec![(300, -500)]] },
|
||
],
|
||
notes: b"licence=Apache-2.0\n\ntext".to_vec(),
|
||
};
|
||
let b = s.write();
|
||
assert_eq!(parse(&b), Ok(s));
|
||
let w = expand(&b).unwrap();
|
||
assert_eq!(crate::wos1::parse(&w.write()).unwrap(), w);
|
||
}
|
||
}
|