From 6ba971530bec06f2aa335340a8b5d5e52306ecc4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 17:00:55 -0400 Subject: [PATCH 1/6] wai-vector: WIP: WSF1 and its expansion, the ASCII stroke set, the test repertoire, the text vectors (not yet wired) Co-Authored-By: Claude Opus 5.5 --- wai/wai-vector/src/vectors/repertoire.rs | 472 ++++++++++++++++++++ wai/wai-vector/src/vectors/stroke.rs | 450 +++++++++++++++++++ wai/wai-vector/src/vectors/text.rs | 322 ++++++++++++++ wai/wai-vector/src/wsf1.rs | 543 +++++++++++++++++++++++ 4 files changed, 1787 insertions(+) create mode 100644 wai/wai-vector/src/vectors/repertoire.rs create mode 100644 wai/wai-vector/src/vectors/stroke.rs create mode 100644 wai/wai-vector/src/vectors/text.rs create mode 100644 wai/wai-vector/src/wsf1.rs diff --git a/wai/wai-vector/src/vectors/repertoire.rs b/wai/wai-vector/src/vectors/repertoire.rs new file mode 100644 index 000000000..bfc04efcf --- /dev/null +++ b/wai/wai-vector/src/vectors/repertoire.rs @@ -0,0 +1,472 @@ +//! The test repertoire (`graphics-vector.md` §4): Arabic, Hebrew and Devanagari glyphs +//! drawn here as integer centre lines, Apache-2.0, for the corpus only. It is never +//! bundled with a sink and never a default for authors. It holds only what the test +//! words need, and a small shaper for them: a joining table that picks Arabic +//! contextual forms by Unicode joining type, the lam-alef ligature, Devanagari's +//! reordered i-matra and one conjunct, and authored mark positions. +//! +//! **It is not a real shaper on a real font.** It lets the corpus pin right-to-left +//! visual order, contextual forms, a conjunct, a reordered matra, marks and clusters, +//! with no third-party font. The fetched-font job tests the real authoring path. +//! +//! The test words: Arabic بَيْت and سلام (initial, medial, final and isolated forms, +//! lam-alef, fatha and sukun); Hebrew שלום (with the final mem); Devanagari हिंदी and +//! हिन्दी (the i-matra before its consonant, the ii-matra, the anusvara above, and the +//! conjunct न्द); and U+1F600, drawn as one monochrome glyph. +//! +//! Glyph IDs are the code points the glyphs draw where those fit 16 bits; the Arabic +//! contextual forms take the code points of their presentation forms; the conjunct +//! न्द is U+E000 and the U+1F600 face U+E001, in the private use area. +//! +//! Joins overlap the way real fonts' joins do: each joining form's connector runs to +//! the edge of its advance, so its round cap passes the edge by the pen's radius and +//! overlaps the neighbour's connector; each Devanagari headline does the same. Inside +//! one run those overlaps over-cover their partly covered edge pixels (the knot of +//! §3.13). + +use alloc::vec; +use alloc::vec::Vec; + +use super::stroke::{Pen, RADIUS, UPEM2, glyph, source_notes}; +use crate::wsf1::{self, Glyph, Wsf1}; + +// Arabic contextual forms: (isolated, final, initial, medial). +pub const BEH: [u16; 4] = [0xFE8F, 0xFE90, 0xFE91, 0xFE92]; +pub const TEH: [u16; 4] = [0xFE95, 0xFE96, 0xFE97, 0xFE98]; +pub const SEEN: [u16; 4] = [0xFEB1, 0xFEB2, 0xFEB3, 0xFEB4]; +pub const LAM: [u16; 4] = [0xFEDD, 0xFEDE, 0xFEDF, 0xFEE0]; +pub const MEEM: [u16; 4] = [0xFEE1, 0xFEE2, 0xFEE3, 0xFEE4]; +pub const YEH: [u16; 4] = [0xFEF1, 0xFEF2, 0xFEF3, 0xFEF4]; +pub const ALEF: [u16; 2] = [0xFE8D, 0xFE8E]; +pub const LAM_ALEF: [u16; 2] = [0xFEFB, 0xFEFC]; +pub const FATHA: u16 = 0x064E; +pub const SUKUN: u16 = 0x0652; +pub const CONJUNCT_NDA: u16 = 0xE000; +pub const FACE: u16 = 0xE001; +pub const SPACE: u16 = 0x0020; + +/// A letter's Unicode joining type, as far as the test words need it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Joining { + /// Dual-joining: joins on both sides. + Dual, + /// Right-joining: joins only to the letter before it. + Right, +} + +/// The joining table: each letter of the test words, its joining type, and its forms +/// (isolated, final, initial, medial; a right-joining letter has no initial or medial +/// form, and repeats its isolated and final ones). +pub const JOINING: [(char, Joining, [u16; 4]); 7] = [ + ('\u{0627}', Joining::Right, [ALEF[0], ALEF[1], ALEF[0], ALEF[1]]), + ('\u{0628}', Joining::Dual, BEH), + ('\u{062A}', Joining::Dual, TEH), + ('\u{0633}', Joining::Dual, SEEN), + ('\u{0644}', Joining::Dual, LAM), + ('\u{0645}', Joining::Dual, MEEM), + ('\u{064A}', Joining::Dual, YEH), +]; + +const HL: i32 = -1300; +const J: i32 = 0; + +/// Where a mark above or below a base sits, in the base's outline units: the x of its +/// main stroke. +pub fn anchor(base: u16) -> i32 { + match base { + 0xFE91 | 0xFE97 | 0xFEF3 => 360, + 0xFE92 | 0xFE98 | 0xFEF4 => 260, + 0xFE8F | 0xFE90 | 0xFE95 | 0xFE96 => 480, + 0x0939 => 560, + 0x0926 => 480, + _ => 300, + } +} + +fn dots(p: &mut Pen, x: i32, y: i32, n: i32) { + if n == 1 { + p.dot(x, y); + } else { + p.dot(x - 120, y).dot(x + 120, y); + } +} + +/// The beh family's forms (beh, teh, yeh share the tooth), with `n` dots above (`up`) +/// or below. +fn beh_like(forms: [u16; 4], n: i32, up: bool, yeh: bool) -> Vec { + let dy = |base: i32| if up { -620 } else { base }; + let mut v = Vec::new(); + // Isolated and final: the bowl (beh, teh) or the swooping tail (yeh). + if yeh { + v.push(glyph(forms[0], 960, |p| { + let (x, y) = Pen::at(640, -380, 160, 160, 60); + p.m(x, y).arc(640, -380, 160, 160, 60, 24).arc(460, -20, 380, 260, -8, 36); + dots(p, 460, 420, n); + })); + v.push(glyph(forms[1], 960, |p| { + p.m(960, J).arc(460, -20, 380, 260, 0, 36); + dots(p, 460, 420, n); + })); + } else { + v.push(glyph(forms[0], 960, |p| { + let (x, y) = Pen::at(480, -300, 340, 300, 0); + p.m(x, y).arc(480, -300, 340, 300, 0, 32); + dots(p, 480, dy(300), n); + })); + v.push(glyph(forms[1], 960, |p| { + p.m(960, J).l(480, J).arc(480, -300, 340, 300, 16, 32); + dots(p, 480, dy(300), n); + })); + } + // Initial and medial: the tooth. + let below = if yeh { 300 } else { 300 }; + v.push(glyph(forms[2], 520, |p| { + p.m(360, -440).l(360, J).l(0, J); + dots(p, 360, dy(below), n); + })); + v.push(glyph(forms[3], 520, |p| { + p.m(520, J).l(0, J).m(260, -440).l(260, J); + dots(p, 260, dy(below), n); + })); + v +} + +/// Every glyph of the test repertoire, in glyph-ID order. +pub fn repertoire_glyphs() -> Vec { + let mut v: Vec = Vec::new(); + v.push(glyph(SPACE, 600, |_| {})); + // Hebrew. + v.push(glyph(0x05D5, 600, |p| { + p.m(160, -1100).l(380, -1100).l(380, 0); + })); + v.push(glyph(0x05DC, 1000, |p| { + p.m(200, -1550).l(200, -1050).l(820, -1050).l(820, -600).l(560, -200); + })); + v.push(glyph(0x05DD, 1060, |p| { + p.m(160, -1100).l(900, -1100).l(900, 0).l(160, 0).l(160, -1100); + })); + v.push(glyph(0x05E9, 1260, |p| { + p.m(160, -1100).l(300, 0).l(1000, 0).l(1100, -1100).m(640, -1100).l(600, 0); + })); + // Arabic marks, zero advance, drawn about x = 0. + v.push(glyph(FATHA, 0, |p| { + p.m(-120, -620).l(120, -760); + })); + v.push(glyph(SUKUN, 0, |p| { + p.ell(0, -720, 130, 120); + })); + // Devanagari. + v.push(glyph(0x0902, 0, |p| { + p.dot(0, HL - 280); + })); + v.push(glyph(0x0926, 940, |p| { + p.m(0, HL).l(940, HL).m(400, HL).l(400, -880).arc(520, -560, 300, 320, 44, 80).l(280, -40); + })); + v.push(glyph(0x0939, 1000, |p| { + p.m(0, HL).l(1000, HL).m(380, HL).l(380, -980).l(640, -980).arc(640, -820, 180, 160, 48, 80).l(360, -660).l(740, -200).arc(620, -200, 120, 140, 0, 30); + })); + v.push(glyph(0x093F, 400, |p| { + p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(560, HL, 360, 300, 32, 60); + })); + v.push(glyph(0x0940, 400, |p| { + p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(-160, HL, 360, 300, 64, 36); + })); + v.push(glyph(CONJUNCT_NDA, 1300, |p| { + p.m(0, HL).l(1300, HL).m(320, HL).l(320, -760).l(120, -760); + p.m(760, HL).l(760, -880).arc(880, -560, 300, 320, 44, 80).l(640, -40); + })); + // The U+1F600 face: one monochrome outline glyph. + v.push(glyph(FACE, 1640, |p| { + p.ell(820, -600, 640, 640).dot(600, -820).dot(1040, -820); + let (x, y) = Pen::at(820, -600, 400, 340, 4); + p.m(x, y).arc(820, -600, 400, 340, 4, 28); + })); + // Arabic letters. + v.push(glyph(ALEF[0], 480, |p| { + p.m(240, -1500).l(240, J); + })); + v.push(glyph(ALEF[1], 480, |p| { + p.m(240, -1500).l(240, J).l(480, J); + })); + v.extend(beh_like(BEH, 1, false, false)); + v.extend(beh_like(TEH, 2, true, false)); + // Seen: three teeth, and the bowl in the isolated and final forms. + v.push(glyph(SEEN[0], 1400, |p| { + p.m(1240, -360).l(1240, J).l(760, J).arc(400, -60, 320, 300, 0, 36); + p.m(1000, -360).l(1000, J); + p.m(760, -360).l(760, J); + })); + v.push(glyph(SEEN[1], 1500, |p| { + p.m(1500, J).l(760, J).arc(400, -60, 320, 300, 0, 36); + p.m(1300, -360).l(1300, J); + p.m(1060, -360).l(1060, J); + p.m(820, -360).l(820, J); + })); + v.push(glyph(SEEN[2], 900, |p| { + p.m(740, -360).l(740, J).l(0, J); + p.m(500, -360).l(500, J); + p.m(260, -360).l(260, J); + })); + v.push(glyph(SEEN[3], 900, |p| { + p.m(900, J).l(0, J); + p.m(680, -360).l(680, J); + p.m(450, -360).l(450, J); + p.m(220, -360).l(220, J); + })); + // Lam. + v.push(glyph(LAM[0], 900, |p| { + p.m(760, -1500).l(760, -200).arc(440, -200, 320, 260, 0, 36); + })); + v.push(glyph(LAM[1], 920, |p| { + p.m(760, -1500).l(760, -200).arc(440, -200, 320, 260, 0, 36); + p.m(920, J).l(645, J); + })); + v.push(glyph(LAM[2], 520, |p| { + p.m(360, -1500).l(360, J).l(0, J); + })); + v.push(glyph(LAM[3], 520, |p| { + p.m(520, J).l(0, J).m(260, -1500).l(260, J); + })); + // Meem. + v.push(glyph(MEEM[0], 760, |p| { + p.ell(480, -170, 190, 170).m(290, -170).l(270, 480); + })); + v.push(glyph(MEEM[1], 860, |p| { + p.m(860, J).l(480, J).ell(480, -170, 190, 170).m(290, -170).l(270, 480); + })); + v.push(glyph(MEEM[2], 700, |p| { + p.ell(480, -170, 190, 170).m(480, J).l(0, J); + })); + v.push(glyph(MEEM[3], 760, |p| { + p.m(760, J).l(0, J).ell(420, -170, 180, 170); + })); + v.extend(beh_like(YEH, 2, false, true)); + // Lam-alef, isolated and final. + v.push(glyph(LAM_ALEF[0], 1000, |p| { + p.m(820, -1500).l(820, -200).arc(560, -200, 260, 240, 0, 16).l(200, -1300); + })); + v.push(glyph(LAM_ALEF[1], 1100, |p| { + p.m(820, -1500).l(820, -200).arc(560, -200, 260, 240, 0, 16).l(200, -1300); + p.m(1100, J).l(560, 40); + })); + v.sort_by_key(|g| g.gid); + v +} + +/// The test repertoire's source. +pub fn repertoire_source() -> Wsf1 { + Wsf1 { + upem2: UPEM2, + radius: RADIUS, + glyphs: repertoire_glyphs(), + notes: source_notes( + "drawn in this repository as integer centre lines (wai-vector/src/vectors/repertoire.rs) for the conformance corpus only; no outline is traced from any font; a test repertoire, never a default for authors", + "the code points each glyph draws where they fit 16 bits; Arabic contextual forms at their presentation-form code points; U+E000 the conjunct na-da; U+E001 the U+1F600 face", + "1550,-480,1300,1000", + ), + } +} + +pub fn repertoire_source_bytes() -> Vec { + repertoire_source().write() +} + +pub fn repertoire_set_bytes() -> Vec { + wsf1::expand(&repertoire_source_bytes()).expect("the test repertoire's source passes Annex A's checks").write() +} + +// ------------------------------------------------------------------ shaping + +/// One shaped glyph in logical order: its ID, the byte range of its cluster in the +/// string, and, for a mark, the index of its base among the shaped glyphs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Shaped { + pub gid: u16, + /// The cluster this glyph belongs to: an index into the clusters, in logical order. + pub cluster: usize, + pub base: Option, +} + +/// The clusters of a shaped string, in logical order: `(byte_first, byte_len)`. +pub type Clusters = Vec<(u32, u32)>; + +fn joining(c: char) -> Option<(Joining, [u16; 4])> { + JOINING.iter().find(|(l, _, _)| *l == c).map(|&(_, j, f)| (j, f)) +} + +fn is_arabic_mark(c: char) -> bool { + matches!(c, '\u{064E}' | '\u{0652}') +} + +/// Shape an Arabic string of the test words: contextual forms by joining type, the +/// lam-alef ligature, and each mark in its base's cluster. A character outside the +/// table (a space) breaks joining and maps to its own glyph. +pub fn shape_arabic(s: &str) -> (Vec, Clusters) { + let chars: Vec<(usize, char)> = s.char_indices().collect(); + let bases: Vec = (0..chars.len()).filter(|&i| !is_arabic_mark(chars[i].1)).collect(); + let mut out: Vec = Vec::new(); + let mut clusters: Clusters = Vec::new(); + let mut b = 0usize; + while b < bases.len() { + let i = bases[b]; + let c = chars[i].1; + let prev = b.checked_sub(1).and_then(|p| joining(chars[bases[p]].1)); + let next = bases.get(b + 1).and_then(|&n| joining(chars[n].1)); + let me = joining(c); + let joins_prev = matches!((prev, me), (Some((Joining::Dual, _)), Some(_))); + // Lam followed by alef is one ligature glyph, joined to the letter before it. + let lam_alef = c == '\u{0644}' && bases.get(b + 1).is_some_and(|&n| chars[n].1 == '\u{0627}'); + let (gid, consumed) = if lam_alef { + (if joins_prev { LAM_ALEF[1] } else { LAM_ALEF[0] }, 2) + } else if let Some((j, forms)) = me { + let joins_next = j == Joining::Dual && next.is_some(); + (forms[usize::from(joins_prev) + 2 * usize::from(joins_next)], 1) + } else { + (c as u16, 1) + }; + // The cluster runs from this base to the next base not consumed. + let start = chars[i].0; + let end = bases.get(b + consumed).map_or(s.len(), |&n| chars[n].0); + let k = clusters.len(); + clusters.push((start as u32, (end - start) as u32)); + let base_index = out.len(); + out.push(Shaped { gid, cluster: k, base: None }); + for &(_, m) in chars.iter().filter(|(o, m)| *o > start && *o < end && is_arabic_mark(*m)) { + out.push(Shaped { gid: m as u16, cluster: k, base: Some(base_index) }); + } + b += consumed; + } + (out, clusters) +} + +/// A Hebrew string: one glyph per character, one cluster each. +pub fn shape_hebrew(s: &str) -> (Vec, Clusters) { + let mut out = Vec::new(); + let mut clusters = Vec::new(); + for (o, c) in s.char_indices() { + out.push(Shaped { gid: c as u16, cluster: clusters.len(), base: None }); + clusters.push((o as u32, c.len_utf8() as u32)); + } + (out, clusters) +} + +/// A Devanagari string of the test words, by syllable: a consonant, or na + virama + +/// da as the conjunct; then the i-matra, drawn before the consonant; the ii-matra after +/// it; and the anusvara above it. Each syllable is one cluster. +pub fn shape_devanagari(s: &str) -> (Vec, Clusters) { + let chars: Vec<(usize, char)> = s.char_indices().collect(); + let mut out = Vec::new(); + let mut clusters: Clusters = Vec::new(); + let mut i = 0usize; + while i < chars.len() { + let start = chars[i].0; + let k = clusters.len(); + if chars[i].1 == ' ' { + out.push(Shaped { gid: SPACE, cluster: k, base: None }); + clusters.push((start as u32, 1)); + i += 1; + continue; + } + // The base: a consonant, or the conjunct. + let base_gid = if chars[i].1 == '\u{0928}' && chars.get(i + 1).map(|c| c.1) == Some('\u{094D}') && chars.get(i + 2).map(|c| c.1) == Some('\u{0926}') { + i += 3; + CONJUNCT_NDA + } else { + i += 1; + chars[i - 1].1 as u16 + }; + let mut pre = None; + let mut post = Vec::new(); + while let Some(&(_, c)) = chars.get(i) { + match c { + '\u{093F}' => pre = Some(c as u16), + '\u{0940}' | '\u{0902}' => post.push(c as u16), + _ => break, + } + i += 1; + } + let end = chars.get(i).map_or(s.len(), |c| c.0); + clusters.push((start as u32, (end - start) as u32)); + if let Some(m) = pre { + out.push(Shaped { gid: m, cluster: k, base: None }); + } + let base_index = out.len(); + out.push(Shaped { gid: base_gid, cluster: k, base: None }); + // The ii-matra advances; the anusvara is a mark on the base. + for m in post.iter().copied().filter(|&m| m == 0x0940) { + out.push(Shaped { gid: m, cluster: k, base: None }); + } + for m in post.iter().copied().filter(|&m| m == 0x0902) { + out.push(Shaped { gid: m, cluster: k, base: Some(base_index) }); + } + } + (out, clusters) +} + +/// The glyphs of a run in visual order, `(gid, x, y)` in outline units from the run's +/// origin, and its clusters in glyph order `(glyph_count, byte_first, byte_len)`, from +/// shaped glyphs in logical order. A right-to-left run reverses the clusters; within a +/// cluster a mark keeps its base's position plus its anchor, and the other glyphs +/// advance. +pub fn lay_out(set: &Wsf1, shaped: &[Shaped], clusters: &Clusters, rtl: bool, byte_offset: u32) -> (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>) { + let mut order: Vec = (0..clusters.len()).collect(); + if rtl { + order.reverse(); + } + let adv = |g: u16| i32::from(set.glyph(g).map_or(0, |g| g.advance)); + let mut glyphs = Vec::new(); + let mut runs = Vec::new(); + let mut pen = 0i32; + for k in order { + let members: Vec = (0..shaped.len()).filter(|&i| shaped[i].cluster == k).collect(); + // Non-marks first, in logical order (an RTL cluster's bases reversed), then marks. + let mut bases: Vec = members.iter().copied().filter(|&i| shaped[i].base.is_none()).collect(); + if rtl { + bases.reverse(); + } + let mut at: Vec<(usize, i32)> = Vec::new(); + for &i in &bases { + at.push((i, pen)); + glyphs.push((shaped[i].gid, pen, 0)); + pen += adv(shaped[i].gid); + } + for &i in members.iter().filter(|&&i| shaped[i].base.is_some()) { + let b = shaped[i].base.unwrap_or(i); + let bx = at.iter().find(|(j, _)| *j == b).map_or(pen, |&(_, x)| x); + let ax = anchor(shaped[b].gid); + glyphs.push((shaped[i].gid, bx + ax, 0)); + } + runs.push((members.len() as u16, clusters[k].0 + byte_offset, clusters[k].1)); + } + (glyphs, runs) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_joining_table_picks_the_forms_of_the_test_words() { + let gids = |s: &str| shape_arabic(s).0.iter().map(|g| g.gid).collect::>(); + // بَيْت: beh initial with fatha, yeh medial with sukun, teh final. + assert_eq!(gids("\u{0628}\u{064E}\u{064A}\u{0652}\u{062A}"), vec![BEH[2], FATHA, YEH[3], SUKUN, TEH[1]]); + // سلام: seen initial, lam-alef final, meem isolated (alef joins nothing after it). + assert_eq!(gids("\u{0633}\u{0644}\u{0627}\u{0645}"), vec![SEEN[2], LAM_ALEF[1], MEEM[0]]); + // A lam-alef that begins a word is isolated, and a lone beh is isolated. + assert_eq!(gids("\u{0644}\u{0627} \u{0628}"), vec![LAM_ALEF[0], SPACE, BEH[0]]); + // Medial and final forms in a longer join: beh beh beh. + assert_eq!(gids("\u{0628}\u{0628}\u{0628}"), vec![BEH[2], BEH[3], BEH[1]]); + } + + #[test] + fn clusters_follow_the_syllables() { + let (g, c) = shape_devanagari("\u{0939}\u{093F}\u{0902}\u{0926}\u{0940}"); + assert_eq!(g.iter().map(|g| g.gid).collect::>(), vec![0x093F, 0x0939, 0x0902, 0x0926, 0x0940]); + assert_eq!(c, vec![(0, 9), (9, 6)]); + let (g, c) = shape_devanagari("\u{0939}\u{093F}\u{0928}\u{094D}\u{0926}\u{0940}"); + assert_eq!(g.iter().map(|g| g.gid).collect::>(), vec![0x093F, 0x0939, CONJUNCT_NDA, 0x0940]); + assert_eq!(c, vec![(0, 6), (6, 12)]); + let (_, c) = shape_arabic("\u{0633}\u{0644}\u{0627}\u{0645}"); + assert_eq!(c, vec![(0, 2), (2, 4), (6, 2)]); + } +} diff --git a/wai/wai-vector/src/vectors/stroke.rs b/wai/wai-vector/src/vectors/stroke.rs new file mode 100644 index 000000000..2b779e91d --- /dev/null +++ b/wai/wai-vector/src/vectors/stroke.rs @@ -0,0 +1,450 @@ +//! The default stroke set (`graphics-vector.md` §4, Annexes A and B): printable ASCII, +//! U+0020–U+007E, drawn here as integer centre lines and expanded into a `WOS1` by +//! Annex A. Apache-2.0, like the rest of this repository. +//! +//! **The drawing source.** Every glyph below is drawn here from original centre lines: +//! straight segments, and arcs whose points are ellipse points taken from Annex A's +//! pinned table of unit vectors, so the drawing is integers throughout. No outline is +//! traced from any font. The set's notes say so, and carry the licence text. +//! +//! Outline units: an em is 2,000 (so `upem2` = 2000); the baseline is y = 0 and y points +//! down; caps are 1,400 tall, the x-height 1,000, ascenders 1,480 and descenders 440. The +//! pen's radius is 80, so stems are 160 wide. A glyph's ID is its code point, and its +//! advance is in the source for the authoring tool's layout. + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use crate::arith::r_half_up; +use crate::json::hex; +use crate::sha256::sha256; +use crate::wsf1::{self, Glyph, UNIT, Wsf1}; + +/// The pen radius of the in-house sets, in outline units. +pub const RADIUS: u16 = 80; +/// Twice the units per em. +pub const UPEM2: u16 = 2000; + +/// The licence text the in-house sets carry in their notes: the Apache License, +/// Version 2.0, as the repository states it. +pub const LICENCE: &str = include_str!("../../../../LICENSE"); + +/// A centre-line builder: `m` starts a stroke path, `l` draws to a point, `arc` draws +/// through ellipse points from Annex A's table, and `dot` is a one-point stroke. +#[derive(Default)] +pub struct Pen { + pub strokes: Vec>, +} + +impl Pen { + pub fn m(&mut self, x: i32, y: i32) -> &mut Pen { + self.strokes.push(vec![(x as i16, y as i16)]); + self + } + /// A point, unless it equals the last one. + pub fn l(&mut self, x: i32, y: i32) -> &mut Pen { + let p = (x as i16, y as i16); + match self.strokes.last_mut() { + Some(s) if s.last() == Some(&p) => {} + Some(s) => s.push(p), + None => self.strokes.push(vec![p]), + } + self + } + /// The ellipse point of table direction `k` (any integer, taken mod 64). + pub fn at(cx: i32, cy: i32, rx: i32, ry: i32, k: i32) -> (i32, i32) { + let u = UNIT[k.rem_euclid(64) as usize]; + (cx + r_half_up(i64::from(rx) * i64::from(u.0), 15) as i32, cy + r_half_up(i64::from(ry) * i64::from(u.1), 15) as i32) + } + /// Ellipse points from direction `k0` to `k1` in steps of `step` table directions, + /// `k1` always included; drawn on from the current point. + pub fn arc_s(&mut self, cx: i32, cy: i32, rx: i32, ry: i32, k0: i32, k1: i32, step: i32) -> &mut Pen { + let s = if k1 >= k0 { step } else { -step }; + let mut k = k0; + loop { + let (x, y) = Pen::at(cx, cy, rx, ry, k); + self.l(x, y); + if k == k1 { + break; + } + k = if (k1 - k).abs() <= step { k1 } else { k + s }; + } + self + } + pub fn arc(&mut self, cx: i32, cy: i32, rx: i32, ry: i32, k0: i32, k1: i32) -> &mut Pen { + self.arc_s(cx, cy, rx, ry, k0, k1, 2) + } + /// A closed ellipse, as its own stroke path. + pub fn ell(&mut self, cx: i32, cy: i32, rx: i32, ry: i32) -> &mut Pen { + let (x, y) = Pen::at(cx, cy, rx, ry, 0); + self.m(x, y).arc(cx, cy, rx, ry, 0, 64) + } + pub fn dot(&mut self, x: i32, y: i32) -> &mut Pen { + self.m(x, y) + } +} + +/// A glyph from its ID, its advance and a drawing. +pub fn glyph(gid: u16, advance: u16, draw: impl FnOnce(&mut Pen)) -> Glyph { + let mut p = Pen::default(); + draw(&mut p); + Glyph { gid, advance, strokes: p.strokes } +} + +/// The notes of an in-house source: its key lines, a blank line, then the licence text. +pub fn source_notes(source: &str, glyphs: &str, metrics: &str) -> Vec { + let mut s = String::from("licence=Apache-2.0\nlicence_sha256="); + s.push_str(&hex(&sha256(LICENCE.as_bytes()))); + s.push_str("\ncopyright=Copyright (c) 2026 Transaction Science, Inc.\nrfn=none\nsource="); + s.push_str(source); + s.push_str("\nglyphs="); + s.push_str(glyphs); + s.push_str("\nmetrics="); + s.push_str(metrics); + s.push_str("\n\n"); + s.push_str(LICENCE); + s.into_bytes() +} + +// Vertical metrics, outline units, y down. +const CAP: i32 = -1400; +const XH: i32 = -1000; +const ASC: i32 = -1480; +const DSC: i32 = 440; +/// A dot's centre sits one radius above the baseline, so its ink rests on it. +const DOT: i32 = -80; + +/// Every glyph of the stroke set, in code-point order. +pub fn ascii_glyphs() -> Vec { + let mut v: Vec = Vec::new(); + let mut g = |c: char, adv: u16, draw: &dyn Fn(&mut Pen)| v.push(glyph(c as u16, adv, draw)); + g(' ', 600, &|_| {}); + g('!', 520, &|p| { + p.m(260, CAP).l(260, -440).dot(260, DOT); + }); + g('"', 760, &|p| { + p.m(240, CAP).l(240, -1000).m(520, CAP).l(520, -1000); + }); + g('#', 1240, &|p| { + p.m(460, -1300).l(380, -100).m(880, -1300).l(800, -100).m(180, -900).l(1080, -900).m(160, -500).l(1060, -500); + }); + g('$', 1150, &|p| { + p.m(Pen::at(575, -1000, 380, 300, 56).0, Pen::at(575, -1000, 380, 300, 56).1).arc(575, -1000, 380, 300, 56, 16).arc(575, -400, 400, 300, 48, 88); + p.m(575, -1520).l(575, 120); + }); + g('%', 1500, &|p| { + p.ell(380, -1080, 220, 300).ell(1120, -320, 220, 300).m(1240, CAP).l(260, 0); + }); + g('&', 1300, &|p| { + p.m(1180, 0).l(470, -905).arc(640, -1110, 240, 290, 24, 72).arc(560, -330, 300, 330, 40, 8).l(1100, -560); + }); + g('\'', 440, &|p| { + p.m(220, CAP).l(220, -1000); + }); + g('(', 640, &|p| { + let (x, y) = Pen::at(840, -575, 580, 1000, 42); + p.m(x, y).arc(840, -575, 580, 1000, 42, 22); + }); + g(')', 640, &|p| { + let (x, y) = Pen::at(-200, -575, 580, 1000, 54); + p.m(x, y).arc(-200, -575, 580, 1000, 54, 74); + }); + g('*', 900, &|p| { + p.m(450, CAP).l(450, -800).m(190, -1250).l(710, -950).m(190, -950).l(710, -1250); + }); + g('+', 1100, &|p| { + p.m(550, -1000).l(550, -200).m(150, -600).l(950, -600); + }); + g(',', 440, &|p| { + p.m(260, -60).l(170, 240); + }); + g('-', 760, &|p| { + p.m(160, -560).l(600, -560); + }); + g('.', 440, &|p| { + p.dot(220, DOT); + }); + g('/', 800, &|p| { + p.m(140, 100).l(660, -1450); + }); + g('0', 1120, &|p| { + p.ell(560, -700, 420, 700); + }); + g('1', 1120, &|p| { + p.m(320, -1180).l(600, CAP).l(600, 0); + }); + g('2', 1120, &|p| { + let (x, y) = Pen::at(560, -1030, 400, 370, 38); + p.m(x, y).arc(560, -1030, 400, 370, 38, 70).l(160, 0).l(980, 0); + }); + g('3', 1120, &|p| { + let (x, y) = Pen::at(550, -1060, 380, 340, 40); + p.m(x, y).arc(550, -1060, 380, 340, 40, 80).arc(550, -360, 410, 360, 48, 88); + }); + g('4', 1120, &|p| { + p.m(820, 0).l(820, CAP).l(140, -440).l(1000, -440); + }); + g('5', 1120, &|p| { + p.m(920, CAP).l(250, CAP).arc(560, -420, 400, 420, 40, 88); + }); + g('6', 1120, &|p| { + let (x, y) = Pen::at(600, -700, 440, 700, 54); + p.m(x, y).arc(600, -700, 440, 700, 54, 32).l(160, -450); + p.ell(560, -450, 400, 450); + }); + g('7', 1120, &|p| { + p.m(140, CAP).l(980, CAP).l(420, 0); + }); + g('8', 1120, &|p| { + p.ell(560, -1060, 340, 340).ell(560, -360, 400, 360); + }); + g('9', 1120, &|p| { + p.ell(560, -950, 400, 450); + p.m(960, -950).l(960, -700).arc(520, -700, 440, 700, 0, 22); + }); + g(':', 440, &|p| { + p.dot(220, DOT).dot(220, -920); + }); + g(';', 440, &|p| { + p.dot(220, -920).m(240, -60).l(150, 240); + }); + g('<', 1100, &|p| { + p.m(940, -1050).l(160, -600).l(940, -150); + }); + g('=', 1100, &|p| { + p.m(160, -800).l(940, -800).m(160, -400).l(940, -400); + }); + g('>', 1100, &|p| { + p.m(160, -1050).l(940, -600).l(160, -150); + }); + g('?', 1000, &|p| { + let (x, y) = Pen::at(500, -1080, 340, 320, 36); + p.m(x, y).arc(500, -1080, 340, 320, 36, 72).l(500, -620).l(500, -440).dot(500, DOT); + }); + g('@', 1700, &|p| { + p.ell(800, -560, 240, 300); + p.m(1040, -860).l(1040, -400).arc(1220, -400, 180, 200, 32, 0).arc(800, -560, 600, 760, 0, -54); + }); + g('A', 1280, &|p| { + p.m(140, 0).l(640, CAP).l(1140, 0).m(311, -480).l(969, -480); + }); + g('B', 1250, &|p| { + p.m(180, 0).l(180, CAP).l(680, CAP).arc(680, -1050, 350, 350, 48, 80).l(180, -700); + p.m(180, -700).l(720, -700).arc(720, -350, 370, 350, 48, 80).l(180, 0); + }); + g('C', 1300, &|p| { + let (x, y) = Pen::at(700, -700, 540, 700, 56); + p.m(x, y).arc(700, -700, 540, 700, 56, 8); + }); + g('D', 1300, &|p| { + p.m(180, 0).l(180, CAP).l(600, CAP).arc(600, -700, 520, 700, 48, 80).l(180, 0); + }); + g('E', 1150, &|p| { + p.m(1000, CAP).l(180, CAP).l(180, 0).l(1000, 0).m(180, -700).l(900, -700); + }); + g('F', 1100, &|p| { + p.m(1000, CAP).l(180, CAP).l(180, 0).m(180, -700).l(880, -700); + }); + g('G', 1350, &|p| { + let (x, y) = Pen::at(720, -700, 540, 700, 56); + p.m(x, y).arc(720, -700, 540, 700, 56, 0).l(780, -700); + }); + g('H', 1300, &|p| { + p.m(180, 0).l(180, CAP).m(1120, 0).l(1120, CAP).m(180, -700).l(1120, -700); + }); + g('I', 520, &|p| { + p.m(260, 0).l(260, CAP); + }); + g('J', 1000, &|p| { + p.m(820, CAP).l(820, -420).arc(470, -420, 350, 420, 0, 26); + }); + g('K', 1200, &|p| { + p.m(180, 0).l(180, CAP).m(1060, CAP).l(180, -520).m(470, -810).l(1080, 0); + }); + g('L', 1050, &|p| { + p.m(180, CAP).l(180, 0).l(980, 0); + }); + g('M', 1500, &|p| { + p.m(180, 0).l(180, CAP).l(750, -300).l(1320, CAP).l(1320, 0); + }); + g('N', 1300, &|p| { + p.m(180, 0).l(180, CAP).l(1120, 0).l(1120, CAP); + }); + g('O', 1420, &|p| { + p.ell(710, -700, 550, 700); + }); + g('P', 1200, &|p| { + p.m(180, 0).l(180, CAP).l(680, CAP).arc(680, -1030, 400, 370, 48, 80).l(180, -660); + }); + g('Q', 1420, &|p| { + p.ell(710, -700, 550, 700).m(860, -260).l(1220, 100); + }); + g('R', 1250, &|p| { + p.m(180, 0).l(180, CAP).l(680, CAP).arc(680, -1030, 400, 370, 48, 80).l(180, -660); + p.m(640, -660).l(1100, 0); + }); + g('S', 1150, &|p| { + let (x, y) = Pen::at(580, -1050, 420, 350, 56); + p.m(x, y).arc(580, -1050, 420, 350, 56, 16).arc(580, -350, 440, 350, 48, 88); + }); + g('T', 1150, &|p| { + p.m(100, CAP).l(1050, CAP).m(575, CAP).l(575, 0); + }); + g('U', 1300, &|p| { + p.m(180, CAP).l(180, -480).arc(650, -480, 470, 480, 32, 0).l(1120, CAP); + }); + g('V', 1250, &|p| { + p.m(120, CAP).l(625, 0).l(1130, CAP); + }); + g('W', 1700, &|p| { + p.m(110, CAP).l(450, 0).l(850, -1100).l(1250, 0).l(1590, CAP); + }); + g('X', 1200, &|p| { + p.m(140, CAP).l(1060, 0).m(1060, CAP).l(140, 0); + }); + g('Y', 1200, &|p| { + p.m(120, CAP).l(600, -700).l(1080, CAP).m(600, -700).l(600, 0); + }); + g('Z', 1150, &|p| { + p.m(160, CAP).l(1000, CAP).l(160, 0).l(1000, 0); + }); + g('[', 600, &|p| { + p.m(480, -1450).l(220, -1450).l(220, 250).l(480, 250); + }); + g('\\', 800, &|p| { + p.m(140, -1450).l(660, 100); + }); + g(']', 600, &|p| { + p.m(120, -1450).l(380, -1450).l(380, 250).l(120, 250); + }); + g('^', 1000, &|p| { + p.m(180, -800).l(500, CAP).l(820, -800); + }); + g('_', 1000, &|p| { + p.m(80, 200).l(920, 200); + }); + g('`', 600, &|p| { + p.m(200, -1420).l(400, -1150); + }); + g('a', 1080, &|p| { + p.ell(520, -500, 360, 500).m(880, XH).l(880, 0); + }); + g('b', 1120, &|p| { + p.m(200, ASC).l(200, 0).ell(580, -500, 380, 500); + }); + g('c', 1000, &|p| { + let (x, y) = Pen::at(560, -500, 400, 500, 56); + p.m(x, y).arc(560, -500, 400, 500, 56, 8); + }); + g('d', 1120, &|p| { + p.ell(540, -500, 380, 500).m(920, ASC).l(920, 0); + }); + g('e', 1080, &|p| { + p.m(160, -500).l(940, -500).arc(550, -500, 390, 500, 0, -56); + }); + g('f', 700, &|p| { + p.m(360, 0).l(360, -1180).arc(580, -1180, 220, 260, 32, 56).m(140, XH).l(640, XH); + }); + g('g', 1100, &|p| { + p.ell(520, -500, 360, 500).m(880, XH).l(880, 180).arc(520, 180, 360, 260, 0, 28); + }); + g('h', 1120, &|p| { + p.m(200, ASC).l(200, 0).m(200, -620).arc(560, -620, 360, 380, 32, 64).l(920, 0); + }); + g('i', 480, &|p| { + p.m(240, 0).l(240, XH).dot(240, -1300); + }); + g('j', 520, &|p| { + p.m(320, XH).l(320, 220).arc(100, 220, 220, 220, 0, 24).dot(320, -1300); + }); + g('k', 1000, &|p| { + p.m(200, ASC).l(200, 0).m(880, XH).l(200, -380).m(440, -599).l(900, 0); + }); + g('l', 480, &|p| { + p.m(240, ASC).l(240, 0); + }); + g('m', 1420, &|p| { + p.m(180, XH).l(180, 0); + p.m(180, -640).arc(445, -640, 265, 360, 32, 64).l(710, 0); + p.m(710, -640).arc(975, -640, 265, 360, 32, 64).l(1240, 0); + }); + g('n', 1120, &|p| { + p.m(200, XH).l(200, 0).m(200, -620).arc(560, -620, 360, 380, 32, 64).l(920, 0); + }); + g('o', 1120, &|p| { + p.ell(560, -500, 380, 500); + }); + g('p', 1120, &|p| { + p.m(200, XH).l(200, DSC).ell(580, -500, 380, 500); + }); + g('q', 1120, &|p| { + p.ell(540, -500, 380, 500).m(920, XH).l(920, DSC); + }); + g('r', 760, &|p| { + p.m(200, XH).l(200, 0).m(200, -600).arc(520, -600, 320, 400, 32, 56); + }); + g('s', 940, &|p| { + let (x, y) = Pen::at(470, -750, 330, 250, 56); + p.m(x, y).arc(470, -750, 330, 250, 56, 16).arc(470, -250, 350, 250, 48, 88); + }); + g('t', 720, &|p| { + p.m(340, -1300).l(340, -180).arc(540, -180, 200, 180, 32, 8).m(120, XH).l(640, XH); + }); + g('u', 1120, &|p| { + p.m(200, XH).l(200, -380).arc(560, -380, 360, 380, 32, 0).m(920, XH).l(920, 0); + }); + g('v', 1040, &|p| { + p.m(120, XH).l(520, 0).l(920, XH); + }); + g('w', 1500, &|p| { + p.m(100, XH).l(400, 0).l(750, -800).l(1100, 0).l(1400, XH); + }); + g('x', 1000, &|p| { + p.m(140, XH).l(860, 0).m(860, XH).l(140, 0); + }); + g('y', 1040, &|p| { + p.m(120, XH).l(531, 0).m(920, XH).l(360, DSC); + }); + g('z', 960, &|p| { + p.m(160, XH).l(800, XH).l(160, 0).l(800, 0); + }); + g('{', 700, &|p| { + p.m(560, -1450).l(400, -1400).l(380, -700).l(220, -575).l(380, -450).l(400, 250).l(560, 300); + }); + g('|', 480, &|p| { + p.m(240, -1500).l(240, 300); + }); + g('}', 700, &|p| { + p.m(140, -1450).l(300, -1400).l(320, -700).l(480, -575).l(320, -450).l(300, 250).l(140, 300); + }); + g('~', 1020, &|p| { + let (x, y) = Pen::at(330, -560, 180, 110, 32); + p.m(x, y).arc(330, -560, 180, 110, 32, 64).arc(690, -560, 180, 110, 32, 0); + }); + v +} + +/// The stroke set's source. +pub fn ascii_source() -> Wsf1 { + Wsf1 { + upem2: UPEM2, + radius: RADIUS, + glyphs: ascii_glyphs(), + notes: source_notes( + "drawn in this repository as integer centre lines (wai-vector/src/vectors/stroke.rs); no outline is traced from any font", + "32-126; each glyph ID is the code point it draws", + "1480,-440,1400,1000", + ), + } +} + +/// The stroke set's `WSF1` bytes. +pub fn ascii_source_bytes() -> Vec { + ascii_source().write() +} + +/// The stroke set: its source expanded by Annex A. +pub fn ascii_set_bytes() -> Vec { + wsf1::expand(&ascii_source_bytes()).expect("the stroke set's source passes Annex A's checks").write() +} diff --git a/wai/wai-vector/src/vectors/text.rs b/wai/wai-vector/src/vectors/text.rs new file mode 100644 index 000000000..7c1eb19e1 --- /dev/null +++ b/wai/wai-vector/src/vectors/text.rs @@ -0,0 +1,322 @@ +//! Vectors drawn from the in-house sets (`graphics-vector.md` §4): the `text` group, +//! and the stroke-glyph members of `runs` and `motion`. +//! +//! `text` renders text runs from the ASCII stroke set and the test repertoire at two +//! listed sizes each: Latin; Arabic and Hebrew right to left; Devanagari with a +//! reordered matra, a conjunct and a mark; a mixed-direction line whose runs come from +//! both sets; a block split over three runs and lines, with an inserted hyphen and a +//! zero-glyph cluster; a ruby annotation; a vertical-rl block; and the U+1F600 face. The +//! text layer's projection lands later; these vectors pin runs, clusters, blocks and +//! frames. +//! +//! Both sets are held, found by SHA-256 among `sets/`. Every string here is written +//! here. + +use alloc::format; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use super::Vector; +use super::canonical::{TB, held}; +use super::raster::{BLUE, ONE, RED, WHITE, frame_doc}; +use super::repertoire::{self, FACE, lay_out, repertoire_source, shape_arabic, shape_devanagari, shape_hebrew}; +use super::stroke::ascii_source; +use crate::arith::r_rat; +use crate::scene::*; +use crate::wsf1::Wsf1; + +/// Q16 design subpixels per outline unit: `n` subpixels per unit. +const fn us(n: u32) -> u32 { + n * 65_536 +} + +/// A glyph's outline-unit position to design subpixels, from an origin. +fn place(glyphs: &[(u16, i32, i32)], origin: (i32, i32), unit_scale: u32) -> Vec<(u16, i32, i32)> { + let s = i64::from(unit_scale); + glyphs.iter().map(|&(g, x, y)| (g, origin.0 + r_rat(i64::from(x) * s, 65_536) as i32, origin.1 + r_rat(i64::from(y) * s, 65_536) as i32)).collect() +} + +/// A Latin string in the stroke set: one glyph per byte, its ID the byte, advancing +/// left to right; one cluster per glyph from `byte_offset`. +fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>, i32) { + let mut pen = 0i32; + let mut g = Vec::new(); + let mut c = Vec::new(); + for (i, b) in s.bytes().enumerate() { + g.push((u16::from(b), pen, 0)); + c.push((1, byte_offset + i as u32, 1)); + pen += i32::from(set.glyph(u16::from(b)).expect("printable ASCII").advance); + } + (g, c, pen) +} + +/// A run item over the whole document. +#[allow(clippy::too_many_arguments)] +fn run(set: usize, text: u16, glyphs: Vec<(u16, i32, i32)>, clusters: Vec<(u16, u32, u32)>, unit_scale: u32, paint: [u8; 4], t: (u32, u32), slots: Slots) -> ItemDef { + ItemDef::Run { set, paint, text, t_on: t.0, t_off: t.1, unit_scale, slots, glyphs, clusters } +} + +fn block(purpose: u8, lang: &str, dir: u8, s: &str) -> TextDef { + let mut t = TextDef::new(purpose, lang, s); + t.dir = dir; + t +} + +/// A one-frame document at `(w, h)`, listed at that size and twice it. +fn doc2(w: u16, h: u16) -> SceneDef { + let mut s = frame_doc(w, h); + s.sizes.push((2 * w, 2 * h)); + s +} + +const PX: i32 = 256; + +pub fn vectors() -> Vec { + let latin_set = ascii_source(); + let rep = repertoire_source(); + let stroke = held(super::stroke::ascii_set_bytes()); + let tests = held(repertoire::repertoire_set_bytes()); + let mut v = Vec::new(); + let mut add = |g: &'static str, name: &str, s: &SceneDef, note: &str| v.push(Vector::wvg1(g, name, s.bytes(), note)); + + // ---- text/latin-ascii: the whole stroke set, one block over three runs. + { + let all: String = (0x20u8..=0x7E).map(char::from).collect(); + let mut s = doc2(320, 96); + s.sets = vec![stroke.clone()]; + s.texts = vec![block(7, "und", 0, &all)]; + let u = us(2); + for (k, (a, b)) in [(0usize, 32usize), (32, 64), (64, 95)].into_iter().enumerate() { + let (g, c, _) = latin(&latin_set, &all[a..b], a as u32); + s.items.push(run(0, 0, place(&g, (8 * PX, (22 + 28 * k as i32) * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)); + } + add("text", "latin-ascii", &s, "printable ASCII, U+0020-U+007E, from the held stroke set: one block drawn by three runs, one per line, one cluster per glyph: accepted"); + } + + // ---- text/arabic: right to left, contextual forms, lam-alef and two marks. + { + let word = "\u{0628}\u{064E}\u{064A}\u{0652}\u{062A} \u{0633}\u{0644}\u{0627}\u{0645}"; + let (sh, cl) = shape_arabic(word); + let (g, c) = lay_out(&rep, &sh, &cl, true, 0); + let mut s = doc2(160, 64); + s.sets = vec![tests.clone()]; + s.texts = vec![block(7, "ar", 1, word)]; + let u = us(5); + s.items = vec![run(0, 0, place(&g, (27 * PX + 100, 40 * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)]; + add("text", "arabic", &s, "Arabic, right to left, in the test repertoire: initial, medial, final and isolated forms chosen by joining type, the lam-alef ligature, fatha and sukun on their bases; glyphs in visual order and clusters with byte_first decreasing; joins whose connectors overlap: accepted"); + } + + // ---- text/hebrew. + { + let word = "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"; + let (sh, cl) = shape_hebrew(word); + let (g, c) = lay_out(&rep, &sh, &cl, true, 0); + let mut s = doc2(128, 48); + s.sets = vec![tests.clone()]; + s.texts = vec![block(7, "he", 1, word)]; + let u = us(4); + s.items = vec![run(0, 0, place(&g, (33 * PX, 34 * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)]; + add("text", "hebrew", &s, "Hebrew, right to left, in the test repertoire, with the final mem: accepted"); + } + + // ---- text/devanagari: the i-matra before its consonant, the anusvara above, the conjunct. + { + let words = "\u{0939}\u{093F}\u{0902}\u{0926}\u{0940} \u{0939}\u{093F}\u{0928}\u{094D}\u{0926}\u{0940}"; + let (sh, cl) = shape_devanagari(words); + let (g, c) = lay_out(&rep, &sh, &cl, false, 0); + let mut s = doc2(128, 48); + s.sets = vec![tests.clone()]; + s.texts = vec![block(7, "hi", 0, words)]; + let u = us(4); + s.items = vec![run(0, 0, place(&g, (12 * PX + 77, 38 * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)]; + add("text", "devanagari", &s, "Devanagari in the test repertoire, one word spelt with the anusvara and with the conjunct: the i-matra drawn before the consonant it follows in logical order, the anusvara above its base, the conjunct na-da as one glyph; one cluster per syllable; headlines that overlap: accepted"); + } + + // ---- text/mixed-direction: Latin and digits inside an Arabic block, from both sets. + { + let line = "\u{0633}\u{0644}\u{0627}\u{0645} WAI 2026"; + let u = us(4); + let (la, lc, lw) = latin(&latin_set, "WAI 2026", 9); + let (sh, cl) = shape_arabic(&line[..9]); + let (ag, ac) = lay_out(&rep, &sh, &cl, true, 0); + let mut s = doc2(224, 48); + s.sets = vec![stroke.clone(), tests.clone()]; + s.texts = vec![block(7, "ar", 1, line)]; + let x0 = 18 * PX; + let xa = x0 + r_rat(i64::from(lw) * i64::from(u), 65_536) as i32; + s.items = vec![ + run(0, 0, place(&la, (x0, 36 * PX), u), lc, u, WHITE, (0, ONE), NO_SLOTS), + run(1, 0, place(&ag, (xa, 36 * PX), u), ac, u, WHITE, (0, ONE), NO_SLOTS), + ]; + add("text", "mixed-direction", &s, "an Arabic block, right to left, holding a Latin word and digits: the left-to-right run from the stroke set, then the right-to-left run with its space from the test repertoire, both drawing parts of one block: accepted"); + } + + // ---- text/split-lines: one block over three runs, an inserted hyphen, a zero-glyph cluster. + { + let text = "Titles drawn from integer instructions"; + let u = us(3); + let mut s = doc2(208, 96); + s.sets = vec![stroke.clone()]; + s.texts = vec![block(2, "en", 0, text)]; + let (g1, mut c1, _) = latin(&latin_set, &text[0..12], 0); + c1.push((0, 12, 1)); + let (mut g2, mut c2, w2) = latin(&latin_set, &text[13..28], 13); + g2.push((u16::from(b'-'), w2, 0)); + c2.push((1, 28, 0)); + let (g3, c3, _) = latin(&latin_set, &text[28..], 28); + for (k, (g, c)) in [(g1, c1), (g2, c2), (g3, c3)].into_iter().enumerate() { + s.items.push(run(0, 0, place(&g, (8 * PX, (24 + 30 * k as i32) * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)); + } + add("text", "split-lines", &s, "one block broken over three runs, one per line: the space at the first break drawn by no glyph (a cluster of zero glyphs), and a hyphen inserted at the second (a cluster of no bytes): accepted"); + } + + // ---- text/ruby: a Latin transliteration over a Devanagari base. + { + let base = "\u{0939}\u{093F}\u{0902}\u{0926}\u{0940}"; + let (sh, cl) = shape_devanagari(base); + let (g, c) = lay_out(&rep, &sh, &cl, false, 0); + let (rg, rc, _) = latin(&latin_set, "hindi", 0); + let mut s = doc2(80, 56); + s.sets = vec![tests.clone(), stroke.clone()]; + let mut ruby = block(7, "hi-Latn", 0, "hindi"); + ruby.ruby = 1; + ruby.ruby_start = 0; + ruby.ruby_len = base.len() as u32; + s.texts = vec![block(7, "hi", 0, base), ruby]; + s.items = vec![ + run(0, 0, place(&g, (18 * PX + 31, 46 * PX), us(4)), c, us(4), WHITE, (0, ONE), NO_SLOTS), + run(1, 1, place(&rg, (27 * PX, 14 * PX), 98_304), rc, 98_304, WHITE, (0, ONE), NO_SLOTS), + ]; + add("text", "ruby", &s, "a ruby block, Latin from the stroke set, annotating the whole string of a Devanagari base block from the test repertoire: accepted"); + } + + // ---- text/vertical-rl: upright Latin stacked, mode 1. + { + let u = us(4); + let mut g = Vec::new(); + let mut c = Vec::new(); + for (i, b) in "WAI".bytes().enumerate() { + let adv = i32::from(latin_set.glyph(u16::from(b)).map_or(0, |g| g.advance)); + g.push((u16::from(b), -adv / 2, 1_700 * i as i32)); + c.push((1, i as u32, 1)); + } + let mut s = doc2(48, 96); + s.sets = vec![stroke.clone()]; + let mut t = block(7, "en", 0, "WAI"); + t.mode = 1; + s.texts = vec![t]; + s.items = vec![run(0, 0, place(&g, (24 * PX, 30 * PX), u), c, u, WHITE, (0, ONE), NO_SLOTS)]; + add("text", "vertical-rl", &s, "a vertical-rl block (mode 1): Latin from the stroke set, upright and stacked; the sink draws the glyphs where the payload puts them, and the mode is in the block's identity: accepted"); + } + + // ---- text/emoji: U+1F600 drawn by one monochrome glyph, one cluster of four bytes. + { + let text = "OK \u{1F600}"; + let u = us(3); + let (lg, lc, lw) = latin(&latin_set, "OK ", 0); + let mut s = doc2(80, 40); + s.sets = vec![stroke.clone(), tests.clone()]; + s.texts = vec![block(7, "en", 0, text)]; + let x0 = 6 * PX; + let xf = x0 + r_rat(i64::from(lw) * i64::from(u), 65_536) as i32; + s.items = vec![ + run(0, 0, place(&lg, (x0, 30 * PX), u), lc, u, WHITE, (0, ONE), NO_SLOTS), + run(1, 0, place(&[(FACE, 0, 0)], (xf, 30 * PX), u), vec![(1, 3, 4)], u, WHITE, (0, ONE), NO_SLOTS), + ]; + add("text", "emoji", &s, "U+1F600 drawn by one monochrome outline glyph from the test repertoire, one cluster mapping its four bytes to that glyph: accepted"); + } + + // ---- runs: an Arabic joining sequence in one run, and split across two. + { + let word = "\u{0628}\u{064A}\u{062A}"; + let (sh, cl) = shape_arabic(word); + let (g, c) = lay_out(&rep, &sh, &cl, true, 0); + let u = us(10); + let origin = (9 * PX, 26 * PX); + let mut s = frame_doc(96, 48); + s.sets = vec![tests.clone()]; + s.texts = vec![block(7, "ar", 1, word)]; + s.items = vec![run(0, 0, place(&g, origin, u), c.clone(), u, WHITE, (0, ONE), NO_SLOTS)]; + add("runs", "arabic-joins", &s, "an Arabic joining sequence from the test repertoire in one run, large: each connector's round cap passes its glyph's edge and overlaps the neighbour, and the glyph-wise union over-covers the partly covered edge pixels of each overlap (the join knot, §3.13): accepted"); + // The same glyphs in two runs: the encoder rule keeps one joining sequence in + // one run; a sink still renders the split, compositing the runs by source-over. + let placed = place(&g, origin, u); + s.items = vec![ + run(0, 0, placed[..1].to_vec(), c[..1].to_vec(), u, WHITE, (0, ONE), NO_SLOTS), + run(0, 0, placed[1..].to_vec(), c[1..].to_vec(), u, WHITE, (0, ONE), NO_SLOTS), + ]; + add("runs", "arabic-joins-split", &s, "the same joining sequence split across two runs, which an encoder never writes (one joining sequence per run): the runs composite by source-over, so the join's pixels differ from the one-run vector: accepted"); + } + + // ---- motion/stroke-slice: a lower third and a short credit roll in stroke text. + { + let frames = 50u32; + let end = frames * ONE; + let mut s = SceneDef::new(TB, 0, end, 320, 180); + s.sizes = vec![(320, 180)]; + s.rates = vec![(25, 1)]; + s.sets = vec![stroke.clone()]; + let roll_lines = ["Opening titles", "Credits", "Lower thirds"]; + let name = "Stroke set"; + let role = "Integer outlines, drawn here"; + s.texts = vec![{ + let mut t = block(3, "en", 0, name); + t.group = 1; + t + }, { + let mut t = block(3, "en", 0, role); + t.group = 1; + t + }]; + for l in roll_lines { + s.texts.push(block(2, "en", 0, l)); + } + // The lower third slides in from the left on an easing table over ten frames, + // holds, and fades out over the last ten, as one group. + let ease_out = Ease::Table([0, 9_000, 17_000, 24_000, 30_500, 36_000, 41_000, 45_500, 49_500, 53_000, 56_000, 58_500, 60_700, 62_500, 64_000, 65_000, 65_536]); + let slide = TrackDef { kind: 2, keys: vec![KeyDef { t: 0, value: -300 * PX, ease: ease_out }, KeyDef { t: 10 * ONE, value: 0, ease: Ease::Hold }] }; + let fade = TrackDef { + kind: 1, + keys: vec![KeyDef { t: 0, value: 255, ease: Ease::Hold }, KeyDef { t: 40 * ONE, value: 255, ease: Ease::Linear }, KeyDef { t: end, value: 0, ease: Ease::Hold }], + }; + let u1 = us(3); + let u2 = us(2); + let (ng, nc, _) = latin(&latin_set, name, 0); + let (rg, rc, _) = latin(&latin_set, role, 0); + s.items.push(ItemDef::Begin { clip: None, t_on: 0, t_off: end, tx: Some(slide), ty: None, op: Some(fade) }); + s.items.push(ItemDef::Path { path: PathDef::rect((12 * PX) as i16, (128 * PX) as i16, (236 * PX) as i16, (170 * PX) as i16), rule: 0, paint: [20, 40, 120, 220], text: None, t_on: 0, t_off: end, unit_scale: 65_536, slots: NO_SLOTS }); + s.items.push(run(0, 0, place(&ng, (20 * PX, 148 * PX), u1), nc, u1, WHITE, (0, end), NO_SLOTS)); + s.items.push(run(0, 1, place(&rg, (20 * PX, 164 * PX), u2), rc, u2, [200, 200, 200, 255], (0, end), NO_SLOTS)); + s.items.push(ItemDef::End); + // The roll: three lines moving up three design pixels a frame, keys on frame + // ticks, so frame k is frame 0 moved 3k pixels. + let ty = TrackDef::linear(2, 0, 0, end, -3 * PX * frames as i32); + for (i, l) in roll_lines.iter().enumerate() { + let (g, c, w) = latin(&latin_set, l, 0); + let x = 160 * PX - r_rat(i64::from(w) * i64::from(u1), 2 * 65_536) as i32; + let mut slots = NO_SLOTS; + slots[5] = Some(ty.clone()); + let paint = [WHITE, RED, BLUE][i]; + s.items.push(run(0, (2 + i) as u16, place(&g, (x, (90 + 34 * i as i32) * PX), u1), c, u1, paint, (0, end), slots)); + } + add("motion", "stroke-slice", &s, "a lower third in the stroke set (a bar and two lines that slide in on an easing table, hold and fade out as one group) and a short credit roll moving up three design pixels a frame: accepted"); + } + v +} + +/// The test words' shaping, checked by the joining-table test: `(text, gids in logical +/// order)`. +pub fn test_words() -> Vec<(&'static str, Vec)> { + use repertoire::*; + vec![ + ("\u{0628}\u{064E}\u{064A}\u{0652}\u{062A}", vec![BEH[2], FATHA, YEH[3], SUKUN, TEH[1]]), + ("\u{0633}\u{0644}\u{0627}\u{0645}", vec![SEEN[2], LAM_ALEF[1], MEEM[0]]), + ("\u{0628}\u{064A}\u{062A}", vec![BEH[2], YEH[3], TEH[1]]), + ] +} + +#[allow(dead_code)] +fn _unused() -> String { + format!("{}", 0) +} diff --git a/wai/wai-vector/src/wsf1.rs b/wai/wai-vector/src/wsf1.rs new file mode 100644 index 000000000..4fe506c1a --- /dev/null +++ b/wai/wai-vector/src/wsf1.rs @@ -0,0 +1,543 @@ +//! `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 { + 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>, +} + +/// A parsed `WSF1`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Wsf1 { + pub upem2: u16, + pub radius: u16, + pub glyphs: Vec, + pub notes: Vec, +} + +/// 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(check: u8, rule: &'static str, index: u64) -> Result { + 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 { + // 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 = (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 = 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 { + 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 / 2 + 1; + 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 = (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 { + 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 = 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 { + 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 { + let s = parse(src)?; + let rho = i64::from(s.radius); + let glyphs: Vec<(u16, Vec>)> = + 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); + } +} -- 2.54.0 (Apple Git-157) From 4c61c5b67b33c30a2c41e73d8320b88ad4444f8c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 17:41:08 -0400 Subject: [PATCH 2/6] wai-vector: the default stroke set and the test repertoire, as WSF1 sources expanded by Annex A; the text vectors (D7-5) - 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 --- .github/workflows/wai-vector.yml | 20 +- wai/extensions/graphics-vector.md | 169 ++++++++++++- wai/vector-conformance/README.md | 34 ++- .../expected/motion/stroke-slice.json | 1 + .../expected/runs/arabic-joins-split.json | 1 + .../expected/runs/arabic-joins.json | 1 + .../expected/text/arabic.json | 1 + .../expected/text/devanagari.json | 1 + .../expected/text/emoji.json | 1 + .../expected/text/hebrew.json | 1 + .../expected/text/latin-ascii.json | 1 + .../expected/text/mixed-direction.json | 1 + .../expected/text/ruby.json | 1 + .../expected/text/split-lines.json | 1 + .../expected/text/vertical-rl.json | 1 + wai/vector-conformance/sets/stroke-ascii.wos1 | Bin 0 -> 105523 bytes .../sets/test-repertoire.wos1 | Bin 0 -> 59879 bytes wai/vector-conformance/sha256.json | 23 +- .../sources/stroke-ascii.wsf1 | Bin 0 -> 19181 bytes .../sources/test-repertoire.wsf1 | Bin 0 -> 15168 bytes .../vectors/motion/index.json | 3 +- .../vectors/motion/stroke-slice.json | 211 ++++++++++++++++ .../vectors/motion/stroke-slice.wvg1 | Bin 0 -> 2639 bytes .../vectors/runs/arabic-joins-split.json | 40 ++++ .../vectors/runs/arabic-joins-split.wvg1 | Bin 0 -> 336 bytes .../vectors/runs/arabic-joins.json | 39 +++ .../vectors/runs/arabic-joins.wvg1 | Bin 0 -> 288 bytes .../vectors/runs/index.json | 2 + .../vectors/text/arabic.json | 50 ++++ .../vectors/text/arabic.wvg1 | Bin 0 -> 429 bytes .../vectors/text/devanagari.json | 49 ++++ .../vectors/text/devanagari.wvg1 | Bin 0 -> 432 bytes .../vectors/text/emoji.json | 44 ++++ .../vectors/text/emoji.wvg1 | Bin 0 -> 409 bytes .../vectors/text/hebrew.json | 42 ++++ .../vectors/text/hebrew.wvg1 | Bin 0 -> 322 bytes .../vectors/text/index.json | 11 + .../vectors/text/latin-ascii.json | 226 ++++++++++++++++++ .../vectors/text/latin-ascii.wvg1 | Bin 0 -> 2690 bytes .../vectors/text/mixed-direction.json | 60 +++++ .../vectors/text/mixed-direction.wvg1 | Bin 0 -> 611 bytes wai/vector-conformance/vectors/text/ruby.json | 54 +++++ wai/vector-conformance/vectors/text/ruby.wvg1 | Bin 0 -> 557 bytes .../vectors/text/split-lines.json | 113 +++++++++ .../vectors/text/split-lines.wvg1 | Bin 0 -> 1276 bytes .../vectors/text/vertical-rl.json | 40 ++++ .../vectors/text/vertical-rl.wvg1 | Bin 0 -> 293 bytes wai/wai-vector/Cargo.toml | 8 +- wai/wai-vector/src/bin/wai-vector.rs | 2 +- wai/wai-vector/src/builtin/mod.rs | 27 +++ wai/wai-vector/src/lib.rs | 7 + wai/wai-vector/src/vectors.rs | 33 ++- wai/wai-vector/src/vectors/repertoire.rs | 19 +- wai/wai-vector/src/vectors/sets.rs | 19 +- wai/wai-vector/src/vectors/text.rs | 11 +- wai/wai-vector/src/wsf1.rs | 2 +- wai/wai-vector/tests/stroke_sets.rs | 135 +++++++++++ wai/wai-vector/wasm/Cargo.toml | 3 +- wai/wai-vector/wasm/verify.mjs | 19 +- 59 files changed, 1479 insertions(+), 48 deletions(-) create mode 100644 wai/vector-conformance/expected/motion/stroke-slice.json create mode 100644 wai/vector-conformance/expected/runs/arabic-joins-split.json create mode 100644 wai/vector-conformance/expected/runs/arabic-joins.json create mode 100644 wai/vector-conformance/expected/text/arabic.json create mode 100644 wai/vector-conformance/expected/text/devanagari.json create mode 100644 wai/vector-conformance/expected/text/emoji.json create mode 100644 wai/vector-conformance/expected/text/hebrew.json create mode 100644 wai/vector-conformance/expected/text/latin-ascii.json create mode 100644 wai/vector-conformance/expected/text/mixed-direction.json create mode 100644 wai/vector-conformance/expected/text/ruby.json create mode 100644 wai/vector-conformance/expected/text/split-lines.json create mode 100644 wai/vector-conformance/expected/text/vertical-rl.json create mode 100644 wai/vector-conformance/sets/stroke-ascii.wos1 create mode 100644 wai/vector-conformance/sets/test-repertoire.wos1 create mode 100644 wai/vector-conformance/sources/stroke-ascii.wsf1 create mode 100644 wai/vector-conformance/sources/test-repertoire.wsf1 create mode 100644 wai/vector-conformance/vectors/motion/stroke-slice.json create mode 100644 wai/vector-conformance/vectors/motion/stroke-slice.wvg1 create mode 100644 wai/vector-conformance/vectors/runs/arabic-joins-split.json create mode 100644 wai/vector-conformance/vectors/runs/arabic-joins-split.wvg1 create mode 100644 wai/vector-conformance/vectors/runs/arabic-joins.json create mode 100644 wai/vector-conformance/vectors/runs/arabic-joins.wvg1 create mode 100644 wai/vector-conformance/vectors/text/arabic.json create mode 100644 wai/vector-conformance/vectors/text/arabic.wvg1 create mode 100644 wai/vector-conformance/vectors/text/devanagari.json create mode 100644 wai/vector-conformance/vectors/text/devanagari.wvg1 create mode 100644 wai/vector-conformance/vectors/text/emoji.json create mode 100644 wai/vector-conformance/vectors/text/emoji.wvg1 create mode 100644 wai/vector-conformance/vectors/text/hebrew.json create mode 100644 wai/vector-conformance/vectors/text/hebrew.wvg1 create mode 100644 wai/vector-conformance/vectors/text/index.json create mode 100644 wai/vector-conformance/vectors/text/latin-ascii.json create mode 100644 wai/vector-conformance/vectors/text/latin-ascii.wvg1 create mode 100644 wai/vector-conformance/vectors/text/mixed-direction.json create mode 100644 wai/vector-conformance/vectors/text/mixed-direction.wvg1 create mode 100644 wai/vector-conformance/vectors/text/ruby.json create mode 100644 wai/vector-conformance/vectors/text/ruby.wvg1 create mode 100644 wai/vector-conformance/vectors/text/split-lines.json create mode 100644 wai/vector-conformance/vectors/text/split-lines.wvg1 create mode 100644 wai/vector-conformance/vectors/text/vertical-rl.json create mode 100644 wai/vector-conformance/vectors/text/vertical-rl.wvg1 create mode 100644 wai/wai-vector/src/builtin/mod.rs create mode 100644 wai/wai-vector/tests/stroke_sets.rs diff --git a/.github/workflows/wai-vector.yml b/.github/workflows/wai-vector.yml index 98229e06e..33c9e1447 100644 --- a/.github/workflows/wai-vector.yml +++ b/.github/workflows/wai-vector.yml @@ -8,12 +8,16 @@ name: wai-vector vector graphics # - Linux x86_64, Linux arm64 and macOS arm64, natively, debug and release: the unit # tests, the refusal-order, canonical, clock, track, geometry, cost, held-set and # raster tests, the corpus tests (every frame of the rendering groups recomputed -# to its SHA-256), and `wai-vector vectors verify`, which also regenerates the -# whole corpus and converts every payload's authoring form back to its bytes; +# to its SHA-256), the in-house sets' tests (the default stroke set's SHA-256 is +# Annex B's, Annex A's table is the expansion's, and both sets re-derive from their +# WSF1 sources), and `wai-vector vectors verify`, which also regenerates the +# whole corpus and converts every payload's authoring form back to its bytes; and +# the sink's build, without the feature `author`, so with no stroker; # - WASI (wasm32-wasip2) under wasmtime, a 32-bit target: the same tests and the same # `vectors verify`; # - a browser's engine (wasm32-unknown-unknown) under Node: the recomputation of -# every expected file and every authoring form; +# every expected file and every authoring form, in a sink's build, and the default +# stroke set's SHA-256 against Annex B; # - the fuzz leg: the corpus-seeded fuzz of the WOS1 and WVG1 parsers at depth, each # accepted payload of small cost rendered, the peak-allocation test and the # duplicate-detection comparison count; @@ -45,6 +49,8 @@ on: - 'wai/tools/wai_vector_*.py' # The text the crate and the corpus are held to. - 'wai/extensions/graphics-vector.md' + # The licence text the in-house sets carry in their notes. + - 'LICENSE' - '.github/workflows/wai-vector.yml' push: branches: @@ -55,6 +61,7 @@ on: - 'wai/vector-conformance/**' - 'wai/tools/wai_vector_*.py' - 'wai/extensions/graphics-vector.md' + - 'LICENSE' - '.github/workflows/wai-vector.yml' jobs: @@ -101,6 +108,13 @@ jobs: shell: bash run: cargo run --locked --release --features std --bin wai-vector -- vectors verify .. + # A sink has no stroker (graphics-vector.md §3.14): the library builds without + # the authoring code. + - name: The sink's build, without the feature author + working-directory: wai/wai-vector + shell: bash + run: cargo build --locked --no-default-features --lib + wasip2: name: wasm32-wasip2 (wasmtime) runs-on: linux diff --git a/wai/extensions/graphics-vector.md b/wai/extensions/graphics-vector.md index 795287c94..3a4ce0e8c 100644 --- a/wai/extensions/graphics-vector.md +++ b/wai/extensions/graphics-vector.md @@ -3,9 +3,10 @@ > Status: Draft. This extension defines `wai.graphics.vector`: titles, credits and other authored graphics carried as integer drawing instructions, which the sink rasterises into an overlay layer. This revision defines: > - the model, the conformance criteria and what they cover (§1); > - the payload format `WVG1`, the outline-set format `WOS1`, the digest sidecar, and the order in which a sink refuses a payload (§2); -> - the arithmetic: the clock, track evaluation, placement, the load-time interval check and cost, flattening, coverage and compositing, written as the raster stage family RS1–RS9 (§3). +> - the arithmetic: the clock, track evaluation, placement, the load-time interval check and cost, flattening, coverage and compositing, written as the raster stage family RS1–RS9 (§3); +> - outline sets: pinning and carriage, the notes and the licence they carry, the default stroke set and the test repertoire (§4), the stroke source format `WSF1` and its integer expansion (Annex A), and the default stroke set's digest (Annex B). > -> Sections 4 to 6 and Annexes A and B are headings in this revision. Their text lands in later revisions of this Draft: outline sets and the default stroke set (§4, Annexes A and B), text and accessibility (§5), and packages and floors (§6). Where §1–§3 depend on them, they say what they depend on. +> Sections 5 and 6 are headings in this revision. Their text lands in later revisions of this Draft: text and accessibility (§5), and packages and floors (§6). Where §1–§4 depend on them, they say what they depend on. > > **What a reader can build from this text.** A sink that loads a `WVG1` payload and the `WOS1` sets it names, refuses exactly the payloads §2.7 refuses with the code §2.7 gives, and renders each listed size, rate and frame to the bytes §3 defines; and a verifier that checks rendered frames against a digest sidecar (§2.10). > @@ -811,7 +812,63 @@ Every stage is total once the payload has loaded: the point cap, the range and t ## 4. Outline sets -*This section lands in a later revision of this Draft:* how a set is made from a font (authoring only), pinning and carriage, the keys of a set's notes and the licence they carry, and the default stroke set (Annexes A and B). §2.1 defines the format a sink reads. +### 4.1 The sink reads `WOS1` only + +A sink never parses a font file and never runs a font program. It reads `WOS1` (§2.1): fixed-width records and bounded counts, no recursion (composite glyphs are flattened before a set is written), no bytecode, subroutines, compression, bitmaps or variations, an exact box per glyph, and named refusals. + +An authoring tool makes a set, from a font or from a stroke source (Annex A). How it does so is not sink conformance: a sink conforms by matching the set's digest, and a set is what its bytes are. + +### 4.2 Pinning and carriage + +- **Pinned** means named by SHA-256 over the whole `WOS1`, notes included, in a `SET` record, which also states the set's exact length, whether the set is held or inline (§2.3). +- **Inline:** the set's bytes are in `INLINE`. The sink hashes them and refuses the payload on a mismatch (§2.7 step 6). The payload is then self-contained. +- **Held:** the sink obtains the bytes by digest under its resolution policy: for example, only the sets bundled with it, a cache, or a fetch by digest. It reads at most `length + 1` bytes, refuses a length mismatch (`outline_mismatch`), and hashes the bytes before it parses them, whatever their source (§2.7 step 6). +- **Bundled:** the default stroke set (§4.4) ships with every reference sink and is always held. +- **Never substitute** (§1.6). A set the sink cannot obtain refuses the payload (`outline_not_held`), and the sink presents its floor. +- **Revocation** of a held set is a later revision's (§2.7 step 6 reserves its names). + +### 4.3 The notes: provenance and licence + +The notes of a set made by the authoring tool are UTF-8 `key=value` lines, each ending in U+000A, then a blank line, then the full licence text of the set's source. A key that does not apply to a source is omitted. + +| key | content | +|---|---| +| `parent_sha256` | the SHA-256 of the source: the font file, or the stroke source (Annex A), in lowercase hexadecimal | +| `converter` | the tool's name and version, or for a stroke source the expansion's (Annex A) | +| `licence` | the source's licence, as an SPDX identifier, for example `Apache-2.0` | +| `licence_sha256` | the SHA-256 of the licence text that follows the blank line | +| `copyright` | the source's copyright lines, verbatim; repeatable | +| `rfn` | each name the source's licence reserves, or `none` | +| `rules` | for a set converted from a font: the canonicalisation, that is the ×2 grid, whether overlaps were removed (`off`, or the SHA-256 of the pinned module that removed them), and any down-shift | +| `instance` | for a variable font: the axis tags and values the set was instanced at | +| `source` | for a stroke source: where its drawing comes from | +| `glyphs` | the source glyph IDs the set keeps, as ranges, and what they mean | +| `metrics` | `ascender,descender,cap_height,x_height` in outline units, read by the authoring tool's checks and never by a sink | + +- **The licence travels inside the digest.** A set served by digest from any origin carries its licence and notices in the pinned bytes, and no copy can drop them without changing the digest. +- A set has no family-name field, so it never presents a reserved name as its own. +- **What checks what.** A sink checks only that the notes are valid UTF-8 (§2.1). A verifier checks that `licence_sha256` is the SHA-256 of the text after the blank line, and reports `licence_mismatch` when it is not. + +### 4.4 The default stroke set + +- **A stroke source, expanded once.** The default stroke set is drawn in this repository as original centre lines, in the stroke source format `WSF1` (Annex A). No outline is traced from any font. Annex A's integer expansion turns it, once, into an ordinary polygon-only `WOS1`, which is pinned and bundled. The sink has no stroker (§3.14). +- **Licence:** Apache-2.0. The set's notes carry the licence text and say where the drawing comes from. +- **Coverage:** printable ASCII, U+0020–U+007E. Each glyph ID is the code point the glyph draws. A set with further characters is a new set with its own digest, never a change to this one. +- **Carriage:** an encoder names it with a held `SET` record stating Annex B's digest and length. A reference sink resolves it from the bytes bundled with it. +- **Layout** is the authoring tool's. It places the glyphs from the advances in the source (Annex A); a sink reads none. +- Where two stroke paths of one glyph overlap inside a partly covered pixel, the fill over-covers (§3.13). + +### 4.5 The test repertoire + +A second stroke source, drawn in this repository under Apache-2.0, is for the conformance corpus only. It is never bundled, and never a default for authors. It holds only what the corpus's test words need: +- **Arabic:** the isolated, initial, medial and final forms of the letters of the test words, the lam-alef ligature as one glyph, and two marks; +- **Hebrew:** the letters of the test words, among them one final form; +- **Devanagari:** the consonants of the test words, each with its headline; one conjunct as a single glyph; the vowel sign i, which displays before the consonant it follows in logical order; and one mark above; +- one monochrome glyph for U+1F600, for a visible character that a colour glyph would otherwise draw. + +Its shaping is a small table in the corpus generator: Arabic forms chosen by Unicode joining type, the ligature, the reordered vowel sign, and authored mark positions. Each joining form's connector, and each Devanagari headline, runs to the edge of its glyph's advance, so the pen's round cap passes the edge and overlaps the neighbour, as joins in production fonts overlap; inside one run those overlaps over-cover their partly covered edge pixels (§3.8, §3.13). + +With it, the corpus pins right-to-left visual order, contextual forms, a conjunct, a reordered vowel sign, marks, clusters and mixed direction on every fabric, with no third-party font. It does not test a shaper on a production font. ## 5. Text and accessibility @@ -823,8 +880,110 @@ Every stage is total once the payload has loaded: the point cap, the range and t ## Annex A. `WSF1`, the stroke source format -*Lands with §4.* +A `WSF1` is an authoring-side source: a sink never reads one. It holds, for each glyph, stroke paths: polylines of integer centre-line points in outline units, drawn with one round pen of radius `ρ`, with round caps and round joins. The expansion below turns it into a `WOS1` (§2.1) with integer arithmetic alone. §2.0's common rules apply: little-endian fixed-width integers, no offsets, the exact length computed from the header, and caps checked before anything is allocated. + +### A.1 Layout + +Header (32 bytes) · glyph table (8 × `n_glyphs`) · stroke ends (2 × `strokes_total`) · points (4 × `points_total`) · notes (`notes_len`). The exact length is `32 + 8·n_glyphs + 2·strokes_total + 4·points_total + notes_len`. + +**Header (32 bytes)** + +| off | size | field | rule | +|---|---|---|---| +| 0 | 4 | magic | the ASCII bytes `WSF1` | +| 4 | 4 | `format` | `u32` = 1 | +| 8 | 2 | `upem2` | `u16`: twice the source's units per em, copied to the set's `upem2` | +| 10 | 2 | `radius` | `u16`: the pen radius `ρ` in outline units, 1 ..= 4096 | +| 12 | 4 | `n_glyphs` | `u32`, 1 ..= 65,536 | +| 16 | 4 | `strokes_total` | `u32`, ≤ 2^14 | +| 20 | 4 | `points_total` | `u32`, ≤ 2^16 | +| 24 | 4 | `notes_len` | `u32`, 1 ..= 2^19 | +| 28 | 4 | `zero` | | + +**Glyph record (8 bytes):** `gid: u16`, strictly increasing across records. `n_strokes: u16`, ≤ 64, ≤ `n_points`, and at least 1 when `n_points ≥ 1`. `n_points: u16`, ≤ 256. `advance: u16`: the glyph's advance in outline units, which an authoring tool's layout reads and the set does not carry. + +**Stroke ends.** For each glyph in record order, `n_strokes` values, `u16`: the index, within the glyph, of each stroke path's last point. They strictly increase, and the last equals `n_points − 1`. + +**Points.** For each glyph in record order, `n_points` points of 4 bytes, `x: i16` and `y: i16`: centre-line points in outline units, y down, the origin at the glyph's pen position on the baseline. + +**Notes.** UTF-8, in §4.3's form; the expansion copies them into the set's notes. + +**Checks, in order.** An expansion refuses a source at the first failure: +1. fewer than 32 bytes; the magic, `format` or the `zero` field; +2. `radius`, `n_glyphs`, `strokes_total`, `points_total` and `notes_len`, in field order, each against its range; +3. the exact length; +4. each glyph record: `n_strokes` and `n_points` against their caps, then `n_strokes` against `n_points`; +5. the `gid`s strictly increase; +6. the records' `n_strokes` sum to `strokes_total`, and their `n_points` to `points_total`; +7. each glyph's stroke ends; +8. every point has `−2^15 + ρ ≤ x, y ≤ 2^15 − 1 − ρ`; +9. within each stroke path, consecutive points differ, and no two consecutive segments point in exactly opposite directions (their cross product is 0 and their dot product negative); +10. the notes are valid UTF-8 (RFC 3629). + +**Why the caps fit `WOS1`.** A stroke path of `m ≥ 2` points expands to at most `36·m − 4` vertices and one of one point to 64, so a glyph expands to at most `36·256 + 64·64 = 13,312 ≤ 16,383` points (§2.1), and a source to at most `36·2^16 + 64·2^14 < 2^22` points and `2^14` contours. Every vertex lies within `ρ` of its centre-line point on each axis, so by check 8 it fits `i16`. + +### A.2 The table of unit vectors + +`U` is 64 unit vectors in Q15: `U[k] = (round(2^15·cos(2πk/64)), round(2^15·sin(2πk/64)))` for `k = 0 … 63`, each rounded once to the nearest integer. With y down, `k` increases from +x toward +y. **These bytes are normative:** for each `k` in order, the `cos` component, then the `sin` component, each an `i32` little-endian, 512 bytes in all: + +```text +0080000000000000627f00008c0c00008a7d0000f91800007d7a000028250000 +42760000fc300000e3700000573c00006e6a00001d470000f262000034510000 +825a0000825a000034510000f26200001d4700006e6a0000573c0000e3700000 +fc30000042760000282500007d7a0000f91800008a7d00008c0c0000627f0000 +000000000080000074f3ffff627f000007e7ffff8a7d0000d8daffff7d7a0000 +04cfffff42760000a9c3ffffe3700000e3b8ffff6e6a0000ccaefffff2620000 +7ea5ffff825a00000e9dffff345100009295ffff1d4700001d8fffff573c0000 +be89fffffc3000008385ffff282500007682fffff91800009e80ffff8c0c0000 +0080ffff000000009e80ffff74f3ffff7682ffff07e7ffff8385ffffd8daffff +be89ffff04cfffff1d8fffffa9c3ffff9295ffffe3b8ffff0e9dffffccaeffff +7ea5ffff7ea5ffffccaeffff0e9dffffe3b8ffff9295ffffa9c3ffff1d8fffff +04cfffffbe89ffffd8daffff8385ffff07e7ffff7682ffff74f3ffff9e80ffff +000000000080ffff8c0c00009e80fffff91800007682ffff282500008385ffff +fc300000be89ffff573c00001d8fffff1d4700009295ffff345100000e9dffff +825a00007ea5fffff2620000ccaeffff6e6a0000e3b8ffffe3700000a9c3ffff +4276000004cfffff7d7a0000d8daffff8a7d000007e7ffff627f000074f3ffff +``` + +Their SHA-256 is `11c05a1323c8b6dde89a5627d29acecb9163b87dfd169054236fb1d1def66437`. No trigonometry is evaluated anywhere else: the expansion reads only these integers. + +### A.3 The expansion (normative) + +The notation is §3.0's (`R_rat`, `R_half_up`, `isqrt`), with `cross(a, b) = a_x·b_y − a_y·b_x`. Every value is an exact integer, and every one fits `i64`. + +**The set.** One `WOS1` glyph record for each source glyph, in order, with the same `gid`. One contour for each stroke path, in order, every point on-curve (tag 0). `format` 1; `upem2` copied; `flags` 0, because overlaps are not removed; each glyph's box exact (§2.1). The notes are the line `parent_sha256=` followed by the source's SHA-256 in lowercase hexadecimal, the line `converter=wsf1-expansion/1`, each ending in U+000A, then the source's notes, byte for byte. + +**A segment's offset.** For a segment `d = (dx, dy)`, `len = isqrt(dx² + dy²)` and `n(d) = (R_rat(−dy·ρ, len), R_rat(dx·ρ, len))`. Check 9 makes `len ≥ 1`. + +**Arc vertices.** The vertex of direction `k` at a point `p` is `p + (R_half_up(ρ·U[k]_x, 15), R_half_up(ρ·U[k]_y, 15))`. An arc at `p`, from an offset `a`, takes the directions `u = U[k]` that lie strictly between its ends in the **negative sense** (the sense of decreasing `k`): +- a **cap**, from `a` to `−a`: every `u` with `cross(a, u) < 0`; +- a **join**, from `a` to `b`: none unless `cross(a, b) < 0`; otherwise every `u` with `cross(a, u) < 0` and `cross(u, b) < 0`. + +The chosen directions lie in one open half-plane, and are taken in order from `a`: `u` before `v` when `cross(u, v) < 0`. + +**The contour of a stroke path** `p_0 … p_{m−1}`: +- **`m = 1`, a dot:** the vertices of every direction at `p_0`, for `k = 0, 63, 62, …, 1`. +- **`m ≥ 2`:** let `d_i = p_{i+1} − p_i` and `n_i = n(d_i)` for `i = 0 … m − 2`, and at each interior point `c_i = cross(d_{i−1}, d_i)`. The vertices are, in order: + 1. `p_0 + n_0`; + 2. for `i = 1 … m − 2`: `p_i + n_{i−1}`; if `c_i < 0`, the join from `n_{i−1}` to `n_i` at `p_i`; then `p_i + n_i`; + 3. `p_{m−1} + n_{m−2}`, then the cap from `n_{m−2}` at `p_{m−1}`; + 4. `p_{m−1} − n_{m−2}`; + 5. for `i = m − 2` down to 1: `p_i − n_i`; if `c_i > 0`, the join from `−n_i` to `−n_{i−1}` at `p_i`; then `p_i − n_{i−1}`; + 6. `p_0 − n_0`, then the cap from `−n_0` at `p_0`. + + So the left offsets run forward, the end cap turns, the right offsets run back, and the start cap closes the contour. The outer side of each join takes the arc; the inner side takes its two offset points only, and the small loop they make lies inside the stroke's body, where the non-zero rule covers it. +- **Then** a vertex equal to the one before it is dropped, and while the contour has more than one vertex and its last equals its first, the last is dropped. + +The same source therefore gives the same set, byte for byte, on every machine. ## Annex B. The default stroke set's digest -*Lands with §4.* +| | | +|---|---| +| the set (`WOS1`) | SHA-256 `05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9`, length 105,523 bytes | +| its source (`WSF1`) | SHA-256 `4eec759d394d021aec8ce99b41e4b25fecb36372c77459ef62df182b9eb8c783`, length 19,181 bytes | +| coverage | U+0020–U+007E; each glyph ID is the code point it draws | +| pen | radius 80 outline units; `upem2` 2000 | +| licence | Apache-2.0, its text in the notes | + +Expanding the source by Annex A gives exactly the set's bytes. The conformance corpus holds both, as `sources/stroke-ascii.wsf1` and `sets/stroke-ascii.wos1`, and every fabric leg checks the set's digest against this annex. diff --git a/wai/vector-conformance/README.md b/wai/vector-conformance/README.md index 18b021d1f..a4b9f300f 100644 --- a/wai/vector-conformance/README.md +++ b/wai/vector-conformance/README.md @@ -5,15 +5,12 @@ Vectors for `wai.graphics.vector` outline set and the `WVG1` payload with the order in which a sink refuses them (§2), the arithmetic a sink loads with: the clock, track evaluation, the load-time interval check and the per-frame cost (§3.1–§3.4); and the raster stage family RS1–RS9 and the -frame procedure (§3.5–§3.10), as the SHA-256 of every frame's `RGBA8P`. The -capability is a Draft and is not registered; this corpus precedes its registration. +frame procedure (§3.5–§3.10), as the SHA-256 of every frame's `RGBA8P`; and text runs +drawn from the in-house outline sets (§4): the default stroke set and the test +repertoire. The capability is a Draft and is not registered; this corpus precedes its +registration. -**Provenance.** Generated here by `wai-vector vectors gen` from formulas: every -payload, outline set and arithmetic input is integers and text written in this -repository, with no media and no font. The outline sets under `sets/` and inside the -payloads are drawn from formulas in `wai-vector/src/vectors/sets.rs`, and each set's -notes state that it was generated here and is licensed Apache-2.0. All text in the -blocks is written here. +**Provenance.** Generated here by `wai-vector vectors gen` from formulas and from centre lines drawn in this repository: every payload, outline set, stroke source and arithmetic input is integers and text written here, with no media and no third-party font. The synthetic outline sets are drawn from formulas in `wai-vector/src/vectors/sets.rs` and `raster.rs`. The two in-house sets are drawn here as integer centre lines, the ASCII stroke set in `wai-vector/src/vectors/stroke.rs` and the Arabic, Hebrew and Devanagari test repertoire in `wai-vector/src/vectors/repertoire.rs`; the generator writes them as `WSF1` sources under `sources/` and expands each by Annex A into `sets/stroke-ascii.wos1` and `sets/test-repertoire.wos1`. No outline is traced from any font. Every set's notes state where it comes from and that it is licensed Apache-2.0, and the two in-house sets' notes carry the licence text. All text in the blocks is written here. **Equivalence.** A sink conforms on this corpus when, for every payload, it reports the expected file's `code`: the first refusal §2.7 gives, or `accepted`; and for every @@ -27,6 +24,10 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. - `sets/.wos1` — the outline sets the payloads hold as held sets. A held `SET` record finds its set here by SHA-256; a digest none of these files has is not held. + Among them: `stroke-ascii.wos1`, the default stroke set, whose SHA-256 is Annex B's; + and `test-repertoire.wos1`, the test repertoire (§4.5). +- `sources/.wsf1` — the `WSF1` stroke sources of those two sets (Annex A). + Expanding a source by Annex A gives its set byte for byte. - `vectors//.wvg1` — a payload, with its JSON authoring form (§2.12) beside it as `.json`. The form has one member per field, zero fields included, and converts back to the same bytes (`wai-vector pack`). @@ -41,7 +42,8 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. identity, in reading order), `sizes` (`[w, h, cost]`) and `rates` (`[num, den, P, first frame, last frame]`; the range is empty when the first exceeds the last); - - in the groups that render, `frames`: for each listed size and rate, every frame's + - in the groups that render (`fills`, `runs`, `composite`, `motion`, `clock`, + `slicing` and `text`), `frames`: for each listed size and rate, every frame's `[g, SHA-256 of RGBA8P]` and the clip digest, the SHA-256 of the frames' digests concatenated in order of `g` (SPEC §7, *Digests*); in `composite`, also each frame's `RGBA8S` digest (`rgba8s`); @@ -86,16 +88,26 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. §3.13. - `runs` — glyphs that abut mid-pixel (no seam within a run, a seam across runs), overlap, carry marks; a run of 4,096 glyphs; one glyph at all 256 subpixel phases. - The run set is drawn here (`vectors/raster.rs`) and carried inline. + The run set is drawn here (`vectors/raster.rs`) and carried inline. And an Arabic + joining sequence from the test repertoire, in one run (its joins over-cover, the + knot of §3.13) and split across two (the runs composite by source-over). - `composite` — opacity, isolated groups four deep, clips under both rules, the group pop, and pixels on which the two candidate fill orders differ (§3.9). - `motion` — a credit roll at an integer speed (frame `k` is frame 0 moved, P2) and at a fractional one, a lower third that slides, holds and fades as a group, and each - easing mode. + easing mode; and a lower third and a short credit roll set in the stroke set. - `clock` — one document at 1280 × 720, 1920 × 1080 and 3840 × 2160, and at 25, 30000/1001 and 60 fps. - `slicing` — a document and three pieces cut from it; each piece's frames equal the whole's at the same `g` (P7). +- `text` — text runs from the held stroke set and test repertoire, each at two listed + sizes: printable ASCII; Arabic right to left, with contextual forms, the lam-alef + ligature and two marks; Hebrew, with a final form; Devanagari, with the vowel sign i + drawn before its consonant, a mark above and a conjunct; an Arabic block holding a + Latin word and digits, its runs from both sets; one block over three runs and lines, + with a cluster of no glyphs and an inserted hyphen; a ruby annotation; a vertical-rl + block; and U+1F600 drawn by one monochrome glyph. The blocks' projection lands with + §5; these vectors pin runs, clusters, blocks and frames. ## Regenerating and verifying diff --git a/wai/vector-conformance/expected/motion/stroke-slice.json b/wai/vector-conformance/expected/motion/stroke-slice.json new file mode 100644 index 000000000..a0072ae44 --- /dev/null +++ b/wai/vector-conformance/expected/motion/stroke-slice.json @@ -0,0 +1 @@ +{"blocks":["8166a9197be92a87f3b474fd6cfec0925a355fd19b6e1a5d56dc136d27607c20","a7802bc583a32d0e59dcfbf04b385f3dda345d510a8e91f0103fed7faf3450e6","22607a28bcee502181efb54a8454a8f4b78e3631141273156b79814bcfb1b835","1f7ca8fa84ab49efeaa3cc1076703b5c4377c520cc61d6f2ab65e540e5bbece3","42c60ae8daa74f0e9be2b0ec1c569cc76c44eea3bf78a060f4c2037191a0812a"],"code":"accepted","frames":[{"clip":"efca9b207efa9d9d487552bb9ed6c1e4718fdfba22c56b075988268a76716a8b","den":1,"frames":[[0,"51f9c33fb9f7c65fc675cf9a8f286a95e39d11eed58a9bd52b91da323ac583ea"],[1,"24af64a8b20b655727d5e62c2964474e58ad993a696354d190b8f1a45ca863cd"],[2,"db89480066e6c1cd54ba1048e45c964f476a46aca8dadd2af9a10267c7ec0c09"],[3,"f0e46cbd094e20023dd845ca7416dc4954381319a6587fab862bb5cdc76daa1c"],[4,"e8f05e1feecd24a6365ec7c25152dde7809f1bf1dd51edd63b14d19457af5c4e"],[5,"c8cf3704099bd7d955e781205b9c7e050c9e98ca80549c54713b9ec2fe0e00d7"],[6,"65bfcac71d5184201c42c2e7351ee3cda8c35da9d43864302829bf13462c9103"],[7,"6d244b03beceff5c11e2bdfe0e0ab9c46abfd0af7ecf85eeaf4a02676867a662"],[8,"94f0bf7aeaf7082ebfdb8285ade3d7c6310a93eb799d2cb4081a7bb34c32d659"],[9,"1c5cb976f91ff2b16bedf7e6c11b47cdcf04fc68b60dca09ce4881892a11fd7e"],[10,"82facce1e6ace3c5f2dd3950d0ecf17c26dd5897f849a4a512a73b7a68d15be5"],[11,"4eb8e66960ff2b8dd5bc5a8eafa29a04ce55ce234c3abe45dfd0b6ca4b10b6f6"],[12,"2308d9f1cc973be6e38c90d8a40d400e6098e18ee0e5235857a3c9b64a7ede3a"],[13,"ea1b0c888c5857d4fa136cc8fcdfc05c4b3d0f40e87d39dbae7852d5850abbcb"],[14,"b54e2d5ba0feca12e0fef1330bc1b8799e06bf7e69442405fc0e371084f83b56"],[15,"f0aec5846b8a7ffa9675e569793bf6c66b9dd72b64b6d9487318710f412d761b"],[16,"802a585234f5bf690b0163cb1744c29a5aba2768e5a7b794516603f4359f2ac2"],[17,"2724930371428790b3c67d870e066bbcaf1d4d3237bbbaf0e1e24fec1d342728"],[18,"973db68e7ec16691846999513f76acb5b0b60264cd09c9c3b4f061ed84857747"],[19,"a52d6e5d92df1d2d6363b2f4fd5a679f08f3f6d91dcde2b2472925a818b13a03"],[20,"5cff1ffd3f5afd9f8e2638ffe81d73c3c730e324841720f8312c1e8d2a07ee8f"],[21,"ff529fbffabfe7f369a64552493306c647871c0e92f69502e2105cec9e1820ac"],[22,"0d9d7100639681c499921c3350d94e3c8b430f34134e572da3d944dbe7ff53c4"],[23,"42f1d26e6ba83fbaeaa2c529b0803884676f6d90fa8925c33bdbf7a6412b4724"],[24,"fe17cf005b9b92a9a87615b917cd26ea9445974ad0ac2ea6cad1f85ce415d8d2"],[25,"41bca0991e394f37dbea57547dae054f215f52679e11156f5ba7979d36db8dc3"],[26,"7e48b6b4aa98b1d2c864982a9a02c52e601941feed5d86b5868c3d1a1f96dc18"],[27,"4d4c3bd23afeb979a1cb0034c75bb5bdbbb54b261cd7ea4f3ac39c4c515784d6"],[28,"eddeb7d5d5cc73c4959e678607ca9c41087cf37b3e9915a5eb7a4333b625698d"],[29,"3b7b02f68a0e884802455183e5b7da724d2a8bf7aad600d9a56e1f1d4ff15ed9"],[30,"3e1fca6d53f6ddb3593f15c80f0d9497e5946fec1cb4a0237284f38eb5e6129d"],[31,"d3d830844430aea59a3bc0f2ba0c78eebff23d1176bd97d5c68f11e994dab90a"],[32,"0ce0e9b42a968d189717f0fa70784941d8ea6a80f117ddc1ecd02bd6abdb6e9a"],[33,"ca66ee4fb115569f49639b0e283fc1e8d83aa7d69f15e145a635a0ad42a00ab3"],[34,"68abea21cbcea92374a5d4b535f19e606245b56d263dbf2029c7188b2afba706"],[35,"b9704483f218a364462a753c6a9fa86eeeb31907f9360c6d7b86f571a8d4372d"],[36,"e736441c2e198921e930da342190caac604e89fe8bd78a2fa29e6d841712044b"],[37,"fddfc9d19e1407a3020e81bfa95d6ff44eec9bc5f2558902ccbec7a2be71dbc9"],[38,"678c26a298fac8829d90179f7efdc6e0ad4599f5dbfb780bf16e94887eea669b"],[39,"de36b5bd62944fcc7d0b405d902a13e4b4b97c29774522cea546a8e510addde3"],[40,"297c16b9eb660a49ed5d0f1ae49079842db1f4b7931935688d1472e2e4f3e763"],[41,"4bd50eb0ab2fe83261de4c761ea03c6ebc5baabd7342b41a3da1d19ec82cdf8f"],[42,"9be53e089b51bf5d3ad39942c820a7457e6838f735b5eade03cdb21b19166160"],[43,"7170ce78e75c7b712e4fbf6584ff4f7afb5182580ffe02bb643047020a09456e"],[44,"160a4bc34b110689eae4bf90b9506d5ec4fbbb93e57018825dd3b80f6e169c16"],[45,"6fcaac14edaef1282473e0fd61f4a142794e0a6eb47bcc94eb5c53834a43edae"],[46,"6cbd88fb80d87b14e384b2ca5dba058bba8e305ad848100abb50c3784f3771e6"],[47,"f8da02c0c14dd4fadd511f9ff14d198fb3214c3cf1b8613a5e96b5c8f00390aa"],[48,"75b7c82c389fa10aff769a103fef2dc963edb6583ac7d40ddb532c5b1f73f81a"],[49,"16898aeb46ad07fb8974a4bff2c476de5df3ad85c90617bcdfa838b6685117c4"]],"h":180,"num":25,"w":320}],"instr":"42d82a584a14fb454f497155849ab6cd482fb5f53e572e8d527953808263edde","rates":[[25,1,4800,0,49]],"sizes":[[320,180,16863033]],"text_hash":"573b489ff1943d4ead50cd8ca74011a52ea4a4546e835ea8c0baf0f9a9ee7fca"} diff --git a/wai/vector-conformance/expected/runs/arabic-joins-split.json b/wai/vector-conformance/expected/runs/arabic-joins-split.json new file mode 100644 index 000000000..a56283605 --- /dev/null +++ b/wai/vector-conformance/expected/runs/arabic-joins-split.json @@ -0,0 +1 @@ +{"blocks":["a757954c6cca3876765c556e0b6548164c4421f220af1a52a8e1070882cf6e5e"],"code":"accepted","frames":[{"clip":"4bce3a67a81481a01b29f1d930cb08a41260dcc5fd8df07a9a2e6deec5bcf55b","den":1,"frames":[[0,"6b605b7066f21ddcfc5545748a575c1158de85a39bb1d31bfb094d73bbdc20ea"]],"h":48,"num":25,"w":96}],"instr":"892dc5c57c58c53e45a20a973dda9e66ebaa4a2b34c9e0947d0eb0ab264f47d5","rates":[[25,1,4800,0,0]],"sizes":[[96,48,718113]],"text_hash":"d9b1b13fa7eb857adc66777ca431c6c5fc4e2f73481b0256faddde4dea1bb119"} diff --git a/wai/vector-conformance/expected/runs/arabic-joins.json b/wai/vector-conformance/expected/runs/arabic-joins.json new file mode 100644 index 000000000..d179a17bc --- /dev/null +++ b/wai/vector-conformance/expected/runs/arabic-joins.json @@ -0,0 +1 @@ +{"blocks":["a757954c6cca3876765c556e0b6548164c4421f220af1a52a8e1070882cf6e5e"],"code":"accepted","frames":[{"clip":"10583262cf57dc99cf295a5ee9211922618353fa41268d5732ab3b063a7599a4","den":1,"frames":[[0,"828d00fc87f087778556d6ed5de3fa8a663aca6ab8455171f6fde0b32a64aff9"]],"h":48,"num":25,"w":96}],"instr":"c7dc3d22c6760632f8c97c6e0832a03337a39becfca8f306fa2292ed4f1c9826","rates":[[25,1,4800,0,0]],"sizes":[[96,48,718535]],"text_hash":"d9b1b13fa7eb857adc66777ca431c6c5fc4e2f73481b0256faddde4dea1bb119"} diff --git a/wai/vector-conformance/expected/text/arabic.json b/wai/vector-conformance/expected/text/arabic.json new file mode 100644 index 000000000..4bdbcf7a4 --- /dev/null +++ b/wai/vector-conformance/expected/text/arabic.json @@ -0,0 +1 @@ +{"blocks":["a1ac5064cd79c3326dede6e01d1ec6c694ab870bc43fd6373945023777fa900d"],"code":"accepted","frames":[{"clip":"b6faa8405f77ad6006d55a270b243e27c43be96c5af6bb3e79d8cf92c37da458","den":1,"frames":[[0,"e33d9768bca8769dee48af7fce73e8bab3c95644b2d18c797c3d8ccec098dca9"]],"h":64,"num":25,"w":160},{"clip":"9a3aa42017dcc2bd42ac3c689c9e1202c5e0deedc08412eebd22544f4b416a06","den":1,"frames":[[0,"e031862eb79ebb800f479f9a616554729c5fc03e9ab8c27b219f79c454f35bd8"]],"h":128,"num":25,"w":320}],"instr":"e2bec5bcae46fe9117c9c0a954340d05bb052dc09f0fe6db0ba7d8f9c3c00dce","rates":[[25,1,4800,0,0]],"sizes":[[160,64,1822464],[320,128,1923747]],"text_hash":"8c94168b3435c74649178864574423da358f59e6a84134622e57ebb7651e20a0"} diff --git a/wai/vector-conformance/expected/text/devanagari.json b/wai/vector-conformance/expected/text/devanagari.json new file mode 100644 index 000000000..d39ce9940 --- /dev/null +++ b/wai/vector-conformance/expected/text/devanagari.json @@ -0,0 +1 @@ +{"blocks":["4f6ea9772cb64faeca1a8200e629d0640490bda90e68f6e25ebf8903eb14bb94"],"code":"accepted","frames":[{"clip":"05c2577e300c87d05547cec85b2bee68e9729e75d918e340e06cabcc4c3002f2","den":1,"frames":[[0,"de7b8a4d77e1ed7f6b1e653d4166ef817f41f2de229122f6014b61a16bf813bb"]],"h":48,"num":25,"w":128},{"clip":"0191405f20ddcfa808bc147688f0d8d40c59acb05dccf4d49eced0e54fd7e9ba","den":1,"frames":[[0,"64a8e694fb523303cb780cbe91bcfb9aa8c92403719a0d9fcddfaf86a9d12f10"]],"h":96,"num":25,"w":256}],"instr":"ee7d172201a0df5f21bdebc0716cee11d376da5890f7b4c073170535a9be62d1","rates":[[25,1,4800,0,0]],"sizes":[[128,48,2409572],[256,96,2538475]],"text_hash":"8bf4aff8247d493288c89751a2b6bc133346d6416568df89eaeb7b8c3e384974"} diff --git a/wai/vector-conformance/expected/text/emoji.json b/wai/vector-conformance/expected/text/emoji.json new file mode 100644 index 000000000..80d3f1a56 --- /dev/null +++ b/wai/vector-conformance/expected/text/emoji.json @@ -0,0 +1 @@ +{"blocks":["21b751ed648ba10c6594afca8701e14dcc8f19880662f3a571f4da32ae581677"],"code":"accepted","frames":[{"clip":"f8f901be288a067da15e32b5374eb4b6961268e7ec44827fc0e1efcf2bb835e0","den":1,"frames":[[0,"db02e147c6aabbb4fb2a37b88f1f0e04c92274929d7899bf39d813701056bb33"]],"h":40,"num":25,"w":80},{"clip":"1a6a95ea77ed01e8ab5a17e8bbc05646d3d30a8cf6935eba8165d5dad1c810aa","den":1,"frames":[[0,"7971ccd3260c96e3e1ad4ad97956af04a6c84b9b16fa660b3aa0a4ea2f3b4a21"]],"h":80,"num":25,"w":160}],"instr":"409258a329f96665f3fb6f4ac25246e44e0bd3627d875365626c8cb90e6b0667","rates":[[25,1,4800,0,0]],"sizes":[[80,40,1001617],[160,80,1046609]],"text_hash":"1838dafc24fd5742f240119096c27e94a7db2933b0ac5b16dfeaee45deb9dbfa"} diff --git a/wai/vector-conformance/expected/text/hebrew.json b/wai/vector-conformance/expected/text/hebrew.json new file mode 100644 index 000000000..10ecd96c6 --- /dev/null +++ b/wai/vector-conformance/expected/text/hebrew.json @@ -0,0 +1 @@ +{"blocks":["722c8daf25a3241f335fb4286ecdce0ab9363527bdd554d42ac0c07203f52066"],"code":"accepted","frames":[{"clip":"190c3cdd7a0106ee114bcf8af5c25a9246a7e672a580a42c279b33965c2bedf4","den":1,"frames":[[0,"7f1c9b3e8ec66c3c0e3caffbe5bf67a0c07d3ad12cbcc60533fbbcbbb9b875da"]],"h":48,"num":25,"w":128},{"clip":"09df67a0972e81f725138d0cf1080f894f9521139440dfa3e4e28a685322ff9a","den":1,"frames":[[0,"10c53c9b1f51c49ee1571c08cebade11e9111d36b331920725b3eaabe8e3b00a"]],"h":96,"num":25,"w":256}],"instr":"35819682ffa104f8255ec1d94704a904ee9020596eaf4bf4d41ad15ede1fe150","rates":[[25,1,4800,0,0]],"sizes":[[128,48,533026],[256,96,575728]],"text_hash":"4b411db5af2ae10ef53ec133e8f606736bf52de5ea1e8c0ee82905262e9e324d"} diff --git a/wai/vector-conformance/expected/text/latin-ascii.json b/wai/vector-conformance/expected/text/latin-ascii.json new file mode 100644 index 000000000..fe80f12bd --- /dev/null +++ b/wai/vector-conformance/expected/text/latin-ascii.json @@ -0,0 +1 @@ +{"blocks":["0a9f4b5d043f8433fa1bdcf9a7c364130caf6264fb47e0b3aa47cb9ad51709e4"],"code":"accepted","frames":[{"clip":"ce815393b148dae5dd44d50b731049d29c6895440f460c95a3a0f5f3f4a0cb3b","den":1,"frames":[[0,"dbcf4bc1b464c3acc88a0e7df30a03ce82d84e477e6fdeb9d8c790caa54ad570"]],"h":96,"num":25,"w":320},{"clip":"577bb19242156d0e2fe41bc6fb89f211cb875d71a7cd4f0d8f8dc87484421a28","den":1,"frames":[[0,"ba471588b8d826ca431dcfd36e7478e907ada1c4a57d14dad7a78233f2277e76"]],"h":192,"num":25,"w":640}],"instr":"f02dc158b0eb14ee9a3875132896c7c1cc5942ac8b71503e86d648b463f3e9ec","rates":[[25,1,4800,0,0]],"sizes":[[320,96,19235383],[640,192,19733375]],"text_hash":"e71f985f2c86cbaa9da7aa055e0a2f0ffba40b742875d87434bc8b24a7eb0d05"} diff --git a/wai/vector-conformance/expected/text/mixed-direction.json b/wai/vector-conformance/expected/text/mixed-direction.json new file mode 100644 index 000000000..abfdef08a --- /dev/null +++ b/wai/vector-conformance/expected/text/mixed-direction.json @@ -0,0 +1 @@ +{"blocks":["723651bdcd9ffacf349cc16afdba5c23242e275538a9a2d561ee291363d9d4fb"],"code":"accepted","frames":[{"clip":"93b1cee5a681d86ba49324b72f931a8f291e7b29e76126ff17c72d1ea8dfb38e","den":1,"frames":[[0,"5ed5b87c38b975b0714f4decf7214c264482f3914345370154fd457f12bede90"]],"h":48,"num":25,"w":224},{"clip":"a3a9cb32b42b5b14e19e4615a6c6fd1aaf817faffb8d1b6c20c0aa0d5b7f6989","den":1,"frames":[[0,"108753723c365bd3c9a997c5cb6e749be31ebb43640a201667fff0a831207442"]],"h":96,"num":25,"w":448}],"instr":"9585340e1aacba74a30fa8625042cbaa7e6cc3683549aa8fae7a01732bc46bfe","rates":[[25,1,4800,0,0]],"sizes":[[224,48,2301369],[448,96,2437421]],"text_hash":"179f28de48d5faea78fd52a432293517adfc39880257fa60fcdb0df966bfea9a"} diff --git a/wai/vector-conformance/expected/text/ruby.json b/wai/vector-conformance/expected/text/ruby.json new file mode 100644 index 000000000..bfa9c968a --- /dev/null +++ b/wai/vector-conformance/expected/text/ruby.json @@ -0,0 +1 @@ +{"blocks":["3fa00b9bbe6f4ce44ff3efa35063be19619e6a9d50491980625dab76495506ef","41597c7d8ebc421391be0391527a59f1ec4b450bcada03db3c699d1551611256"],"code":"accepted","frames":[{"clip":"85268003852f5e1e1be5a327762419452a60c7a5855ed26739520b4b90895dd9","den":1,"frames":[[0,"6d12cec6949e39ef8931d2e5c27370f54ca0965c828b1e97c04c52df7cf97441"]],"h":56,"num":25,"w":80},{"clip":"8ee93c3784dd8b6334eb26e0a815aad5caf5f3887bb74c47eaf302a84bdc5b1e","den":1,"frames":[[0,"d15d75d75ca2ed764bd30e988803fcf31ee9dbd0bde330412d303939e98e8efa"]],"h":112,"num":25,"w":160}],"instr":"e596f338add71735af28eb432cecf6aaa0abdf37f6a7a020fa24ae4303ec83ad","rates":[[25,1,4800,0,0]],"sizes":[[80,56,2260523],[160,112,2339549]],"text_hash":"0d1bb801ca26584e5c64aa7be45785b77f9892f1b40d015d87f5d5c48444235d"} diff --git a/wai/vector-conformance/expected/text/split-lines.json b/wai/vector-conformance/expected/text/split-lines.json new file mode 100644 index 000000000..6b02069d2 --- /dev/null +++ b/wai/vector-conformance/expected/text/split-lines.json @@ -0,0 +1 @@ +{"blocks":["16343f1f036bfa43f6aad1e946f8823155ea0dfd1fabcc25ac18f5987f700345"],"code":"accepted","frames":[{"clip":"10df3daaa6212dc347420f29f4316c9c5058a7ab4b32c6507cd4694461f4de4d","den":1,"frames":[[0,"73d7dd6ead9dc7c14e7a9324c5a7150fb68590a5cd52919dd3651aa6dbe5d470"]],"h":96,"num":25,"w":208},{"clip":"8d364cff370d46b17c2b3fd9d101aaa130272c271ab3e2dd205b8d9eaac98c2a","den":1,"frames":[[0,"f31e767207c61d1b5c3839667d684d7894d63d61dc8a0dc49246e55381fc3e65"]],"h":192,"num":25,"w":416}],"instr":"2782f335e8aac2308c789402ebb19f4025c125ebce9ff460ab26e5c90d3ca8d0","rates":[[25,1,4800,0,0]],"sizes":[[208,96,8058342],[416,192,8346462]],"text_hash":"5765fb1528390e801985580a1ea1cce579549efd67dabc6a0b26ef2c0256ffd9"} diff --git a/wai/vector-conformance/expected/text/vertical-rl.json b/wai/vector-conformance/expected/text/vertical-rl.json new file mode 100644 index 000000000..c87e0fc8c --- /dev/null +++ b/wai/vector-conformance/expected/text/vertical-rl.json @@ -0,0 +1 @@ +{"blocks":["47d74c9ccb8456e3f1171904dda15a84bf74897c9bcc12d9f4fef166730e8c1a"],"code":"accepted","frames":[{"clip":"1d559c2c4ffafa1e45cf5bd5651df602340cc8290607c49a82d7ffe929b23ed4","den":1,"frames":[[0,"d9b43774221ccc5c9d677d6a6d125fd772e95487cfdcae1a308b648fb4c6bd41"]],"h":96,"num":25,"w":48},{"clip":"2bb95d63e1008192572d721190163e09b92ed883d0b9633901c9dde608b1274d","den":1,"frames":[[0,"00e04f0f25e422f25d236f7c46b56b348e9ace34ae048675a8b96d50ecf1cfcf"]],"h":192,"num":25,"w":96}],"instr":"f6577d8e21273c47799a339501620e5484de4770abf1a981513ca2754f05e459","rates":[[25,1,4800,0,0]],"sizes":[[48,96,420615],[96,192,460590]],"text_hash":"c6bd18445aadbee0afa6756365837f765cf2e2da7d9ef2a9eb2caff4d5c76774"} diff --git a/wai/vector-conformance/sets/stroke-ascii.wos1 b/wai/vector-conformance/sets/stroke-ascii.wos1 new file mode 100644 index 0000000000000000000000000000000000000000..caf7d965eab2ec745384234e7912c6aeb288e7f1 GIT binary patch literal 105523 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νB7^iJFc zCU-+SwSkgYY!CsPHyt4lk!lsBltixvP%#EVpd6$!c8ytQI&tt;h83ZtVF)q}U}CGy z3hk&PwhLN+*DT`q9M=XgTOVONUYmE$GMj|&aF7xMvQ+^Yqkr)ftIt*$Mp9_=t!zMS z_%V+4v96C$W49V8uyxwT{pRI3CQEcA_>6Zz`MGRe&Y&^}6Cdx^c&AuyWYV~`RATG- zbuU+>o44RTjS};(u}{{3&q}WQIJ4?z@O!(i;tohO zz$W4Vx)LEQf<&gC7%wpjwHVymE?`Z=HJXi)xg|48EM&PaVVDSnz}JVYEykm#SYcRe zyEkAX=WMiOX3;-r7vGZ45o%#o>Oj6>yQYYyK?6zRh9c`Ca*)b2O;|KFs1>(m{84Na zF}GcjY0D<96+$8oL8m?-Wo_}4qkSb-SB+yIZ-LB~C`=3xk+Yw@@R$K4<)y~460FJH zup`94#~W&q`4V>mPpv#%HH!wXS5RJ!PagwEKQ)SCZ}wfSTeoI9CL~NarM^rp%cr6x zgxx(tp@;H(vcbUGQeLb|QIJP1HX3Ew9ceLVELvi$B2O*6TbmwKPG(agheTbw$dA8k zPyvx>nxp+^InG#nO4I18BV~Wefiqg01T|E}o^}Ci{UR)}>SDlB(Jo$uZtMkXtrIMs z`*_Ub)uXmHGB!vc?oEw`(NMC_p`8*L;k6f%4qCcOBuV5qFYNQX`kMVk3;VkIlEFQ) ztN)N?%lrK!7xwlp?CJ07=<~G?->@yetW#U?EBvKhJxgTnh@;eD&2D)|MX*GmX_2ob zdIqB<_7K)*klnJgsuDS+ZIo!J?1HFq9wijY`u$z~-5sq`Nj+1#dOCZ%dJgV5w4}J2X>4+^4^ll7FyjIbV)XYfh{LyxqcmxdPcw9-yUpKNH0`LxTVUFjO}{LBt6He4}Ck3kZf3^A(}=8K}6bAUVHiyIQ9NfI|&KED;xy}ge5Iw>n>1zt_h{Z zK(v)0nGpMQ*^tXbK$-gh!y_uZH8}MB%Ac4m5h#C$;F%Eo)fo|zv6G=6pQFErJGTPh} zq2rNI*oWq&FL*AjTXqeT;>aO+Z#J8*aW`s`Rh74} z!w_}aD{Qsq1v)l8RAlWe*0P??WI!32$RjShEgM*QB2i~COlb;5ohU_a&{+D(k1f6n z@`_-p%}3!74J#}hcVEu$$j67#o|&poY_wa*qzol~nCMoNV7v=XAJ z7##-8j%TqBxZXb-(V`_xLgPYY#j-4kH}mMKl!=vF2-3P3wfM3*z+yH2Jb@EUk*IGC zY!I2P)~hQf!G^OFp7D*xCR-aDZ3k+(64lVYb)*IBYEI*#37EI$C%n&;tc_xfhNg4X zhMvT`WwGa2GMVs2F?ZxdWjmqKJQ6gi@W|MxcSvTJTB$K1?b&JxdF`yJb7Kvch=Ds4 z`6j)(wJ)O<+Dcwj+9tH<%7d%Ux#FsUQhT9NnvUslj?+As=hVAQ+` zaVH0gnl)v47G!ot%g@*c$(+gNg5gXkUvpYgMq{TD8eo?)knPcY%*te&R%R8RiCEiN zL}4Z?Rdd-qj7dnvX4w?7Ff($14F=6D$w9lmu#E=XqA4pk7u4)5^ieHN zD)IMd?$?|gsd9QqzS-EKUtHTvJZ4n?R{Ff$&uI%;544!TKyyH z0ik{iAJk$lC+|lOMP$1kK@ddCg4lhItBlUVZhOQrXMm|$e>U{n+rf!`3I&14WW08-kGE)~r!%ofaR3!KxTeCV|v~HFX;eP6>DVRkzo} KE&J6r@xKAp+cb3m literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/sets/test-repertoire.wos1 b/wai/vector-conformance/sets/test-repertoire.wos1 new file mode 100644 index 0000000000000000000000000000000000000000..d8b36f1eb2bded7437ed53242d2487b084ded182 GIT binary patch literal 59879 zcmeFad$iZn_wPT~TCevmi9-3L(nS~D)NXgz{X!+BlFBXVCLyUPB+5oeLJ}3}rVC1U zk(4eO*YCUZd$>){ z$qr-UFZBEVKl68mj`nTB+Z%uWo)UGm4jIl`>398jMe;*Coa(IKv3^&N`zLqW$&^3C zSu_1!7{8v=(XPy`c!swEVsK$RB{vx=Sp)qZ^5^gRQ4wpZ-`h@=AC%Ke^qT$oyLx(1 z&Oz~K2ZP=8XZb#fO6Cqm_MLwBu|2s_#&CNo?W~3BPqSS)%~XDze$zgFXZnuzblTe& z`rXK~)7fE!8?WDQ{rS6aG{QZj-_Ps!!uVurg&X0XO$S@|=kM-*gnM4UFBH8-)+&`P z{wC`8&!_6QN)?toUeNDPPnB<#g5IQbWYH;pTcx_|_gl*Epx=v@YR^37S5kZ0gt2x&{2u%B_fh@+^3V1zjCbX% zQ2P%mKl>EET{+M{q~Fbj?>)OA{iDzHUy0tsfBt?wWcq(9zv(G@Rlon${*!9|2zTPY+kaB+AK~)E-@__BT>NK; z;g%Hsjeq{05)HR5HpuR?{`R`Hw6ry~=5~f_ZiDsTR{hO?6Rn3kY~@`a_pMU9*c98Z zIt%sJ1m!+#ADl`l=}0?%Ds|VN>A`KXGhKVP%ud>+>f0{DwdYjcdduhL+Fbiy<-w7( zX2RXXjdn@F=K5934RMRzd*Z2y>P@i!iF34#whbCzQPM^@fog6^o`Bj^&E?lbaD~lk zxPO>sxzc8JTt$h>RWsqKW!BO)Fl+0YXac&HW?fxtvm4zNW_?|Iv)f$nF`-$sy5Vw;0_b2TTDoMnb6;8B6O{ZWLFdMt4(ZlwwZa-Nn%Y?0Prqp z>&X3xa-UFRA9W9cevQNpYp_8E(Ji2pq{1_>pqI=h^+1 z%CnJ{NUBD`q&;HiChZBkAZbrq>7>*_`J~2F<)lrwOOhHTnMtXPT1i`BwUhR~)lJ%m zfZ9ReuovdTeoo4M_9IgEgDV)@x9;57j<^eA`==`#+gGkqYzJK?pgy1(X>G{8igMkk z(IZyfKC%7cZj0>?cUNpFcTa4QzdyDD{=wMJ@uOqAz&{dO8UJW(mHe34G6D4g%}8ra z?p2iQN{t(++uIAvtzP&BdSM>oMPpdlm9qQ8wv>$w8&ftatWMeJup(vIVIe?`$qs5( zb~uoyjL#(4@Ua9IHcE8i{X8L9nP-E;Vs$`xLtPQ(%zc+*=ew_Rtf>1u$If-1xsOPiIWxjZw+wrk-MW`XwsD@c2b+_{vSPK_zleI6`N zoA5nh!u*Jd#wZiDktVwLn>e`7#LI9KSEpuocF^#t9W)$k2hAP{Csu~Qas3id#&JgM zkBr?P7{%W)p1)ya|C2F(fYH8}asLT3U<-3$1G8c^^J6(PWg&BB4zuS!G>^n-N0S2R zY*GbRo0LLVlX|$;q$qAQsf=DG<#CHijoe{UC_|-(B?u{*Q6_csxJmJhH>sdW!XqXH zSO#q@U_wyFCG`^&JzM^}+rhC4ndw!&Qex-YI=pIA!XRrpG z#cEKLb)h6HM0wVVYJj8k%yGa;x+kW4@&qeB^rz7>z6OsDp zkw|^?V5B}89;uH8N9v>7BK1-4NPToeq(15rse7&jv;s6Dtq!@DP_801%24+Lu$%|J zv%!1@G!jtr(2W@-X~s*Ek&{FjO$s@&WT0tU%?aG^evj=ZoA(!=rW#V2{;fRA?^CgW zIy%3X>tU`nQ2sPdE4K^EO_+ z-%%!=VFy}IK!4KiBzG9)Mp7f2x=(`T8SqU6^JHke0=4PTodpN8;bjh7&GA7S4!=B4 zKqF$im+ZHB^}<%K{@Co*GaJ47=mW3bde5uh-uCLj#a?~+x>v8x_UhkPyn6ZtuRb5| z)%#<-#=%2?;ebJ;-AeAwl)H`^U8vh0EUm%U0?dt}(Ew_7p<4?Mvfw2Xt`^37Q_=$M zQ#RAurEI=kp3>sbJY~zQVandI+9~Pc>M2`m7pH8!l}<@N7fWe5DGbODFw#yYmHTs2 z<-Sd-##c$z-JcYePcaQXP73qpq-d;9+RL^kX)oA&7!&U#ZH%o<+C#PiLquQ@pg(Ch zlY1@YI#Z*geUP-C@l`3iA?}p2YvQgcyDGjuWmm*Er|h!0f6AK215?%{zB^?N;|Ef9 zX`G$XHf$_l0$>VhGs&G#xy962N!|CsqJ{pdcq^E9LSuKzdc~io?AG`|%I=B}rEGZo zRmvWU52tJlU_4+FX|Is`8s*-kMwV4hS*A5hNi$VTSygLETHTaXmPDLsO;T3II;Nz& zE=yT4>yol^WSnJcQAtw+1Amz_X;iNbakYn?sE^sk;;`?||=pFt3NkCa7(J?#FPj9bR_A z)lSO&X*gKwAy|703y2E1qclD3F(zkpFZkC8oxF+Ry=V@XeE z8ca>vSewiY7@L%V@p#e(+9OHX8xJR~n+;D|CmWVjqlYG~jop>B8a5zlmF@PVmF2I} zfDG%OlvWhTm8ZcD#Y+1Uusc?{9sIQ-R^8>Xuq=p$Z*DBi&&8tgY%FSHW6>QG!$B-w z?uy0LZL#?4AB)rOa%WiASloAt)s7CaTGS#|+nUE}Ws_L#Z5XTNwPMMjRxD{`#ga=V z<0Uhetg18gs>hOFb;e(2+yNjFXT{w#1mYfmUQEt=nVjmV=FHy~@uMkGo17BeSJd6{ z?38$!lM+{&y0^veF*!d-iRbmq%Uvn8W4F4SiTGUnMM|wano@heO{wL_Qj)<>DM{n^ zl;mRI&j6Lv7E`5|y0U@&R7k=vVcH&CN1b*}8O)A;Kg8OdMd$Xxy`M~3ruY_v@|verJx(X@XrN18z!j*7N4M-pF@Bl}J} zk;3+7j!g2`bJQOKC*>)#NQSTx>_MQvFu zx{G6RuqYNU3uAG$FcyCc<0w~Y1#^{qUarcO$W@Jta#goduCUa|6}~#T!hC72Xf(|g zwU)V}dwH%nXqziuuE-TvSLTX8f&75NrnG;Ua!Z@aRW#M8W~y7ugr$KAUlSANmL?jl zP1LS1(QR+ypo58*jwY_;Mw0~)1eO5a4VTJD2n{gu>tguV%CkM8MxK2cs^r<%p@Mc) zp>&@86pH29ui@N0`#qeQXE`C{S#C(?%HTTzI0iVJtF-;O%KbD~<+kRk#)e$geLq)N z-p&=i#ks=#X0B+=%@ws-xuQEgR~)>OD_&mC6<1So#h-vIUSuF}JXXfHv5FjuRd`>l zYP(_~*cuDf##l(##zMa;7NNIekz5vw_~KY>EQK3s&do# z>(!)P=H8YTb*q!s&TW(;bK7KKxLs1T?sJWMcPMGSoMynS?)#+O>3&MuJx()Vr29>N z5chl1o^mIpxB*iEvq*cL+&2M7oTkvHASmoLtqjo6>t_|Oj9y+vuJ)Q%kNGpTMtSMU z*SyxjDPE@48n4Ozwr`;|*=vp3?r)cF!gF%Kzoezt?*V+s&y2J(q@}j|+G!Q8m{zqO zX(8y87OGKcA-yLp^e?AH==rorE>4U1{Iu9ul@>!Q(_-!Yw4HQo)AqCbAT1BehP1pc zo6@%5Z52|tGp$|LOR+Xxuf(>`X2o)2z8>2Sb&zz^+p&FQYhv4MAI35Qw*&S74w811 z+@C1-2Q|{vjZ9e1WD|D|`?vE=ZrT!Nzi7f}>voZLZmxo?dMBH@u&ZS9eh8ESR3fbg zx%HgNHF2uZ%BgNUCoBzphgiby8CyGlU96^dk7W(ZXLJK#g1;=bmwb!ZUiD35o8cSB zw%eDE?Q?&jyiC4WESa7!?@{IcaMfW~Z%h z7?_qn=hn0h2sZ4{K#e}sy%8+kz}Fef?V-^YYL`Q| zB^Q8tqDs2E9&RE?xiYD7{k zS&@`Xok;4YZj{W`W?m|&exyd%kA%K%Btmr}k<5xjyhbE8szzd{VkFkeMq;u=BzB8L zV*Kn#O~@CiF)nVYIPB%dt?nAh&~z=6a?`g<%1z%nDK~w0 z-2XQv)eC)P6*w7{9o?O>6x@)c`t6>i-1PS*<)*(+R-_xjkP>*v3)~Bs;x(?u`3I7= z&_62oF#~vw*Dzbcp#IoT(ZKZ2C2hZ-pOkW&tnufUCv7i35Bp6rYrW>eVJ~%k*stTK zlOn8)Cqb{R#~PnMC6fP0iMR~XalQn4SN6-jzN9vG zfc{RPC!o74B{R=SE4OtQ$qaQBQr5`HgstPM%N%sIQdY$^NLdA^>0H)bp0ZL-yXX?` znv@lHH)!AE`T(TDi@BksjUe}7$~{SqXQ?|0EHVdY<9eRpvtoPJw+A$d<#D(+mdD}B z*dE0v{gm$-%O9a~WBg6A-Qx$v@=WxL<*gVJ+k^i0Sk8-~u^J;+b}#cuLI;4fk+t3*95seF`u>w)fne z*w(n$W9d3AS?k@h*mk*(V%zJs#nNWG0K4U^c3Q1|ci(ZKpd~Bqeu?coAIDYn; zIputY*`@w$tv!Itd?BqpUdvc7UqmOZfZG8>0rvo&BKJAUy+nKL2J)v+NB_l6i6y__-;|7i6Xs2pZId!-SL-2^!-y z!bolL!u{GWgnLrfAl$88Lbx$yT|)PiT^l-S_Z~EHZVxrJnF=*hHYij|*^r?5HZtS` z$VPfFB~s=1#Xwg996Muk~P7A69oNvf`t(V#F$8|ZMWlu@`vD`Joi zyEe2=imq0_j-jEnStyqjR~Jdcg$t$YLh+<{&d^fHxkiCtNwrAlWk>x_oM8Q!r}lm? zlhc2fCm9^VUnel%eG8OP#s>X{rB=Y-f88W1eoTue7@$id@gWWK9@NypYJ*?pC33apPQVP&uvc2 z=O<3f=WeIvbDz`l`GwQ+dB7#H6tsN)M{I__`i7$z2gt?f?qbNG6tXYjHs{(U?%7Usxs%po!Pd zOs|aFy^8ekDm>Av+T&gb-u6QErWexBywLCUBej$B59ituKPK0X`LWvU`KNO&&x;oy zp3Bwnem>XE1r!HdL|Wyb+?qk<>Ic)rHAS~^I@O2EsbM2aNSFYyKZMm9JeRA!l za6_&RHm^~Of{6DDmjN1+R)^f`l&eS$v3pIp04(Qf!yDA1is7F*RxW&*V;6>fIaVU< z$+7SK;vDh3Fh@(dHvI>@4ku^(r*mZBK9yrH`X_R165s}ZZ;sAzhs)34?~%j650b+G zV1TtwX)B#_mpGLh?o?wK;7%tj1Dx<>IbqIpqEW?(S_LP%znM7r)x^sWCa%6S@we8* z>H8+0-!XCjwq2-sX`OQIMY~!ZVApAeTF+da1x^CIB(v7k(<4n2aky#P4>yg7;TjqO z`8hb$8NvM=V~u3rGfIvEowVvK;R!hk>=$-a=)S6kJLCp-8`^3f*&#nemIlhheGV5&UK<&l;DT+op>om@WUx1 z`m`4KH@F@CS1q94KNp2un7LobX>1g78YMC?<%wbB{J|LdjnTB5arG%9OnznUs2Fv# z8GkbviBlPqlNp`2nZ{{9(}?YfXRHT@_Z3a!xvXhqUtk*J#Q^(bjr-4H&46vO=EC?` zvtkV3AwUb#E+w}%=I?sRwU(BfL^fV6aHRxEbQ$K|E3@C0^_rDMm( zYSBZ0_xQPxjAIlzK;iRE)y^;>XkbEB-GsEO34MFJBUjy{p6+epTcCA9wm|!YY=O=R*#g}XvIVYB$QJ04 zkS%a?LbgD!glvI63AMaWLi(>yLUW;4Vt=l3Kh@%u*qW;vGD*56WSn$PNKEY$i{%$h z$aHCz(44t6A#WI0)3hoa##ZH6wmJv2H94xS!)e;3 z9N#KfP^X(5>0ZeJV`t71uC>l79j$7Y+Mh$NL7XiP2Q+g6Z2%peGP<#my~(L?Kc{N9 zJ0TdtHuqj9q$8csKkP*4F}A}`I1zt}&GI-`OedpGtc`bKay<6Xcnr;Pn4C{xd_I90 z`WOc3!TnAUrf9lXLX+A#ortp5Oe$_ozD4b{Pk+m-th7gFZA*f zFZS{hFZ1#euf*-J+JBz3b@&-JVyq2gP+fmv6&+8-~{|B$V>bcJD@_5yu?u?5m%1XA7!H! zDQg_bOI$yai=k~aCuNsM+J0Rb?E!qq&kXr2qmvYom$-3MF|BIy5?7AoC5|F_i3>&Y z5`PusCH^VMOZ*Q0&doty;%9@r#501t#QlT3#65$&#McISi93Z2X}coGOWZ2ROWZif zOWfYeOWfMO5-Z?f7H$Gtj0@j7)EgJnjLmw0%Pmw0iIm-w9^FY($S2k|;UGF%obVn$x#Goq%kof*kX+%zgJ z??)sr@fDH0#8*b=%efNCOZ-42FY)L|UgGRXUgEitr1p9wFY&@iUgEc+*=gGv$xHlk zBroyKNM7Qdk-WtJjN~Og9LY<3B$Ai-7#(#il9%`xy>%p#m-uirI;C>*6zz=UR@xag zmH#S|mw0QGrMQepUgCw3M$_w&#@gIS10*|IrBl4Ta;UY8 z9+p;#YY^EZceI|@)9S*^%})NKHs%~cY^%Di^Js%$7bxE1^NRnA*}+rr6^Yt za2OAB`yelIn;>sqo1od#h9jmn95G!UG`B9}h^YlfOiejrYRC~&U5=P)a>P`XBc}2k zF_q$o=^q?16~bYfABU;oD?RB2jsuQ*r5*Ch-Ro6ur&o1OJrCv1N z@}l;p7v1?@9L)3LWsVnDeG=2;(o0;J*0OU}+Uh1ANn4}Du(Y|vk+c;^?A73pE{$zN zR3o;&(a2aaS$D@4qJ=sch~~xiV)$5&sIWyrQ$Yi`5n#2~&yT$_x_K4p>s9!7M>Q`5 z)16Sg;e@n@6Z%`62vu_;ndLVA%^-S#9F_^3Y}LZR_o*djk@O9 zx~P-Fv7+{Qb`>WBZK6VX);uaG$4B@k*FFTS2}iUGWT0yZ$bo+q2QpG0KH z5L1hWnA$d&P?|xe_VzKgybo~&eN56AWS69sTPLk@jR1|)s=G8TEbpd;PuhF0EMC#5 zFK?qs&DC_%qU3X0G7!+HlW#L9ZB0<_he72w0h&ju+a?m0t0LihFB0Z;k!XAriCVRU=w>CvLA``{ zc{Cxeo=k|prxW6Ib3#0CO^EyL3AN+5gj)1lLT&p!p;n$usJ;K&IPe*h7$wgzULI%U zNT2?ZkUsq-aW~`YPDa=O##&!S-OY?Yg<5=+xSlb29iy{5<5b{vz)aGnlKVX6#!+Jo zbw`2ae(>D`=DVPA2h;{Y_m+e>=$8;LbG$s2b2zG)?cdd5F~=9v{ToS%bGG^ydt;)1 zF)2U4PCcIV*PE7jyk&1oe07W-OX2Z)Ixat{+zn+me?GcIR8(HM8i`0&8ky>fcBJWYK88rysH+T-Ap7sywF$mB9!4pGU3x| zmj6R@(ETih>=ZQhqx(h&1@4~;YH$ZrI)+i?)K0fQCD-^qg+KvT0v3@rpWGRgn?j9= z)O{K(kApAU3G;(aH12m%S{}YL({{HzFD=)2aVbG}5ugeni!^a{w`)$hwrZg3sDpZU zjSO5}c9X(;MmC&Nv)*)5)AGG;2kelQ<~~c?_ikTWS4k4F0w*#sTnofj$Db7|wxU{W zBLLZ63;knWCo4OB%UBLrfdfPxdQYiN7(CI9(Zu=hcz8Ul*wz>m#-3yGU*OA?ld6 z8!|3+p5G$Erv@vn)~{y&0*uW&>UZOeF1Tav!7IL)5sBxVXS$Qp$ zxE`nB7oCn@bOwIW+4x20;un1bzvv?TqRa7%t}?A3>+o%E#>=?_f9GC2p9k@Q9>p7a z9B>j~g#70t_`eX>0M6$^KuN9!l;cuZW&EPm@r%~NFIo@3=%r3{8{-#kf?u>5e$i(5 z^qb+5Z{|vJo>C-NMb5&BpOEXyr4_{HPUZ;J&pAT+z5LnkNR9{{;@*zJ&k8wt%ksJ1 zIb!HDdD?WxPfQ9N1so(OZm%hK2Ohr71j?;5)m>%6vfPAkkqPq~CK_{1)MlIL&M z-Nef@6IcH^NuIi1dek9U#Q6u}j1gxBVethCi$4z|;R3?qOL>X29AWVnvk|M}CC*C- zi?8k_&P>AMGdcFm^e1)l=#Oh^_-~n3hjjet_rJ|0r!=phgb)-_p7eaEZdAe5qL8h0Z#u_N}B3 zIIVxx-@;|IvAUb#vpIX0Wz^nJ=3xEx*h;Y1*h|a50nBpxxtLS#Ax=de!lb**saj_z z1f`u&RpGK+Y(ihaiO}~ZlE0XUe`#XlpC*R(n^@at>o5^lrR_7!#!pOJ+K)|J+RgT= zREtfPrqT6YokK{|EYihd1@%86EozS`=uFO7T>{NcEBI9)8!&>jyUD$Sa{Z{$le*V} z<4_2*7Pr?23l!FCs;+f9jvkf80=<6ds#n4 zJJ=nuyjz1L2%8xzy!ADyW<}-OlSHj8Q@F3Dl?I$9m?W;4K^>Nyt&xrP(SbE5Ch`?D za#^N3yc*A~U0rUY*3&&N*T&?;5}3#gcmc4Nsc~wOe8XRrxiL}&kmXgb9$=VP-Fv;T zjADLddtqLUKjH(T1U4}>ws>(6GZ#V-S7!wAS0ISfIvn>m2;#m;P&=9hwdkgxw%r`m z%05Bu?HAPYdxKaDAT`mJ4154H;G%SJ)HS{qOeP@c34$t_B`v#F7vx(+OP zUig0Z!u+!rjUT+Iedk5@8zK;oc=3|ybt^d2mv_*VhU2 z04ExEI#JX8{Wb0$Cl2m);^jUku0}ZVCvX&SoU}j4jVYJmRimI+-SfP#T;PSTlo#f5 zUNkQDqE^L=?j>FvRQKW~(~GP3gA~9iC3spD{AEcE)F3iX6!UB;N&9k^&cs92S3v;`$aHK`EaHK`^?C7}GfJlm> zKqMWJFVdnJA}yNfur(IGjj=Fm(L5HkXdVe#G`}QjXm`+}xgA~dA>-xrr2GFj>592F zX`Py@Q~I<1>OOC*h;xDCu|)iBEMXstCH8%>I$&3$5_F}VHZ!pE}ra!>Q(IG z*(`kXv5OaA7b`4zas^}`xI|hcor2QYx;^OfboS(O<+^ew2|5$#3FuB*Cvw|Tt_3w3QMV3QYJjf_ zm@7b|EYwOtw*(v%hnHe-wF2$E0zJMAjlLA!z6dS90DV6n%|Dwp;8j+G$*c<#SRux- zRy@Y4@gVERa8{B*tSS8gJptWG>qKr_%C(?IBkI-xOAYW<0doatl!aO;=$3$k;_y-o zu8P55F;N>P=R;!w)D}T^DI6?=mlbf8AAk1WVtD8G8rg*b z#kjs(n(Mn4V~15|pHiEBN<**V-JE^O<=A2E*r!~@KBY7Jlxx_hbj29!&OW8PSG&8j zPwDD4FR#Hi>&!moD)uSu*r#01KBYPPl!ok6YO_zN&OYU0ueo2^OA8d^2&^zdT%xCK z1oUqSd);MNo6Fhjt~3!}#a?#}d)*D}bw6USyF&)BVtd5+0rt9Iv)BEBz3vJ2x?F~} zi0NDqP=q;Mf;nA|IbDT0oynZ8$DD4=oNnoFTMnU__Vq2-vXC788En7;*?$q^uF0w+vq zKbUgAHkCVIsgKIn#yN)0uhH zi5b;_xz&zY)|&a&f|=KtIarU`n901X!VE3PTrI(@EyDaQ$V`p^iWWz(N>khy!dAHh z`k(ky-5m<55`V(CLFdTgPc&AUsIAmljrbD>%e3Pcf8uJ1PHhAM_>UEwAupxup? zN7LyZ?o#&v{%4)&EN}yHLid-0)9KLXRd;<#%V*b=7Sc|HxhP~^78FKIYm68L#td?; zFk%FH0=kpdiQKl7Ye9`h)U5-S8sMt}<_gd#3$;?vEdd9`;iVW{6@$NGZVG8L$(>KR z#nf0y-S@$=5qw+0yb~I`q4qg+55U18c=-yh4#S_oAi!|a9whfM%8jGO1nN!(%d6m< z4d(gKSOB#}&|L}#%iv`NT+Q*~Z;ltIvwe0-r#Iq$x>q}1@oLdzueMF}YUMLt?S0a# z<=I{`80jSqy|BRvktEdLOIAI7E6$GuIs)2|)|_0u*ujaDYE+|c1+bI`UvV%Og~mBh zI~%%Z!a)Ie$q!fg;V-`zrv>~>a>f1GUhO!?t3^e<+E(1Fm8HGfTfwX4)x2bo< zUUF&fC80K6vg+s$!=FG;K!4KiBzG9)Mp7f2x=(`T8SqU6^JHke0=4PTodpN8;bjh7 zo!-L#1-*HH^)38qrFB|q{ohntr(Ga_6BkG`tcPaU5lyft8e>~rij`3hd!rVXM|EtF z%2*@iuuDo}p`4GcqH|N-=)-<7ESZyf-c@I90!IM{i7MX9<2X8p)d@Z?NUbx~U1h?u z+=OqD3G*8!8gqGKXSRv%4Bk1J&Wk70Ok7RVo2dd?2dCRia#!vc+O?9lBT39#rux>8Jx@nAc(*)~Ak;ZzT2J0acJEA%k#UC5r->UjEBH|j~7Nv^n`NIPDnMN?_pbXxfu?R|rmFF^(?k;WS2 zvIz-oM^<~V2@e8}0*;gR2e~ojGO%t6V%?mFb#npMO(|4RIjozDv2Lnh-CTloQyuFj z6YJ)*3+c2A>96KO+KjW`V8Il30mr7^wnlG*Lrl=YP8u3^x7gc+)!^#@=C;td3~DW)+Y}C(z)K^zY6O3c z(1}gZiA~XoEzpUVp%dGp6FZ<2uSO?ci%#r`PV9$HyaSzhH#%_yIx!nfI2JGgFom=k z*QTT4^guION)j52{>w%ajsOe=+)i3wa&M$uS88;mZdWYoHUe(1~@>iMkC97PYb^I14hbNm)QWL>&fG%kMSsd$)Qz}NgNUgxLqKR=Eq`cZt+ z596I4ji34vG`f0 zaPn6yjaW|LQ8E4aT#yu z%oI((z=p-btU#98HUI>DV#PJzq(?LCy4c3)T{y*gT&;I7tfQ`Z=pjcNWbO4L2B0UP zJ87NBZA-Zp)M!NAI$)^*zA9j@0FAOxD+S#PZIrHq*jtLr&@r66_v()fTNkVQ)FRH< z1Wx^v3*Bg7hR~{<-0x@LwLeo2s0oWCR7&9}!u+_-g>=zZ1>RL)v>s=dTYr-}ibwPB z*CizzrMtVj@LNKsGJq8d;<5K+wOKrgy?0$%arWtJCL^S$`{ z#Ea8CUOa!{#eJSvJ7TXE`JlFipjH+QYVSWd?=Hqsckv);lnH0(G@SF9J(0jcz|lyD z7C%KgwD==Z;WSV0MhVq8GoeF^a}qkVI6tA|kP-{c^<(43kVBX z8YF{vk>6T^1~wti9Rw2WA*A3S!3F>RM~uZ^eRNZOc&-cgb!bNxEviA=s?y4ew6`oR zFM$k-AdRz;OFkqN2g&MWkkpO`$?quOAYcz^JILJ>RBmlhjdz3UE)Bx6APC>QAk4FZ zXuKLk?PX$)zxP?=8;rmU6PUk~6;5oc68f z+;0mffICr#dpQ+6#QES+P704RCQkwkndV2jg^=^90bB~%m!}u1(jPVHnY#2*Lwc(z z{nmmWyo|oQoL+51|F)s0+tBB_8kJUtxW9~cw4g;zXA?mepSPR(fGx>P6!%FKTZRu05Y{ z?Rj3j=(?lccuQ!-IlcCrUVHxES$p;;G{p}CjwLkDe@$qf=Ov^Me1_(E{tV6YLK&Lp zMKd(di)Uz_m(I{UFPEWtUNJ-SymE%-d6f*!^J*EI=hZSa&#Ps~0;rN9>#K5xY?_K0 zvdGG1$TlmTAuFwT##X(Fl_ATmP=;){{28(ye1^=Pyo8L2UlX$RjwNLE9cDJJL;0@5 zFkDC6*9IcMHWCZAnW(U>k=WQlq}XS?NdGx+(;te&?or;W|B>bMHx|+qOKL;|3)0jg zfRcdnq*bNSwP^IE3Ds!Eg4>z}w>=APXBOOUEV$RR;Pzm_y_p5K7YlA57Ti88xP4e~ z`>^2lN=TvH%!1p41^0Ru+-@wmomp_(v*5O7!EMHZduc+ds#Zcut7<~(t9(L=tYkte ztq7nXAR;Zrg8LiG$&V~7M_FPHvDkdhvhx`W&<>WOtt?8LS)MksP;FqzTF2tG3+4Og zM-#;JXQ<}gDCvEu>n~8;hfv{1Q0CvG)_)F?!AVqo8l|5=l4l{wBA6*9F<8n2ssU<~ z){xwml)D1iccd4((H}R`Grj1eTj;IZ=(mCN;9d0PV0v{Z{X3MN9!j4Nh3CO=e;4f- zNQ-WxZMV?MUbOc{THXyAbVM3gAeWX%s3EedjhRvngQYy6B%lasXOWwrT$+XDBumWC zEH>Y>>>ObMI>b`+1&h)?mZ#k;RG+bA?PBpd%z}HE1@|z4F-KT%zhS}sjs^Dz7Tlj% zaDQjP&11oJNHRZ?JR3%jg*31lCd>c2l6E3eZu7>8O9uQ5Vut7tv7{ z(@|CFsOof7O*$%zj;c*Z)uy9r(^0kQs4P0FCLL9sj;cyWT}($^L`PjnNBsj47e?3x z5PP5l^00kQUQy>wDjoF_9rXeo zHIa^*Ku68wD%ebdp=J^mHQR~%*9euG=hUJ%2%mb3;7GlVX7&!jRci>d`jCLDZG>KZ zN)Xl;gkv2hFzc8@et;w(LXu}9$@8#9i({3R!9KkhOZ5__P!^M^9@DBJ6RZhStvQpf z1=Ft;6R{NosTD)26+E|q`{uNx2`y?!+v?HEEZTbsEx#BUltCKBk;{2V=uBi4A+;oN zUnc;^0EeB@zHrL@)T!Jyry3t}V{VNTmUo=+Epx*BmJ^LPoT$xnqWhW?2eX}cnaRz% zCHmHzfd2jU!`{Sc7kKUr_nl}*2U^sQwzZ~}Eog6JT3!zsWFn0!$fX<-DuJwuAhm+X zF9PU_u4HOT`#~=nDZESN4w!1}G1c8+!t#;cHc~*CFt0JuSY@KN(nNQ;iGyV(UY3}+ zI`w6xzlF;94}JPbM%Ku|&UjXU_Z?->G?Y9W&~B=9V7R!$+q1)|Z(#fH`<4v+-``OvwJnOdj+$5A=+^c zU>aZ&Y0r@RIORrDV+3{Y2Fsn`8vy3M(6||DJ)nC%99#!4-Qj95dU7y&axi*wD0=d4 z^yF~#?lTV{3C!!~(peJXbC+DLl7ojItq8--)HUoB&wwK&Plsig| zAF2BrSn|MUNHT#W&p?s|kz^qxsaF&A&K;6G7fGIrB+rHC!f;=Rb`+#VXVA6;tu)%3 z$L#)%+5IE4`zW*f5VLzPvwJ7AdovSzEnp>J5oz6ZcQ!I(^pEqUQ+@ev$CtmkAG^L3r?7!h;v$ zV_b%xbrp`r^@Im+Av}1ONq+kQhXKb(`<2{0%6TN2A4wKMl0}hZar&qAqL2*DB=reV)(#P4##3Q1TSK=TGYvonIcyN&W_}4oQPt;n&9+C0}p{ zb*~=C;8oz`?i=}^-7y{$7I=uC^5BvHJg%_8~Ft})a+SAH%<;Uds53Wa+${c%SrY~+QtLM03IUkUUKiE+yH9y zq3(@f=?1>eU~UhMwotnqx|iA8+P~SiGTQ7%ErHw~t*-BtS|wUk$ua?weI;uKPy?&$ zvn(oizn&Vg(GrtAMzohCbh)j>5x+(kTXmB~@8+GrVV@Vv4X$xERo`4upI2v~YhdoR ztNC<*z(Fx*N9=kz)7=n8>|ohVPDc<`+%Qfl9+l#DkD!qrLn8?+a++9*?=>c!P3Ui{^GW<(Iri6HLF zu>f5h)S@auZL1p8%Em$MZ5q__We77DsPo8Y~)ae=Z@0?MSFhd{V!1n>b-JJRkU%AxzMVPsy^>Sk)+|pgIQ4D9 zsc%D0ed}}TTaQ!UI^0vLgPl_ci>Ds8PkpSQhS)<*u#8$@Beh0-U5N$S8C&#PtkRpX zPy1o14#Z}?8|(D}?AS*DlY+oBz#RNE3-Hq{XZ!yi+y8ZJ|33;swS(>dXKepJXZ!yp z+yAfF{(sH(|41mzX_Z(z8pPz$Aa;*n%YTh6{}s0Um)P>3W6OVrEx!X*`VqGLI&As( zu;rIy%P+u|pMxzw4I%0l9gl4b>HqULF(rrgm6UOGTV^q4zs84mvR+IxsfcF-lr9URp46 z8Z(CKF`6sCHXxBY-vh+(E`0PDS3~ZI+ju zs?Byn@Prep=bex~;Dr8BCqhG=NRD(OKGcbgd-yrniM7G5CqJ8#(Vilg@Rxo*>MjC- zEbiy|YRVH%uer?Tb1E;nb0O&=o)0!s`0HZ137l;7eSnysKap{sSCNEQ;fh|>iu2u; zy1YeGl@GeKWcPHb7ojV?NVedmnk&87xPqS@yjbhVH)J{?ur6LtUUu^%VtEKgE850C z&RdvI%c}Pi)DHim0&@IRc3#tXCrg4D=V$WN&Fom;MG=?{c$2iH2fUiU&;PD{l>Z5* z$@kjP`)!hy-@^N5>-m=28ob1>Da6Ci(w@^#!$GW9a_{qxr}cf8N7H(McC=1v{6oBc zJ4h$}{!Sfo;Yn!fZ<9OF->gkA;4Hi;X9AA68?>DR>;o)ii@X>x->KXzz$mA>53!3L z?u2iM6XwoNG&(v_yTXa?Q$}; zps82gW?op@c;RdBg?S*h&mb>q_h1U$=f%N9FJ4~s;_4MI{$BOs^ldMm-}T~t4UZGA z^=i>BueR;R7~6+!cEGFUM;VFVpcIaJ$>lpQ3H``~_}|9gF@HIuQ`6&WY0pmz}jNAt)H=G)Sse2n(`hu?~ zn6HOMH>h=iZYMbC2roOWi)<;yfC(G~9MuCd@^DB`{^1l?A3NKqU9l8sL2uioAow1E zmuI4sSD))Ko})TL(1*gSc*USy;??KXy{7=L1SPrB!gsy~L?pZKoQSI@N9N zgr&6;zAK$DcXFa}jT5!*PIRw#;@}1+UV1oj)x&+|r2o331T`E7I}OZ1PEEKI{dc=l zvu<^2U|*-E_H;DbsrlD938I6ONZL3Fr#WAjYJg(QauQt?Cm~)0C=NK6v@^*KluMdw z{BElI6YooX%Op8$!u+L)#^<;;_L%5?%Co7vOuU?$E;nF?o_c_159)tAFO%s*cq{}O zFZ!^L?*6+~DCD(rM7fQ;3cQtvR%PnG=w$RxbcbSvlzywwPIo+3(B4Ua$@k!7FhA>L zIzNXWZlaSp{UQ@*DsyH!{yKf8hw~^I;7jo4t;DaV%LKf8q*<{Y|LLCpo-;RK#`gi- zjx9d~TmF7*`O(<&W3c6)4np-jw){)j@~>da&%lxaxK`&HDxE)keyszc5*e@$yH@1SDu|*DRy%IU?*3Iom_r)a>h&lBz`KCwI!U+E;hK#ezaH-6Et@Q{9huXHC~(}nm?-^Y_Wl}+`0 zz!-i`BI6#4JVN1qRJ#iVT|w0gr0qc81wyTyNL~r?=1y$1w(F>U?~vQvvVub0HZ!De?h@zoOcy z4_*HPsv{sxf&K)9B0j5~gm^wLHWK{I@5Nev{FnvsXr760Q;!2Yy5m<=y;;A#%u)j9O_#~f89Id>K&#)4Y_X-qw z$nVfcv+%AC$GbYruT}81pOe;CT;;v&ig&dO-qi+pS8L;4t&VrKI^NYncvth|UA2G3 zX~{!+0AJ~Dz{mJcxBbhBDxd019II0S_mOrl;4aDy0JOoo+6M1xGrX$}@U9leyILIY zY7xAvh48MPF!Az>iK}D!?xoyb;&g|J=k0{ye1v!PLsN@Z5CpRv@9Gk~s|)o>Nt)ilC?rr}+kX6ob_+%2794^r+iYK-HrXEpeFr+f+C)#Z3sSLh>@ zqV~DYL-DR|w{PS&wD06pG+iG!W?JcfG5Il1y<%Pn?`jd8M8)y07ROW60PkuuysK>- zeo?%u18}U~1-KVa(S3lac#38M<^#5o_Ay{L$IXZ1U3~!W>O)>F8t>J%XYj60z`Ht$gYX$%GMJ8c zbq3zmS)9tu;%i!Sm>PeMzYdJa%gJa;VY%gA!QVL<&*wyZpwHk9eGY&Qas4H!$>d~)?6yoqilHTF>VAXvTu-%-vwB8_MC=_*$ka1Liwm37E~ zPTcQm#r98EU%pp#@o_x8f4CNS<44icgYm{o-*56yao)2MXZm~=da1p0!Wtb}zYV{&E|DX6TKM zxRJZ?(j~LG&pyrC$(xK%`Z;~sSAmccUA9Sx4_HG2>KZ7@5zrTKBWYd9?LfKK)M!rK zOTkhbd^Nya1sWBhR?cqGtK;?pKpjxPrgIm5uGTHStDx{)z+u2KzzKj_nNKKGu7-l_ zOkY1Z3*nq4Qo?taB20NVpo;FhE6DF51>WhcAB7w)&=o4(`PZi$R+_%p@D%~E0?#R+ zQX!1rDJWQ9uu$aVnNHz=XFEF}g{@w=*y-uj%DjnM)k&}EKKo$Tz)7#^Qu_mdjjox~ z;|l^i042EXUJ6jrtK7xhfFI~pcc>SZ;a>PwdSQOgi^h5{Y8$-hS`Y_`AYKXvadj4; zQ4puigLu9?i2K$-?HCZ$qJcqe8ywWiyMx;MWKheW43fdPAZa`uB$sD`Bs3{VR+9)I zoD?L#NkNjFg!T0d-+g#GNY3MeB>rTO?4Jzkg}a0LV{lN<3=Ha{0inCPF;vBT(`0HM zq@@~#QtGXsMX4Yl5foe+`yU7=|Bib~nvxf@{buC}!J_ zBTD`m-5=ybRZ)1J<}04H2%sdOJZV+=(p4=!Q*mifjb?nhqID3K_CfeM2Vw3OMC1CP z_Vx(<6apXA-d>@bf@H&00CCkPG$pMbxzdh3g7oC|K^k+W)7_z&Zix%)XCd)~}oz0v6?^g3s^xo*z-xUQUt>Gg&#fGYv5 zNNY@P9m-XwMkVT&1xpF=oe$=7pm7$|3P3j>9Av;t1XmIKMecRdmXP}nF`mCSew3LhcabE3s(yK*}d9`h{S1TXzYVRGXauND+9l*xpj;_x6r=7rU^x?f8DMtM&_2akF7o>W z`TdUkP9VP%$nON4{tnN7zfrx(}1<>J~fG*lP~uhOjG`1>#Xw(IGoh5m(dJ+Er%<7zjCARM?QV|Eg!-BnoED$w&ZhPYM15? z=$5JO;MsA&QNTg%sTAv{+#T$^HuDvXb$kV56<@(vu8po9B@j4^=lvpb!r>6;ciq4{sfkukVAg|hu@t$9nUWx%IPlyDcG{|U*^j|`_Vc3(LVdoLwnIk zpP`#RK}&s%zS@lDT8|D}jW%0>UR#8Qn~$!Wf!3RX{+oa%91F+>j3Di9a_^vAKWg-( z?zLdK8hjnV+!h*_L9GRJo5Dd8cxePzjo_~lIFEbYfd{Vh42M)#$`) z(TP3LiT%)tcc2sRMkkIyCuXAw#{wn*rjRy+-1(GSM2!{HT@9A?;M)x5kD>7i)INjm zUO3nXFZk-BZc(h_`4z}x^Db)c37-5PLE9bT%zRWzN{LzvwjTadrBh!*?_U6JxA_j z%1x)n9O^Cr%Tn;I1oLWWd;ql#(A@+Fo8e^(Tpi}>`C+b}ALjDJmz!Sb{T-xDUxkCMS#Oj%xyvJe8yB$e#NV!K z*B5XByNtg_&w2XcrX^aSKHxU)V-N9iq7Dbl;Vs*FzBf+%o_hbl_YgJTM%28h9rG=D z)v+0`k6xr(0ilfUAcPWfI1|2ndHCheQ|@-0a(Cc!yZt!KzJ)8>131UN182yHFbbe! zrw76?1>A>0d{DPJpcz1&oXx%MZ0>E3=HB)w?ro3c-uC_6+aAWf?ZMpJzJq()w{UN} zC-=6m{?(~%CspNlH$szy{=S2cL+?-Z_+F5;j_pVp3e&4!% zYWAzyw_mLW4QltPSGQihTA6+8^yyPSt6snQb$a(}P^Wg*%{6<~>sPaW-MTev)~Z{e z_niap={tC6-@%QC4e3|&lD@+S-8^u}t#=NrQM179xAyKkuy^0a%?I7wyMNzHvZ`nP zSJHn`@7+%{Yd7fCpl-jqwR-ibRkL@k-nFxO)#+WkUd>+p>eZ=Rzh>>sI<@-NuU)Tq z&CGta`u57qtW~>ayMTNG{c%AMEQqF74g)_-ohtkmwC#$~M*7q#2fzQGom?0Tl( zSlgfT%H93$;>C*>N$r1h+hyyv*;$^&pI+KecfWi2_B)oX(D>QLw{@{>{`~UWZ(mH$ zzWL*e>B~R7;2-vR_UxJat^Y!NxpxY;#(!=7-+isvbcEv6{WaTm89x5~ix=N~@uB;+ z-F^T3`NPA*EVD44RrUJ0MRwzx=O*3l#r01&?)>tNdwq5JW_~-ry1a33uddzQ&BfGR zU%bD*dUN+$Ur%k-oB7S{_56oBePbKEoVhoCRcs5Hw3#K9CsCt`BU#JRZG5H(#cAFg zvd!hCJUnGF&^1Ijfl^m_XK_D`ie;?Liv}|3Sv$OTudc{hrWD2e}! zv_&I9kb6@!0)iN4Aqgzlk3#LnD#XMEo_f^_DvZ-uBbr4Jf{B?CyW$~ z(mqm?wAmqL?kW>5s<|c-(1EWH6i$YVno{$E(=b=a-Hp!+%Hw`FR{KL$e>?((2fkEj z#wtl14=F(1k81P@5KI@<+GLcs>{C{h*`iGOFaO72!4;wjx%`a^WKLLG}-XC190~vcOa>Jlq!m z4pPRQa29+?>0bL!q$2sJD8@7vP<-%MxC+4>fFinioK0~vRXO7!X8nxAs*;4kS{$)1 zwwxjj4}E@&$8k3UEzH|{^s)J3>MNR~?`->6HEO@fic}0555=}fQC1n=ZJ=7^Ufl7c z8E!$eC{DK7#$&ijMq*#;y7-WaL<)WV&>QtgD1NPdwHmzsTIDO&_$NJnXpd3KMnmYl z-heUT^vr=A3ZDanvlz}J6G=NsD~h>EPc(c<_#w7r07`m=@5+7LrRf$xWW>ne9rjXm znH!AF8E=nf!l)8;NIa`7$;-7R=hv#mn5YPq?Rdre!o$H8w=Y5Nx&n-+4lgV+7>P2fmMylZx7k2*LVa;!<4LLzO1+_E zC&+)FECvXq)&WvKDn)Ut!!xkJAQM%tAePqzbknlisAM6U5h&N}o7Qjo+9wF7=5|0J zO^(&bxFSS}DqmnLN(@tWY;*-lf{|W1MS+|hxs~h&$6a=mc4V^?H-?vdO7S~&)ZVC} z|D4Lzp{@z*P^H-w%dYAgl6RBU9|e7|S~jc2j^aMKZ;JKSu%A$rk97Xik*HEJxdcTr zH_p}Bq$mCT*N^yhueKCZto$bB85>aeNK<)msJlm)g(se1lirXVp~vS$1qs;3|Fa{^ zumhn(2P>4u8X~--^b0`r`Yhp(Ra>74k zwr&9|t{mhLEn#>&uiiDmD5apxA1Ivk&UCHj`38;k@eI~eulcjC_F38P&sMePQvtF2 zR4oPMeMY2Sr3|g64ouw^Pf(CFTscO%0nT^bf@iTJfx0U*0=@qRnM0|?+HrrCvr*qR zrultMFtU7vt@xy;G_jl;nF$X1!^lDJ@guJK|L>g>oa%QZR^w$h}c>Ujo04{ab7ZeL62WrpJLmi)}ScX;fRf9fgAMmG0 zEnWX4mN;QFm6goDgl4P*{$M0Ms|Vf~^E`SW=0NOlW3?sz1IarSE*|k9SETwqbMuw5 zGwxGt8|DIC+iPBen(H7!wuVeG@e;_15YTF#3qF)BR&JG3S%JwsJxe83adxLmh)wZn~`-%2=$41fP!yk?iyY` zOonC#p_^W{}#(rGD>lEY#pT7;t1R8|4cyRE(fGCB-xjcOmG`p zAv4GjS{v_eREe=ldLlU<&qQ+ieX6r3*=f+UBhuDtlO$f~Hz%uo(F1x$`-B9~D}5FRkeV{5RH8DA zkc@=T#*Y;}G+KTt&`JV``7pl$x-%%;)$RQC#e`ApXU6a3lj=@9LzH`TPH!k+aRdR6 zj}F4iN&N#9CHytoDoTI=wa;?vF^8Z-geS}@P{Is8A8iv^n}K#v#8jaF?iukH?K;}# zX$zaZ9xG?T2KbgfBLH>tA~%NrVu}sf3NfOfeV@o>=Aw*)tp^K@)f#!=Y2QY@_2WU1 zr5Y)D4hK-KSPeL%lDQ7Z_4NK#o$9`Jil=c)6S$rmrd**mA4MNlQvQu~G6F#d7+{=Q zQ0Hd~GiC=iQEK=<^En!32eI&3J#)9HC$9^?j?Fysw@tG{D=p?}#h0{hv?I<9Wh2XTK@DvZS8Ah(9Ep-dj~03yEKg;EFb3;4Q25bCD&A_G9fvXo>o$Q< z`NN3Ob($PzDAK!gw2fh!Wdr+ImyQ`mqW895+O&$ENd^Y~x`VVrlwg8_@AI}&9 zQ&|s8)Z=ELtv0L=1V97z(HME;3YF(r{&o9Yt` z%u-P>haO0y7JamhAl58+;=|P9O!V736QN6^%CmQt~xPlgd&vb9Y;&>5Y|5ZnZIIq4w8$i$RE_fb<_fPq7#c zvj!Uc<K3rCaAVpS=oU1K5VGh?0TZ@h`@v+h zD92^h%B=nEjig~X8(L;H{cCH!DX>AbrlTA%8@8Szq`}A~@u0vi!hjTNnnE;N({Wqi z@5NCFZtKXnvRTq0Bsi2fgF$MpcPXEd66`TCP)`XV@d#EOKs?p1J%-mxS&BeX=R!lYKQ+oy*fPT7=j2=7{xAJ>gaK!B$I z=Kh(%nR-tu4XZxDKN;Zc`y}W^B|NQxwYi8|tN{khi`Kj#Zk&Qm2ZzPn*Tg(_k6dl! z_Fh8NruShmrS3VnDS?FdKY$Lju0RryKRLf~^P7qL;rwQPlZ^Xee*5Fq-L3m@etms@ zc{{(jaood?uH~=Zas~gh``i5T4RnWyLSeF-s{|aD0F5S}7Cjefi4(#hgY3%AN~Lg$ zYm{IrYly~qlsvR@xAWU~7gI{o<=Oo5?e+ZfFBd;uT;5LIPZ!s(e?-djALj4ow?A75 zy`A4)UfhJ&2hQUj@6WFxn|JTduig8*>-SeT7opm!_iwoM0F{t`hiwXlk5E1L$~fLA zu&y_iOgvXtJ8ocz22*%NbZH4s5QmA-r?9c6M?T6J+-?+?X)0eA%|~FpX)11S(;P4q ziFnUy_rM@Rf6Qndw-k}B8>P$>rd>xD|r8eewE;xZ^==|yDpS7$r1M8EBbjV zG|r>bPOeWE+5{iBOx#NM&SOBbE(_S#(w|O^8Zm;V8%OJYgp+pZ0_cAw-cJHi&`iH9 z7ItlE!mTxr=Sn0+{HAp12cFQYoTKtgFq}RmC8efn40?L6>}Buy!lTgRm0m+KZoy}1Xkq>6O%UAz=p})%J;K4hA#fCjBIxG-9+G;{ z=!fZ9&gCYKj+WrH5AxC?T3^}IQL&H#f10`TrAC*^yM(-w`+Q(_qonw7uWs`&U85~} zWaZ%_9ww;cu5fDfLN#`IU#WK{v~1Fu3@Xcni8#D%Z zabz&FXEkg*=x1-V(89!hs2avP)t1D|JgJ(#7LX1wYVzuGK+S4gp7seT5_B7dL7=TR z)g?&8;Vj{0-)NgvZyT-yX;;D%+*^k#IFvc;LIQ4#ek%4Au)P(d7^HJ&ub#x)vN$;g zll3+xm>q_w(3Asu#BP-EKsE-2pk18O2cgch$C7&$jiWxJmn8)71#iAlSD$(f&WQH; z`}ZuF|Ihbwq3j_B5xU32SNHUriK3(qd(%hX{)-P_PJ4~Q2u43mh)OOhvZiLT0PPIz zXPiMQXI6zTb6%08+LIABI$!9P(!)I_zxkH*l3u85(!ZJUJjKqejb(WxQQVZxzg;x^Uqcmf*zs# z)5_1>2OcaNT~LM98VxdmNhoeJE8I%l0IZ_7hKYe@f<=TqiJq=jkt&oM;hISmC292F z1Wp0N%o%{K{X{Lfq!}99U~m%?mRtCqXQ>|b?@6VB3Ax`7IkI|r&ulg_`q%wCzQ64G z?^pMW$&H-DS$5z5tHoVJj>H=z(_y?#-QR~7LfwfL^zU4rzLJJSqxB;Kg3uPk@wrB2 zx^2QmDmi3?-tCRpmHIYs&6J=(cv~%O9yy*9_X`Q#G$eMv@yh8t=8l&)7xp@}M2v0f zR~UZLsZ literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/sha256.json b/wai/vector-conformance/sha256.json index d04ddf408..5bec2b968 100644 --- a/wai/vector-conformance/sha256.json +++ b/wai/vector-conformance/sha256.json @@ -64,5 +64,26 @@ "slicing/piece-1@160x90@25/1":{"buffer":"RGBA8P, 160 x 90, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 10..=19 at 25/1 fps, in order","frames_sha256":["cff9d4a7f232431f658ede7ba41e6e7d7a962e95d33196157319e772d79c6a58","a06b510d6d22c4a092d79f7c86dfa672ad3330e0c266ed2ce7d83a4226cc706b","fe873cb1801a22a26a96a228f29d0ec2145d73ea75c6f9a1834c8c4e1ef97162","1fbcfd349eb22d8f2711d1a2e9982c1a4603cbdd612f2c8731f7c6b38d13491b","a1ab5ef6bafc069cfa4c3e09614973bd84cb8791630747446186a83833f0f975","597c55503577383c39f9a112c0551870d41a5513019f5819d7bd3ce3d9b41cc9","36c9c48db79eb966ee655673e6ffbc147e10401e0da23b58b26f60fcb581e615","660a1b0ea9ee6158541308d1273e21633f5ae80ec913e3b790a432ee03013935","08f1656af9bf118bac16beb1adaf8ba0a14f97a1b1be128502b69391713da1d0","99ff547ab12fa297fa462a6560113930993561bb0a3ef0c45de3beb151eb0815"],"clip_sha256":"c117bf5a0e203402a102b82f9e34355021f784052f2708842149ce7c214459af"}, "slicing/piece-1@160x90@50/1":{"buffer":"RGBA8P, 160 x 90, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 19..=39 at 50/1 fps, in order","frames_sha256":["92665c212f38919a3f0adb9dd0e442c0d023dcce08e2e1f9c512625507a87496","cff9d4a7f232431f658ede7ba41e6e7d7a962e95d33196157319e772d79c6a58","34c9f854f3f1e36ad7b88e5d236a077cf501e760ba1b13925c32917a8fa5b57d","a06b510d6d22c4a092d79f7c86dfa672ad3330e0c266ed2ce7d83a4226cc706b","d8cd9cc140ffb17e5b9b4394582fe96e82a6f0945e721f71610b51407375cef5","fe873cb1801a22a26a96a228f29d0ec2145d73ea75c6f9a1834c8c4e1ef97162","e693e08a7e3340afd8cca435544f8c64f071762294d8f0abd2119d34985085ba","1fbcfd349eb22d8f2711d1a2e9982c1a4603cbdd612f2c8731f7c6b38d13491b","cf0538db2a0814a82b4e6f4df750e79ef427f7e657ca2d63b7079703fb591239","a1ab5ef6bafc069cfa4c3e09614973bd84cb8791630747446186a83833f0f975","4077ad2675c6945f5e4939016ada436ac76af80c0f4ef49a6cf7aa78cea9459b","597c55503577383c39f9a112c0551870d41a5513019f5819d7bd3ce3d9b41cc9","5753d08d1331f360c8514a8f234a7f8190a0d829ccd19047b87dee326eb7bd4a","36c9c48db79eb966ee655673e6ffbc147e10401e0da23b58b26f60fcb581e615","13c4ad9a85773343544bb8799e4bcadec0c416140af4e9f9b90422b2655847ac","660a1b0ea9ee6158541308d1273e21633f5ae80ec913e3b790a432ee03013935","8f469aa36ca2441536043db4cd8bde002c780faa36754e752c6d147979959cc2","08f1656af9bf118bac16beb1adaf8ba0a14f97a1b1be128502b69391713da1d0","226655d98dbfcfeab2fe31585eb5cf246c0b39e7d34a13ae4104260a04638c33","99ff547ab12fa297fa462a6560113930993561bb0a3ef0c45de3beb151eb0815","4a9774446434d3519797705048eb14fb8ac5a376e743546aedcc2bde68a59f78"],"clip_sha256":"267c1ddeb2083d89d312636f424a09fbb5e22a1f056858698abb3d28c4cebc64"}, "slicing/piece-2@160x90@25/1":{"buffer":"RGBA8P, 160 x 90, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 20..=29 at 25/1 fps, in order","frames_sha256":["dc936081635c336b1d98af10b3e464920bbc3f2b69b598ca17d66c792247a9a6","0f168fdbb3542b9253caa39db08269bd04b88133239b3a117e7a294adcd95d31","c7aba33e68242a7d2e6e0c9e9df5c36c010c53ce8a61e0160e4e32348bcc12cc","a46035af5b9be6142e8f812bd7e6e3f3654542d94480b88ed6fcea1acd4233c5","c3732c4a2ee679edf6166f4044f11819045ca07e0f939bb593f69c1131a93ab8","f4b9f19ea2d4b2c2ee43a5a70f22db3aaa15201039705c27ecc42d7d5678262e","4b00dd134d8c7813add8877d05bd5fda02f418d743a11729e51396a44fd0b7d0","a0788a54e75c196b6c077eaa21b849c97c72a185ce480a723d65bd5e9351d6ec","166bd0ff3c60697ee4c7b1ecee95b2ec720013abc792aeab5cbc08e576f4d5c9","ce02be1012d531d1f886eae303cb966f87d53a5d5eaa10ee606999c54b491346"],"clip_sha256":"a864326ae16ebe027bffd2b038b03dca61d23b0cd8cb768662cbb528b7bb501e"}, -"slicing/piece-2@160x90@50/1":{"buffer":"RGBA8P, 160 x 90, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 40..=59 at 50/1 fps, in order","frames_sha256":["dc936081635c336b1d98af10b3e464920bbc3f2b69b598ca17d66c792247a9a6","f5ce782f09c9fcfba17ca0a1136cf666dbf1ae0fd393fe4f13d6fb1dace470a1","0f168fdbb3542b9253caa39db08269bd04b88133239b3a117e7a294adcd95d31","3889e80648c5c8441041b5150c113211065445c10de2006b6154ef2d42b9ef8e","c7aba33e68242a7d2e6e0c9e9df5c36c010c53ce8a61e0160e4e32348bcc12cc","dbcd989b79b33f889e7863feb738852428c5beb0dafa7949ccce5081a334a342","a46035af5b9be6142e8f812bd7e6e3f3654542d94480b88ed6fcea1acd4233c5","cfc7b9cf3820367628247fe6b3af03362b9e7a76ffe24acac0b9cd78eb477b99","c3732c4a2ee679edf6166f4044f11819045ca07e0f939bb593f69c1131a93ab8","78751cd438b6e16c1fa1cd24fb38c9a3405a132478961decf44777c4bedeb9ad","f4b9f19ea2d4b2c2ee43a5a70f22db3aaa15201039705c27ecc42d7d5678262e","e510f57da223c3aaa84b01fb0eb734d26631d1eb24ae633e942694e28d41caba","4b00dd134d8c7813add8877d05bd5fda02f418d743a11729e51396a44fd0b7d0","23601909304027af52a58b31c8ea17ac585bb79474fabb8af3269b598efadcd1","a0788a54e75c196b6c077eaa21b849c97c72a185ce480a723d65bd5e9351d6ec","b86a51bb3707dcb5c9f5c46c1c6873279b0232b2c246cac794e89546169ad246","166bd0ff3c60697ee4c7b1ecee95b2ec720013abc792aeab5cbc08e576f4d5c9","03b45d692c0ab04e55ca7a240380ceafe6e923336920458112973054b5f2e0bf","ce02be1012d531d1f886eae303cb966f87d53a5d5eaa10ee606999c54b491346","42c5a35c3cda08f0a44c87f43164a0819f66e9148308b397ad0ba4ba8a4b348e"],"clip_sha256":"d43f7d4a8c5088834130ad0cee2e09b28029a99e7e42ad84987ca05d2d0361da"} +"slicing/piece-2@160x90@50/1":{"buffer":"RGBA8P, 160 x 90, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 40..=59 at 50/1 fps, in order","frames_sha256":["dc936081635c336b1d98af10b3e464920bbc3f2b69b598ca17d66c792247a9a6","f5ce782f09c9fcfba17ca0a1136cf666dbf1ae0fd393fe4f13d6fb1dace470a1","0f168fdbb3542b9253caa39db08269bd04b88133239b3a117e7a294adcd95d31","3889e80648c5c8441041b5150c113211065445c10de2006b6154ef2d42b9ef8e","c7aba33e68242a7d2e6e0c9e9df5c36c010c53ce8a61e0160e4e32348bcc12cc","dbcd989b79b33f889e7863feb738852428c5beb0dafa7949ccce5081a334a342","a46035af5b9be6142e8f812bd7e6e3f3654542d94480b88ed6fcea1acd4233c5","cfc7b9cf3820367628247fe6b3af03362b9e7a76ffe24acac0b9cd78eb477b99","c3732c4a2ee679edf6166f4044f11819045ca07e0f939bb593f69c1131a93ab8","78751cd438b6e16c1fa1cd24fb38c9a3405a132478961decf44777c4bedeb9ad","f4b9f19ea2d4b2c2ee43a5a70f22db3aaa15201039705c27ecc42d7d5678262e","e510f57da223c3aaa84b01fb0eb734d26631d1eb24ae633e942694e28d41caba","4b00dd134d8c7813add8877d05bd5fda02f418d743a11729e51396a44fd0b7d0","23601909304027af52a58b31c8ea17ac585bb79474fabb8af3269b598efadcd1","a0788a54e75c196b6c077eaa21b849c97c72a185ce480a723d65bd5e9351d6ec","b86a51bb3707dcb5c9f5c46c1c6873279b0232b2c246cac794e89546169ad246","166bd0ff3c60697ee4c7b1ecee95b2ec720013abc792aeab5cbc08e576f4d5c9","03b45d692c0ab04e55ca7a240380ceafe6e923336920458112973054b5f2e0bf","ce02be1012d531d1f886eae303cb966f87d53a5d5eaa10ee606999c54b491346","42c5a35c3cda08f0a44c87f43164a0819f66e9148308b397ad0ba4ba8a4b348e"],"clip_sha256":"d43f7d4a8c5088834130ad0cee2e09b28029a99e7e42ad84987ca05d2d0361da"}, +"text/latin-ascii@320x96@25/1":{"buffer":"RGBA8P, 320 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["dbcf4bc1b464c3acc88a0e7df30a03ce82d84e477e6fdeb9d8c790caa54ad570"],"clip_sha256":"ce815393b148dae5dd44d50b731049d29c6895440f460c95a3a0f5f3f4a0cb3b"}, +"text/latin-ascii@640x192@25/1":{"buffer":"RGBA8P, 640 x 192, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["ba471588b8d826ca431dcfd36e7478e907ada1c4a57d14dad7a78233f2277e76"],"clip_sha256":"577bb19242156d0e2fe41bc6fb89f211cb875d71a7cd4f0d8f8dc87484421a28"}, +"text/arabic@160x64@25/1":{"buffer":"RGBA8P, 160 x 64, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["e33d9768bca8769dee48af7fce73e8bab3c95644b2d18c797c3d8ccec098dca9"],"clip_sha256":"b6faa8405f77ad6006d55a270b243e27c43be96c5af6bb3e79d8cf92c37da458"}, +"text/arabic@320x128@25/1":{"buffer":"RGBA8P, 320 x 128, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["e031862eb79ebb800f479f9a616554729c5fc03e9ab8c27b219f79c454f35bd8"],"clip_sha256":"9a3aa42017dcc2bd42ac3c689c9e1202c5e0deedc08412eebd22544f4b416a06"}, +"text/hebrew@128x48@25/1":{"buffer":"RGBA8P, 128 x 48, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["7f1c9b3e8ec66c3c0e3caffbe5bf67a0c07d3ad12cbcc60533fbbcbbb9b875da"],"clip_sha256":"190c3cdd7a0106ee114bcf8af5c25a9246a7e672a580a42c279b33965c2bedf4"}, +"text/hebrew@256x96@25/1":{"buffer":"RGBA8P, 256 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["10c53c9b1f51c49ee1571c08cebade11e9111d36b331920725b3eaabe8e3b00a"],"clip_sha256":"09df67a0972e81f725138d0cf1080f894f9521139440dfa3e4e28a685322ff9a"}, +"text/devanagari@128x48@25/1":{"buffer":"RGBA8P, 128 x 48, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["de7b8a4d77e1ed7f6b1e653d4166ef817f41f2de229122f6014b61a16bf813bb"],"clip_sha256":"05c2577e300c87d05547cec85b2bee68e9729e75d918e340e06cabcc4c3002f2"}, +"text/devanagari@256x96@25/1":{"buffer":"RGBA8P, 256 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["64a8e694fb523303cb780cbe91bcfb9aa8c92403719a0d9fcddfaf86a9d12f10"],"clip_sha256":"0191405f20ddcfa808bc147688f0d8d40c59acb05dccf4d49eced0e54fd7e9ba"}, +"text/mixed-direction@224x48@25/1":{"buffer":"RGBA8P, 224 x 48, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["5ed5b87c38b975b0714f4decf7214c264482f3914345370154fd457f12bede90"],"clip_sha256":"93b1cee5a681d86ba49324b72f931a8f291e7b29e76126ff17c72d1ea8dfb38e"}, +"text/mixed-direction@448x96@25/1":{"buffer":"RGBA8P, 448 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["108753723c365bd3c9a997c5cb6e749be31ebb43640a201667fff0a831207442"],"clip_sha256":"a3a9cb32b42b5b14e19e4615a6c6fd1aaf817faffb8d1b6c20c0aa0d5b7f6989"}, +"text/split-lines@208x96@25/1":{"buffer":"RGBA8P, 208 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["73d7dd6ead9dc7c14e7a9324c5a7150fb68590a5cd52919dd3651aa6dbe5d470"],"clip_sha256":"10df3daaa6212dc347420f29f4316c9c5058a7ab4b32c6507cd4694461f4de4d"}, +"text/split-lines@416x192@25/1":{"buffer":"RGBA8P, 416 x 192, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["f31e767207c61d1b5c3839667d684d7894d63d61dc8a0dc49246e55381fc3e65"],"clip_sha256":"8d364cff370d46b17c2b3fd9d101aaa130272c271ab3e2dd205b8d9eaac98c2a"}, +"text/ruby@80x56@25/1":{"buffer":"RGBA8P, 80 x 56, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["6d12cec6949e39ef8931d2e5c27370f54ca0965c828b1e97c04c52df7cf97441"],"clip_sha256":"85268003852f5e1e1be5a327762419452a60c7a5855ed26739520b4b90895dd9"}, +"text/ruby@160x112@25/1":{"buffer":"RGBA8P, 160 x 112, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["d15d75d75ca2ed764bd30e988803fcf31ee9dbd0bde330412d303939e98e8efa"],"clip_sha256":"8ee93c3784dd8b6334eb26e0a815aad5caf5f3887bb74c47eaf302a84bdc5b1e"}, +"text/vertical-rl@48x96@25/1":{"buffer":"RGBA8P, 48 x 96, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["d9b43774221ccc5c9d677d6a6d125fd772e95487cfdcae1a308b648fb4c6bd41"],"clip_sha256":"1d559c2c4ffafa1e45cf5bd5651df602340cc8290607c49a82d7ffe929b23ed4"}, +"text/vertical-rl@96x192@25/1":{"buffer":"RGBA8P, 96 x 192, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["00e04f0f25e422f25d236f7c46b56b348e9ace34ae048675a8b96d50ecf1cfcf"],"clip_sha256":"2bb95d63e1008192572d721190163e09b92ed883d0b9633901c9dde608b1274d"}, +"text/emoji@80x40@25/1":{"buffer":"RGBA8P, 80 x 40, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["db02e147c6aabbb4fb2a37b88f1f0e04c92274929d7899bf39d813701056bb33"],"clip_sha256":"f8f901be288a067da15e32b5374eb4b6961268e7ec44827fc0e1efcf2bb835e0"}, +"text/emoji@160x80@25/1":{"buffer":"RGBA8P, 160 x 80, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["7971ccd3260c96e3e1ad4ad97956af04a6c84b9b16fa660b3aa0a4ea2f3b4a21"],"clip_sha256":"1a6a95ea77ed01e8ab5a17e8bbc05646d3d30a8cf6935eba8165d5dad1c810aa"}, +"runs/arabic-joins@96x48@25/1":{"buffer":"RGBA8P, 96 x 48, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["828d00fc87f087778556d6ed5de3fa8a663aca6ab8455171f6fde0b32a64aff9"],"clip_sha256":"10583262cf57dc99cf295a5ee9211922618353fa41268d5732ab3b063a7599a4"}, +"runs/arabic-joins-split@96x48@25/1":{"buffer":"RGBA8P, 96 x 48, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=0 at 25/1 fps, in order","frames_sha256":["6b605b7066f21ddcfc5545748a575c1158de85a39bb1d31bfb094d73bbdc20ea"],"clip_sha256":"4bce3a67a81481a01b29f1d930cb08a41260dcc5fd8df07a9a2e6deec5bcf55b"}, +"motion/stroke-slice@320x180@25/1":{"buffer":"RGBA8P, 320 x 180, row-major, premultiplied, sRGB-encoded, bytes R G B A; frames g = 0..=49 at 25/1 fps, in order","frames_sha256":["51f9c33fb9f7c65fc675cf9a8f286a95e39d11eed58a9bd52b91da323ac583ea","24af64a8b20b655727d5e62c2964474e58ad993a696354d190b8f1a45ca863cd","db89480066e6c1cd54ba1048e45c964f476a46aca8dadd2af9a10267c7ec0c09","f0e46cbd094e20023dd845ca7416dc4954381319a6587fab862bb5cdc76daa1c","e8f05e1feecd24a6365ec7c25152dde7809f1bf1dd51edd63b14d19457af5c4e","c8cf3704099bd7d955e781205b9c7e050c9e98ca80549c54713b9ec2fe0e00d7","65bfcac71d5184201c42c2e7351ee3cda8c35da9d43864302829bf13462c9103","6d244b03beceff5c11e2bdfe0e0ab9c46abfd0af7ecf85eeaf4a02676867a662","94f0bf7aeaf7082ebfdb8285ade3d7c6310a93eb799d2cb4081a7bb34c32d659","1c5cb976f91ff2b16bedf7e6c11b47cdcf04fc68b60dca09ce4881892a11fd7e","82facce1e6ace3c5f2dd3950d0ecf17c26dd5897f849a4a512a73b7a68d15be5","4eb8e66960ff2b8dd5bc5a8eafa29a04ce55ce234c3abe45dfd0b6ca4b10b6f6","2308d9f1cc973be6e38c90d8a40d400e6098e18ee0e5235857a3c9b64a7ede3a","ea1b0c888c5857d4fa136cc8fcdfc05c4b3d0f40e87d39dbae7852d5850abbcb","b54e2d5ba0feca12e0fef1330bc1b8799e06bf7e69442405fc0e371084f83b56","f0aec5846b8a7ffa9675e569793bf6c66b9dd72b64b6d9487318710f412d761b","802a585234f5bf690b0163cb1744c29a5aba2768e5a7b794516603f4359f2ac2","2724930371428790b3c67d870e066bbcaf1d4d3237bbbaf0e1e24fec1d342728","973db68e7ec16691846999513f76acb5b0b60264cd09c9c3b4f061ed84857747","a52d6e5d92df1d2d6363b2f4fd5a679f08f3f6d91dcde2b2472925a818b13a03","5cff1ffd3f5afd9f8e2638ffe81d73c3c730e324841720f8312c1e8d2a07ee8f","ff529fbffabfe7f369a64552493306c647871c0e92f69502e2105cec9e1820ac","0d9d7100639681c499921c3350d94e3c8b430f34134e572da3d944dbe7ff53c4","42f1d26e6ba83fbaeaa2c529b0803884676f6d90fa8925c33bdbf7a6412b4724","fe17cf005b9b92a9a87615b917cd26ea9445974ad0ac2ea6cad1f85ce415d8d2","41bca0991e394f37dbea57547dae054f215f52679e11156f5ba7979d36db8dc3","7e48b6b4aa98b1d2c864982a9a02c52e601941feed5d86b5868c3d1a1f96dc18","4d4c3bd23afeb979a1cb0034c75bb5bdbbb54b261cd7ea4f3ac39c4c515784d6","eddeb7d5d5cc73c4959e678607ca9c41087cf37b3e9915a5eb7a4333b625698d","3b7b02f68a0e884802455183e5b7da724d2a8bf7aad600d9a56e1f1d4ff15ed9","3e1fca6d53f6ddb3593f15c80f0d9497e5946fec1cb4a0237284f38eb5e6129d","d3d830844430aea59a3bc0f2ba0c78eebff23d1176bd97d5c68f11e994dab90a","0ce0e9b42a968d189717f0fa70784941d8ea6a80f117ddc1ecd02bd6abdb6e9a","ca66ee4fb115569f49639b0e283fc1e8d83aa7d69f15e145a635a0ad42a00ab3","68abea21cbcea92374a5d4b535f19e606245b56d263dbf2029c7188b2afba706","b9704483f218a364462a753c6a9fa86eeeb31907f9360c6d7b86f571a8d4372d","e736441c2e198921e930da342190caac604e89fe8bd78a2fa29e6d841712044b","fddfc9d19e1407a3020e81bfa95d6ff44eec9bc5f2558902ccbec7a2be71dbc9","678c26a298fac8829d90179f7efdc6e0ad4599f5dbfb780bf16e94887eea669b","de36b5bd62944fcc7d0b405d902a13e4b4b97c29774522cea546a8e510addde3","297c16b9eb660a49ed5d0f1ae49079842db1f4b7931935688d1472e2e4f3e763","4bd50eb0ab2fe83261de4c761ea03c6ebc5baabd7342b41a3da1d19ec82cdf8f","9be53e089b51bf5d3ad39942c820a7457e6838f735b5eade03cdb21b19166160","7170ce78e75c7b712e4fbf6584ff4f7afb5182580ffe02bb643047020a09456e","160a4bc34b110689eae4bf90b9506d5ec4fbbb93e57018825dd3b80f6e169c16","6fcaac14edaef1282473e0fd61f4a142794e0a6eb47bcc94eb5c53834a43edae","6cbd88fb80d87b14e384b2ca5dba058bba8e305ad848100abb50c3784f3771e6","f8da02c0c14dd4fadd511f9ff14d198fb3214c3cf1b8613a5e96b5c8f00390aa","75b7c82c389fa10aff769a103fef2dc963edb6583ac7d40ddb532c5b1f73f81a","16898aeb46ad07fb8974a4bff2c476de5df3ad85c90617bcdfa838b6685117c4"],"clip_sha256":"efca9b207efa9d9d487552bb9ed6c1e4718fdfba22c56b075988268a76716a8b"} } diff --git a/wai/vector-conformance/sources/stroke-ascii.wsf1 b/wai/vector-conformance/sources/stroke-ascii.wsf1 new file mode 100644 index 0000000000000000000000000000000000000000..84c1bfe9e33779e7ebb9ad2e338d56aac7553bd4 GIT binary patch literal 19181 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 literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/motion/index.json b/wai/vector-conformance/vectors/motion/index.json index db353fb52..9d3c6c120 100644 --- a/wai/vector-conformance/vectors/motion/index.json +++ b/wai/vector-conformance/vectors/motion/index.json @@ -2,5 +2,6 @@ "easing":"one item per easing mode: hold, linear, a monotone table and a table that is not: accepted", "lower-third":"a lower third that slides in on an easing table, holds and fades out, as one group: accepted", "roll-fractional":"the same roll at 1.5 design pixels per frame: no whole-pixel shift between frames: accepted", -"roll-integer":"a credit roll at 2 design pixels per frame, keys on frame ticks: frame k is frame 0 moved 2k pixels at the design size, 4k at twice it (P2): accepted" +"roll-integer":"a credit roll at 2 design pixels per frame, keys on frame ticks: frame k is frame 0 moved 2k pixels at the design size, 4k at twice it (P2): accepted", +"stroke-slice":"a lower third in the stroke set (a bar and two lines that slide in on an easing table, hold and fade out as one group) and a short credit roll moving up three design pixels a frame: accepted" } diff --git a/wai/vector-conformance/vectors/motion/stroke-slice.json b/wai/vector-conformance/vectors/motion/stroke-slice.json new file mode 100644 index 000000000..7cd455474 --- /dev/null +++ b/wai/vector-conformance/vectors/motion/stroke-slice.json @@ -0,0 +1,211 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":240000,"design_w":320,"design_h":180,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":1,"n_paints":5,"n_texts":5,"n_tracks":3,"n_paths":1,"n_items":8,"n_keys":7,"n_glyphs":71,"n_clusters":71,"path_contours":1,"path_points":4,"strings_len":81,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":320,"h":180,"cost":16863033} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[ +[0,9000,17000,24000,30500,36000,41000,45500,49500,53000,56000,58500,60700,62500,64000,65000,65536] +], +"paints":[ +[20,40,120,220], +[255,255,255,255], +[200,200,200,255], +[220,40,40,255], +[40,80,220,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2}, +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":3}, +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2} +], +"keys":[ +[0,-76800,2,[0,0,0]], +[48000,0,0,[0,0,0]], +[0,255,0,[0,0,0]], +[192000,255,1,[0,0,0]], +[240000,0,0,[0,0,0]], +[0,0,1,[0,0,0]], +[240000,-38400,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":12,"ymin":128,"xmax":236,"ymax":172} +], +"path_ends":[3], +"path_points":[ +[12,128,0], +[236,128,0], +[236,172,0], +[12,172,0] +], +"texts":[ +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":1,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":10}, +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":1,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":28}, +{"purpose":2,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":14}, +{"purpose":2,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":7}, +{"purpose":2,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":12} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":65535,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":240000,"unit_scale":0,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":1,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":240000,"unit_scale":16777216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":0,"zero@10":0,"n_glyphs":10,"n_clusters":10,"t_on":0,"t_off":240000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":2,"text":1,"zero@10":0,"n_glyphs":28,"n_clusters":28,"t_on":0,"t_off":240000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":2,"zero@10":0,"n_glyphs":14,"n_clusters":14,"t_on":0,"t_off":240000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":2,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":3,"text":3,"zero@10":0,"n_glyphs":7,"n_clusters":7,"t_on":0,"t_off":240000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":2,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":4,"text":4,"zero@10":0,"n_glyphs":12,"n_clusters":12,"t_on":0,"t_off":240000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":2,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[83,0,5120,37888], +[116,0,8570,37888], +[114,0,10730,37888], +[111,0,13010,37888], +[107,0,16370,37888], +[101,0,19370,37888], +[32,0,22610,37888], +[115,0,24410,37888], +[101,0,27230,37888], +[116,0,30470,37888], +[73,0,5120,41984], +[110,0,6160,41984], +[116,0,8400,41984], +[101,0,9840,41984], +[103,0,12000,41984], +[101,0,14200,41984], +[114,0,16360,41984], +[32,0,17880,41984], +[111,0,19080,41984], +[117,0,21320,41984], +[116,0,23560,41984], +[108,0,25000,41984], +[105,0,25960,41984], +[110,0,26920,41984], +[101,0,29160,41984], +[115,0,31320,41984], +[44,0,33200,41984], +[32,0,34080,41984], +[100,0,35280,41984], +[114,0,37520,41984], +[97,0,39040,41984], +[119,0,41200,41984], +[110,0,44200,41984], +[32,0,46440,41984], +[104,0,47640,41984], +[101,0,49880,41984], +[114,0,52040,41984], +[101,0,53560,41984], +[79,0,22270,23040], +[112,0,26530,23040], +[101,0,29890,23040], +[110,0,33130,23040], +[105,0,36490,23040], +[110,0,37930,23040], +[103,0,41290,23040], +[32,0,44590,23040], +[116,0,46390,23040], +[105,0,48550,23040], +[116,0,49990,23040], +[108,0,52150,23040], +[101,0,53590,23040], +[115,0,56830,23040], +[67,0,31360,31744], +[114,0,35260,31744], +[101,0,37540,31744], +[100,0,40780,31744], +[105,0,44140,31744], +[116,0,45580,31744], +[115,0,47740,31744], +[76,0,24085,40448], +[111,0,27235,40448], +[119,0,30595,40448], +[101,0,35095,40448], +[114,0,38335,40448], +[32,0,40615,40448], +[116,0,42415,40448], +[104,0,44575,40448], +[105,0,47935,40448], +[114,0,49375,40448], +[100,0,51655,40448], +[115,0,55015,40448] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,22,1], +[1,0,23,1], +[1,0,24,1], +[1,0,25,1], +[1,0,26,1], +[1,0,27,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1] +], +"strings":"enStroke setenInteger outlines, drawn hereenOpening titlesenCreditsenLower thirds", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/motion/stroke-slice.wvg1 b/wai/vector-conformance/vectors/motion/stroke-slice.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..1dc1b5bc410e5bc41f7fdfb475b4d0dfb4dc3fd5 GIT binary patch literal 2639 zcmd6oU1(HC6vxlaCMHd{F|DztO`unluoV%s5G@Fn1oPEEv_cHlDq?Q48<(57dnF-6 z+0qtTDI!Io52a0uZB-WgLG6Q$)TR%K^uZ`WL{jR5&``83(kH=e|Nnb-lIarAmw>a( z{D1e%IWuR@{n(wI&FdWzxirO5By%PruQ=a}gH|FX5)ntrU@t{)&Ubniqi;uNd+B41 zB{TNr4##;8Y^FDdqcN!F%;jH2LS`)MQrPC~y}2;N39BL!m5khxEcPP- z!y(HF3=!nRNYlyzg|=GQio(2&mq^6|Hr|y&zi2*$mC(G@U{0WR&g60?4^xo^{2k-3 zs%vR2G`d`P0#l&~R<)b$ujj56p3eBA!vAaPZ}QTshmR&7o}(hHlv&@-hkNN->K_T8 zh|=)ceiZ*=DMok{KQ^uB!t3v#Llyc7kpMEZQaJ_r^F`%8$h8g12O#&hD0`5zuPVEc z*SeI`kX?I~>9eO-nd>jX+rqtCA>qEKM0{XnH3l$!CS7I5_SY%*KyIy@w-0VqW}Y+5 z2eB^X##UwK{9c>#AxH}knqR+cw{jA4dY^Iv66c2bd}1_!X_`5x%yV@ODX)cm^Omv; zao<($hD^S%%-oKDsLX9LA1Sj0|BNW}?5DHJE+l?lIS#45pv)X@j45-?mS5-Xo31K% zKxR8J>_W{TpR-_K#*VS5VD8n+oWVTa(PQ)WHMl3VGpy}1Wf!vUoHCcFXKwk}FE7s9 zn|@JdJ%3I)zt}8nbHfmw#dv9!fw+$CtTw?JOi}?*T{`{vi{Dj58IlGys zF803svYP##v^Z+@^kx z=lo3^^>M4`V&u6PYd*>|ywJ1c`8$GC-vQ4}dx7V-`hnNurQE(lK{DZc>9uZmYTtnG n#=Vs1`5pb9pYVI!AQ2?JwC6XbyzWE*Xzv?9P!La~y3_vwyZKu% literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/runs/arabic-joins-split.json b/wai/vector-conformance/vectors/runs/arabic-joins-split.json new file mode 100644 index 000000000..a0104b36e --- /dev/null +++ b/wai/vector-conformance/vectors/runs/arabic-joins-split.json @@ -0,0 +1,40 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":96,"design_h":48,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":2,"n_keys":0,"n_glyphs":3,"n_clusters":3,"path_contours":0,"path_points":0,"strings_len":8,"inline_len":0,"held_len":59879,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":96,"h":48,"cost":718113} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":1,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":1,"n_clusters":1,"t_on":0,"t_off":4800,"unit_scale":655360,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":4800,"unit_scale":655360,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[65174,0,2304,7680], +[65268,0,11904,7680], +[65169,0,17104,7680] +], +"clusters":[ +[1,0,4,2], +[1,0,2,2], +[1,0,0,2] +], +"strings":"arبيت", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/runs/arabic-joins-split.wvg1 b/wai/vector-conformance/vectors/runs/arabic-joins-split.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..c64c01f6b5397300750a3d33a927f5e1f21c5774 GIT binary patch literal 336 zcmWF!b2nsUU|=|K1w;aAAqIv71_K60Mn(n@1OX;6$-ux2q(K-4IG~K@FJU4Oy^3GC z7$kv05Zw$6AbDkh)hTz+`?kvZ)^O}TCnCQ~yr22+=?&Gaw=I$%R<`8-2Lg6Rm`xD# z*nkwWeK7Zc+{nPd#Q@TS01!RM>Y1P}$ESYUKcGHNAeLib_yS}%=t0;Mf$R%T5H`pk aEI literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/runs/arabic-joins.json b/wai/vector-conformance/vectors/runs/arabic-joins.json new file mode 100644 index 000000000..9b52a0171 --- /dev/null +++ b/wai/vector-conformance/vectors/runs/arabic-joins.json @@ -0,0 +1,39 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":96,"design_h":48,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":1,"n_keys":0,"n_glyphs":3,"n_clusters":3,"path_contours":0,"path_points":0,"strings_len":8,"inline_len":0,"held_len":59879,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":96,"h":48,"cost":718535} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":1,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":4800,"unit_scale":655360,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[65174,0,2304,7680], +[65268,0,11904,7680], +[65169,0,17104,7680] +], +"clusters":[ +[1,0,4,2], +[1,0,2,2], +[1,0,0,2] +], +"strings":"arبيت", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/runs/arabic-joins.wvg1 b/wai/vector-conformance/vectors/runs/arabic-joins.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..24f5db4552b98f6b7dd94953019529f54012e8f3 GIT binary patch literal 288 zcmWF!b2nsUU|=|K1w;aAAqIv71_K60Mn(n@1OW)iz`zV-gD?zmKpD?p!bBi?kALH0 zkOT_B^nv7+1y-lrJ@4Bp>s!OI`<#gUF7bZmzo$1;v);ByepuO({~rk08JQ3U0alm~Jn0|OTWNDl&l#26T+{R0x5KrF|=@CC?j(1Wli0@)XwAZ(DkSb!L08;A{} SVQe51%uX!2vEpXeja2}sTR1HM literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/runs/index.json b/wai/vector-conformance/vectors/runs/index.json index f5f9c2876..11fa97d6c 100644 --- a/wai/vector-conformance/vectors/runs/index.json +++ b/wai/vector-conformance/vectors/runs/index.json @@ -1,5 +1,7 @@ { "abutting":"three glyphs abutting mid-pixel: the seam pixels read 255 (the glyph-wise union): accepted", +"arabic-joins":"an Arabic joining sequence from the test repertoire in one run, large: each connector's round cap passes its glyph's edge and overlaps the neighbour, and the glyph-wise union over-covers the partly covered edge pixels of each overlap (the join knot, §3.13): accepted", +"arabic-joins-split":"the same joining sequence split across two runs, which an encoder never writes (one joining sequence per run): the runs composite by source-over, so the join's pixels differ from the one-run vector: accepted", "marks":"marks overlapping their bases: accepted", "overlapping":"glyphs that overlap: the partly covered edge pixels of the overlap over-cover (the knot, §3.13): accepted", "phases":"one glyph at all 256 subpixel phases of the x axis: accepted", diff --git a/wai/vector-conformance/vectors/text/arabic.json b/wai/vector-conformance/vectors/text/arabic.json new file mode 100644 index 000000000..66b0f5bec --- /dev/null +++ b/wai/vector-conformance/vectors/text/arabic.json @@ -0,0 +1,50 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":160,"design_h":64,"n_sizes":2,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":1,"n_keys":0,"n_glyphs":9,"n_clusters":7,"path_contours":0,"path_points":0,"strings_len":21,"inline_len":0,"held_len":59879,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":160,"h":64,"cost":1822464}, +{"w":320,"h":128,"cost":1923747} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":1,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":19} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":9,"n_clusters":7,"t_on":0,"t_off":4800,"unit_scale":327680,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[65249,0,7012,10240], +[65276,0,10812,10240], +[65203,0,16312,10240], +[32,0,20812,10240], +[65174,0,23812,10240], +[65268,0,28612,10240], +[1618,0,29912,10240], +[65169,0,31212,10240], +[1614,0,33012,10240] +], +"clusters":[ +[1,0,17,2], +[1,0,13,4], +[1,0,11,2], +[1,0,10,1], +[1,0,8,2], +[2,0,4,4], +[2,0,0,4] +], +"strings":"arبَيْت سلام", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/text/arabic.wvg1 b/wai/vector-conformance/vectors/text/arabic.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..d96dfd7ee0f5aadd57c3297711fcd7e423ec9b57 GIT binary patch literal 429 zcmWF!b2nsUU|=|K1w;aAAqIv83=RxTjEoE*2m%n2fq@gqW(SKv2vI2e`AdW#NH4>A zX$A+z28P8^vJ8?yDVTncI%R>?DRYoFc32%iEu()0dgw?11kea4+20GKK#ePkRlCaYcMeU0kUnhAneUR_6~ap zTLDP<1VY%;fZ{B%5cU@!`$#^79mK}KaH9mmo(L3wQwd@F0mZ*GK-eJv2?8<5)gU%6 z5VJtp+&~&8&IJ_%so{W%gXBPJK^W$15IeEx#)_MLH@j|5y0J>(#^#$XHFv-Bc0;EA01~{P%sWzAjh~9}F z{0s|#rUd@rXOIM{ftbR;U_6JBfz>_7E#iG~+VKK*GmpA8J~q;|ifWz}$1g-*Iqp<$ zb`oUd^Op<^$^xrX?w~NPs9KsHy zm?2Cs$qeOz`~;E#=|KR9LXdix`(XaXr`{i=jSYz97#O^P)DkfWTLDOIQGu`-A22ZN h(T1{t6bOLqK*yj^gNcFM0SepHJb!P65A$a<006!?QJnw) literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/text/hebrew.json b/wai/vector-conformance/vectors/text/hebrew.json new file mode 100644 index 000000000..c63f9d9b8 --- /dev/null +++ b/wai/vector-conformance/vectors/text/hebrew.json @@ -0,0 +1,42 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":128,"design_h":48,"n_sizes":2,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":1,"n_keys":0,"n_glyphs":4,"n_clusters":4,"path_contours":0,"path_points":0,"strings_len":10,"inline_len":0,"held_len":59879,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":128,"h":48,"cost":533026}, +{"w":256,"h":96,"cost":575728} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":1,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":8} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":4,"n_clusters":4,"t_on":0,"t_off":4800,"unit_scale":262144,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1501,0,8448,8704], +[1493,0,12688,8704], +[1500,0,15088,8704], +[1513,0,19088,8704] +], +"clusters":[ +[1,0,6,2], +[1,0,4,2], +[1,0,2,2], +[1,0,0,2] +], +"strings":"heשלום", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/text/hebrew.wvg1 b/wai/vector-conformance/vectors/text/hebrew.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..618aee36d7eca1b3e43fe835db7496274b657546 GIT binary patch literal 322 zcmWF!b2nsUU|=|K1w;aAAqIv91_K5rMn(n@1OW)iz`z1zgD?zmK^f0q!bBi?m6SMu zni3d3oZw)P1WLj5gVZSttWLRm-nUiOw}xZ)IT86?;{D8jPj9GZy={^Fu(BopKM=4p zG9e5EvVf*BFe8buKwSZHE67D4JqQ3%c$XDOD*~|+1H)Aydx9Z^eFwx*gz(josoKd<@Gt&r(T~60Iz2{NB{r; literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/text/index.json b/wai/vector-conformance/vectors/text/index.json new file mode 100644 index 000000000..8a2180d72 --- /dev/null +++ b/wai/vector-conformance/vectors/text/index.json @@ -0,0 +1,11 @@ +{ +"arabic":"Arabic, right to left, in the test repertoire: initial, medial, final and isolated forms chosen by joining type, the lam-alef ligature, fatha and sukun on their bases; glyphs in visual order and clusters with byte_first decreasing; joins whose connectors overlap: accepted", +"devanagari":"Devanagari in the test repertoire, one word spelt with the anusvara and with the conjunct: the i-matra drawn before the consonant it follows in logical order, the anusvara above its base, the conjunct na-da as one glyph; one cluster per syllable; headlines that overlap: accepted", +"emoji":"U+1F600 drawn by one monochrome outline glyph from the test repertoire, one cluster mapping its four bytes to that glyph: accepted", +"hebrew":"Hebrew, right to left, in the test repertoire, with the final mem: accepted", +"latin-ascii":"printable ASCII, U+0020-U+007E, from the held stroke set: one block drawn by three runs, one per line, one cluster per glyph: accepted", +"mixed-direction":"an Arabic block, right to left, holding a Latin word and digits: the left-to-right run from the stroke set, then the right-to-left run with its space from the test repertoire, both drawing parts of one block: accepted", +"ruby":"a ruby block, Latin from the stroke set, annotating the whole string of a Devanagari base block from the test repertoire: accepted", +"split-lines":"one block broken over three runs, one per line: the space at the first break drawn by no glyph (a cluster of zero glyphs), and a hyphen inserted at the second (a cluster of no bytes): accepted", +"vertical-rl":"a vertical-rl block (mode 1): Latin from the stroke set, upright and stacked; the sink draws the glyphs where the payload puts them, and the mode is in the block's identity: accepted" +} diff --git a/wai/vector-conformance/vectors/text/latin-ascii.json b/wai/vector-conformance/vectors/text/latin-ascii.json new file mode 100644 index 000000000..e769ee853 --- /dev/null +++ b/wai/vector-conformance/vectors/text/latin-ascii.json @@ -0,0 +1,226 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":320,"design_h":96,"n_sizes":2,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":95,"n_clusters":95,"path_contours":0,"path_points":0,"strings_len":98,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":320,"h":96,"cost":19235383}, +{"w":640,"h":192,"cost":19733375} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":3,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":95} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":32,"n_clusters":32,"t_on":0,"t_off":4800,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":32,"n_clusters":32,"t_on":0,"t_off":4800,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":31,"n_clusters":31,"t_on":0,"t_off":4800,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[32,0,2048,5632], +[33,0,3248,5632], +[34,0,4288,5632], +[35,0,5808,5632], +[36,0,8288,5632], +[37,0,10588,5632], +[38,0,13588,5632], +[39,0,16188,5632], +[40,0,17068,5632], +[41,0,18348,5632], +[42,0,19628,5632], +[43,0,21428,5632], +[44,0,23628,5632], +[45,0,24508,5632], +[46,0,26028,5632], +[47,0,26908,5632], +[48,0,28508,5632], +[49,0,30748,5632], +[50,0,32988,5632], +[51,0,35228,5632], +[52,0,37468,5632], +[53,0,39708,5632], +[54,0,41948,5632], +[55,0,44188,5632], +[56,0,46428,5632], +[57,0,48668,5632], +[58,0,50908,5632], +[59,0,51788,5632], +[60,0,52668,5632], +[61,0,54868,5632], +[62,0,57068,5632], +[63,0,59268,5632], +[64,0,2048,12800], +[65,0,5448,12800], +[66,0,8008,12800], +[67,0,10508,12800], +[68,0,13108,12800], +[69,0,15708,12800], +[70,0,18008,12800], +[71,0,20208,12800], +[72,0,22908,12800], +[73,0,25508,12800], +[74,0,26548,12800], +[75,0,28548,12800], +[76,0,30948,12800], +[77,0,33048,12800], +[78,0,36048,12800], +[79,0,38648,12800], +[80,0,41488,12800], +[81,0,43888,12800], +[82,0,46728,12800], +[83,0,49228,12800], +[84,0,51528,12800], +[85,0,53828,12800], +[86,0,56428,12800], +[87,0,58928,12800], +[88,0,62328,12800], +[89,0,64728,12800], +[90,0,67128,12800], +[91,0,69428,12800], +[92,0,70628,12800], +[93,0,72228,12800], +[94,0,73428,12800], +[95,0,75428,12800], +[96,0,2048,19968], +[97,0,3248,19968], +[98,0,5408,19968], +[99,0,7648,19968], +[100,0,9648,19968], +[101,0,11888,19968], +[102,0,14048,19968], +[103,0,15448,19968], +[104,0,17648,19968], +[105,0,19888,19968], +[106,0,20848,19968], +[107,0,21888,19968], +[108,0,23888,19968], +[109,0,24848,19968], +[110,0,27688,19968], +[111,0,29928,19968], +[112,0,32168,19968], +[113,0,34408,19968], +[114,0,36648,19968], +[115,0,38168,19968], +[116,0,40048,19968], +[117,0,41488,19968], +[118,0,43728,19968], +[119,0,45808,19968], +[120,0,48808,19968], +[121,0,50808,19968], +[122,0,52888,19968], +[123,0,54808,19968], +[124,0,56208,19968], +[125,0,57168,19968], +[126,0,58568,19968] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,22,1], +[1,0,23,1], +[1,0,24,1], +[1,0,25,1], +[1,0,26,1], +[1,0,27,1], +[1,0,28,1], +[1,0,29,1], +[1,0,30,1], +[1,0,31,1], +[1,0,32,1], +[1,0,33,1], +[1,0,34,1], +[1,0,35,1], +[1,0,36,1], +[1,0,37,1], +[1,0,38,1], +[1,0,39,1], +[1,0,40,1], +[1,0,41,1], +[1,0,42,1], +[1,0,43,1], +[1,0,44,1], +[1,0,45,1], +[1,0,46,1], +[1,0,47,1], +[1,0,48,1], +[1,0,49,1], +[1,0,50,1], +[1,0,51,1], +[1,0,52,1], +[1,0,53,1], +[1,0,54,1], +[1,0,55,1], +[1,0,56,1], +[1,0,57,1], +[1,0,58,1], +[1,0,59,1], +[1,0,60,1], +[1,0,61,1], +[1,0,62,1], +[1,0,63,1], +[1,0,64,1], +[1,0,65,1], +[1,0,66,1], +[1,0,67,1], +[1,0,68,1], +[1,0,69,1], +[1,0,70,1], +[1,0,71,1], +[1,0,72,1], +[1,0,73,1], +[1,0,74,1], +[1,0,75,1], +[1,0,76,1], +[1,0,77,1], +[1,0,78,1], +[1,0,79,1], +[1,0,80,1], +[1,0,81,1], +[1,0,82,1], +[1,0,83,1], +[1,0,84,1], +[1,0,85,1], +[1,0,86,1], +[1,0,87,1], +[1,0,88,1], +[1,0,89,1], +[1,0,90,1], +[1,0,91,1], +[1,0,92,1], +[1,0,93,1], +[1,0,94,1] +], +"strings":"und !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/text/latin-ascii.wvg1 b/wai/vector-conformance/vectors/text/latin-ascii.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..bea8091708de2c713f42431e39551b8ef0a77281 GIT binary patch literal 2690 zcmbu_@ED3sjs3?9bUuxJq#f4=L|3k@*HJAL_|=;QA9=sN{umXVq(*z zO_Q=}U6^(jq_JI@hLzMN8e)hsDQR60bzvfj8y8L5|9?J@e(9o{-pSnmZ_b>1?wNDX zeBE8{Jey@%3(GtsH(OSft>22U*(|kyJ}a}2yf!JPEtM_yZWDcLp~v0!~1 zn`PU9HuEj}ZE%k^_@0jsEe0Z8o|%xhEOt8H(HLA@>RDc_nRMOx8%N~pJIW1sPcsL$ z*zw}Qxms3)#cSRiZ2N!xcU!x|WBk|ncJ_D+@9mZqkC=}VCm1F^MGoPsE;6sICWHDY!Rxy9+USDb;k*)GmRc>Urm#4Row?wgGW9Tn#w;zq=| z2>&T@9@~TBe8kGExBxNtnb?W&e<9{{T!jd)KPFradbZ&NbICk>d~huyii-!=A)Nd{ za6Q7$o4^eSA5Soscx|89gP1xXZbV!^B5p$5JSJ{NT;>|$T0W9ht|7b^5%Yc*!Lwx$Pn2*-GAnrhT?ua`Pb@#o@Wp2+$84KS=}P;g_CxQ`{3nWVs5GH4skymUVr?+!(e#-!vnCL&jRL> zJ^DyIh?wWbM?1G;AX_|yc;pguS$*6H=rfF1<3oc-5c6DA_&6fK1%po@X1N&fD58}U zhfgA+IVpGyk;*S&KB}i@#R0^PY4K^q@D=eHMCvv1I3ngtF)s_ui6;=@XNNJ*!Asvu z`y^uRM=`g;4eojLIgbe57GFSoeNTK55%WMih4}n;F{jn~r+5bO>#7(lA3m7>en+Ub zb;|3NHz;pZj#OqzGh@H4{EqUw%2CSiDZj70Ntvb5jQ@f1X5}r)Ta~%3%(dH%MW1vR}DZx$l)H#s=;235iL`j+E52^o-1`?3~=Z`~qj8tEjkSPia|sMP*fW zO>JF$gWJ>C)a>26ucdW=TYE=mSGVuL!5)8a-=Y4)14jmr4jmgFIeudF2S|f33jS806z8MAO#@vR2Uc>fz$*Y2-_3Hc7U)I zfYb&L2-^rq2?Rmd20&^;GLm>Hglz^AZ-cNEfRw-t2>anb28M!#5cVG+J76t@y&1@E z*au;Q+|CKaAR2_Zkl5TvY*4(x)bJv)`Hs!OI`<#gUF7bZmzo$1;v);By zepuO(4>Hnt4kH7rdyZSg`{K0Y1?*-Xb!&WVq-zz`JS&c0h`w^%sod=3e;{Cox)16M z27Vxe9qdAgjUYZN5HrIRfcX%&g8a$Az`_7B2LT`oLFz#!fG{iA1W33rG%#XSZ_mlV zAP+K6kAcAw$c~qQu$h2tdld*<4aokl4q-b0+3U?9>`xY`ct+-fB|9H1 c+5cck(}N|;9xQFh$kg>oEXm8r%uC4x095Q`+W-In literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/text/split-lines.json b/wai/vector-conformance/vectors/text/split-lines.json new file mode 100644 index 000000000..03b74e15d --- /dev/null +++ b/wai/vector-conformance/vectors/text/split-lines.json @@ -0,0 +1,113 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":4800,"design_w":208,"design_h":96,"n_sizes":2,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":1,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":38,"n_clusters":39,"path_contours":0,"path_points":0,"strings_len":40,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":208,"h":96,"cost":8058342}, +{"w":416,"h":192,"cost":8346462} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":2,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":38} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":12,"n_clusters":13,"t_on":0,"t_off":4800,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":16,"n_clusters":16,"t_on":0,"t_off":4800,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":10,"n_clusters":10,"t_on":0,"t_off":4800,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[84,0,2048,6144], +[105,0,5498,6144], +[116,0,6938,6144], +[108,0,9098,6144], +[101,0,10538,6144], +[115,0,13778,6144], +[32,0,16598,6144], +[100,0,18398,6144], +[114,0,21758,6144], +[97,0,24038,6144], +[119,0,27278,6144], +[110,0,31778,6144], +[102,0,2048,13824], +[114,0,4148,13824], +[111,0,6428,13824], +[109,0,9788,13824], +[32,0,14048,13824], +[105,0,15848,13824], +[110,0,17288,13824], +[116,0,20648,13824], +[101,0,22808,13824], +[103,0,26048,13824], +[101,0,29348,13824], +[114,0,32588,13824], +[32,0,34868,13824], +[105,0,36668,13824], +[110,0,38108,13824], +[45,0,41468,13824], +[115,0,2048,21504], +[116,0,4868,21504], +[114,0,7028,21504], +[117,0,9308,21504], +[99,0,12668,21504], +[116,0,15668,21504], +[105,0,17828,21504], +[111,0,19268,21504], +[110,0,22628,21504], +[115,0,25988,21504] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[0,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,22,1], +[1,0,23,1], +[1,0,24,1], +[1,0,25,1], +[1,0,26,1], +[1,0,27,1], +[1,0,28,0], +[1,0,28,1], +[1,0,29,1], +[1,0,30,1], +[1,0,31,1], +[1,0,32,1], +[1,0,33,1], +[1,0,34,1], +[1,0,35,1], +[1,0,36,1], +[1,0,37,1] +], +"strings":"enTitles drawn from integer instructions", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/text/split-lines.wvg1 b/wai/vector-conformance/vectors/text/split-lines.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..ed243be318d310de767c0218bef3aa2833f06582 GIT binary patch literal 1276 zcmb7@zfTlV5Xa|E5W!!7s0gSFA|Qzc@x1CxjK5AJg#}RzY9!nqIdklh-6dxTN@GDR zw6V0%gu=o<;<}3+F~rIkD`ID5XVmX^XP=BV#<$u1esaWy)B}XI`owU%7Vc+p@gMH00jQsvLpM&C;*6SiLg+H7Yk3 z+wxZ*znv`fKN=jl_OQ7zv)Q~*&%gVLk(F#3S%rCB4Ycf$y~zY?UFACD*#qSOGPI&xh9vdmMYuM{ErV;20e%GV0;Ish1bdLrP%`WPa!#2s*Dfk^ zFK=!rv;N-O%JYy04OmNAXF@P#a#h)b$gZ_5EZ zz>FF5Ji$wlJHyIzkd+B#`sY|be3)zfvNHF6>AEuWmZp`N>j?{ozw1rv-#5F^+1P)M zTMV}v-f6haFx$kfZ8yv=bN1aSL(t#bc6Pe+*=KmaVSZ}o(`C5ZFptyu^cZH_I{P8R zhYh0zfBU%qnQQutouA0X831$eo$TyK4IeXn+^}bO(D0C9{!%U`+rjb4l>K0;5?B4m dD}}T5z$=Hf1+Nms{=6STqc~ieiz~Gt`UPixmv*P%L=qtya%FRyx2Lg7W z?Jzq)G`hXaP*;H5%D}+F0Mdg1ATb7pa3J2n1tjDc7#u+Yd<+Z|EEpI(fy@uW3=9gv S2zP-jL&r=|vDCb9M^6A9lP+Tb literal 0 HcmV?d00001 diff --git a/wai/wai-vector/Cargo.toml b/wai/wai-vector/Cargo.toml index 68765194c..101654c62 100644 --- a/wai/wai-vector/Cargo.toml +++ b/wai/wai-vector/Cargo.toml @@ -25,10 +25,16 @@ path = "src/lib.rs" [[bin]] name = "wai-vector" path = "src/bin/wai-vector.rs" -required-features = ["std"] +required-features = ["std", "author"] [features] +default = ["author"] std = [] +# Authoring: the `WSF1` stroke source and its expansion into a `WOS1` (Annex A), the +# in-house sets' drawings, and the corpus generator that needs them. A sink has no +# stroker (§3.14), so a sink's build turns this off (`default-features = false`) and +# keeps the parsers, the raster and the recomputation of every vector. +author = [] # The 32-bit identities of RS1 (§3.5), for a fabric without `i64` and for the model # checker; the unit tests build them too. i32-forms = [] diff --git a/wai/wai-vector/src/bin/wai-vector.rs b/wai/wai-vector/src/bin/wai-vector.rs index c53abfd6b..feea57df9 100644 --- a/wai/wai-vector/src/bin/wai-vector.rs +++ b/wai/wai-vector/src/bin/wai-vector.rs @@ -88,7 +88,7 @@ fn verify(wai: &Path) -> Result<(), String> { } } let mut have: BTreeSet = BTreeSet::new(); - for sub in ["sets", "vectors", "expected"] { + for sub in vectors::GENERATED_DIRS { walk(wai, &corpus.join(sub), &mut have); } walk(wai, &wai.join(SEEDS), &mut have); diff --git a/wai/wai-vector/src/builtin/mod.rs b/wai/wai-vector/src/builtin/mod.rs new file mode 100644 index 000000000..00c3ccab7 --- /dev/null +++ b/wai/wai-vector/src/builtin/mod.rs @@ -0,0 +1,27 @@ +//! The default stroke set (`graphics-vector.md` §4, Annex B): printable ASCII, +//! U+0020–U+007E, each glyph ID the code point it draws. It ships with every reference +//! sink and is always held: a payload names it by SHA-256 and length in a held `SET` +//! record, and a sink resolves it from these bytes. +//! +//! The bytes are the corpus's `sets/stroke-ascii.wos1`, which `wai-vector vectors gen` +//! writes by expanding `sources/stroke-ascii.wsf1` (Annex A). Their digest is +//! [`STROKE_ASCII_SHA256`], which Annex B states; the tests hold the three equal. + +use crate::wvg1::SetStore; + +/// The bundled stroke set, a `WOS1`. +pub const STROKE_ASCII: &[u8] = include_bytes!("../../../vector-conformance/sets/stroke-ascii.wos1"); + +/// Its SHA-256, as Annex B states it. +pub const STROKE_ASCII_SHA256: [u8; 32] = [ + 0x05, 0x47, 0x6c, 0x46, 0x58, 0xef, 0x73, 0x66, 0xc7, 0x70, 0x07, 0x36, 0x48, 0x7e, 0xac, 0x4c, + 0x3c, 0x1b, 0x7d, 0x21, 0x26, 0x49, 0x78, 0xc7, 0xd0, 0x5b, 0xd4, 0xc7, 0x42, 0x77, 0x36, 0xc9, +]; + +/// Its exact length, as a held `SET` record states it. +pub const STROKE_ASCII_LEN: u32 = 105_523; + +/// Hold the bundled sets in `store`, as a reference sink does. +pub fn hold(store: &mut SetStore) { + store.add(STROKE_ASCII.to_vec()); +} diff --git a/wai/wai-vector/src/lib.rs b/wai/wai-vector/src/lib.rs index cd8e65b50..4134ee5f2 100644 --- a/wai/wai-vector/src/lib.rs +++ b/wai/wai-vector/src/lib.rs @@ -14,6 +14,10 @@ //! - [`arith`]: `R_rat`, `R_half_up`, `div255`, the quantisers and the straight-alpha //! channel (§3.0, §3.8, §3.9, §2.9). //! - [`scene`]: a builder that writes canonical payloads (authoring). +//! - [`wsf1`] (feature `author`): the stroke source format and its integer expansion +//! into a `WOS1` (Annex A). +//! - [`builtin`]: the default stroke set's bytes and digest (§4, Annex B), bundled with +//! every reference sink. //! - [`form`]: the JSON authoring form (§2.12, informative). //! - [`vectors`]: the conformance vectors of `../vector-conformance/`, generated and //! recomputed. @@ -27,6 +31,7 @@ extern crate alloc; pub mod arith; pub mod bounds; +pub mod builtin; pub mod clock; pub mod cover; pub mod form; @@ -41,6 +46,8 @@ pub mod sha256; pub mod track; pub mod vectors; pub mod wos1; +#[cfg(feature = "author")] +pub mod wsf1; pub mod wvg1; pub use refuse::{Code, Refusal}; diff --git a/wai/wai-vector/src/vectors.rs b/wai/wai-vector/src/vectors.rs index e4be8395b..226226381 100644 --- a/wai/wai-vector/src/vectors.rs +++ b/wai/wai-vector/src/vectors.rs @@ -3,7 +3,9 @@ //! //! Layout, under `vector-conformance/`: //! - `sets/.wos1`: the outline sets the vectors name. A held set is found by its -//! SHA-256 among these files. +//! SHA-256 among these files. Two of them are drawn here as stroke sources and +//! expanded by Annex A: the ASCII stroke set and the test repertoire. +//! - `sources/.wsf1`: those two sets' `WSF1` sources. //! - `vectors//.wvg1`, with its JSON authoring form (§2.12) beside it as //! `.json`; `vectors//.wos1`, an outline set checked on its own; //! and, in `arith` and `flatten`, `.json`, an arithmetic input. @@ -18,15 +20,29 @@ //! //! [`generate`] writes every file. [`recompute`] gives a vector's expected file from //! the vector alone and the sets, which is what every fabric leg runs. +//! +//! The generator is authoring code. Without the feature `author` (a sink's build) the +//! module keeps [`recompute`] and what it needs, and the generator's entry points +//! are absent. mod arith_vectors; +#[cfg_attr(not(feature = "author"), allow(dead_code))] mod canonical; +#[cfg_attr(not(feature = "author"), allow(dead_code))] mod cost; +#[cfg_attr(not(feature = "author"), allow(dead_code))] mod geometry; pub mod motion; pub mod raster; +#[cfg_attr(not(feature = "author"), allow(dead_code))] mod refusals; +#[cfg(feature = "author")] +pub mod repertoire; pub mod sets; +#[cfg(feature = "author")] +pub mod stroke; +#[cfg(feature = "author")] +pub mod text; use alloc::collections::BTreeMap; use alloc::format; @@ -44,6 +60,8 @@ use crate::wvg1::{self, Doc, SetStore}; pub const CORPUS: &str = "vector-conformance"; /// The fuzz seeds directory, relative to `wai/`. pub const SEEDS: &str = "wai-vector/fuzz/seeds"; +/// The corpus directories the generator writes, every file in them generated. +pub const GENERATED_DIRS: [&str; 4] = ["sets", "sources", "vectors", "expected"]; /// One generated vector: a group, a name, its bytes and extension, and what it pins. pub struct Vector { @@ -67,6 +85,7 @@ impl Vector { } /// Every vector of the corpus, in group order. +#[cfg(feature = "author")] pub fn all_vectors() -> Vec { let mut v = Vec::new(); v.extend(arith_vectors::vectors()); @@ -76,6 +95,7 @@ pub fn all_vectors() -> Vec { v.extend(cost::vectors()); v.extend(raster::vectors()); v.extend(motion::vectors()); + v.extend(text::vectors()); v } @@ -83,9 +103,10 @@ pub fn all_vectors() -> Vec { pub const INPUT_GROUPS: [&str; 2] = ["arith", "flatten"]; /// The groups whose expected files state every frame's digest. -pub const FRAME_GROUPS: [&str; 6] = ["fills", "runs", "composite", "motion", "clock", "slicing"]; +pub const FRAME_GROUPS: [&str; 7] = ["fills", "runs", "composite", "motion", "clock", "slicing", "text"]; /// A resolver holding every set under `sets/`. +#[cfg(feature = "author")] pub fn corpus_store() -> SetStore { let mut s = SetStore::new(); for (_, b) in sets::corpus_sets() { @@ -95,6 +116,7 @@ pub fn corpus_store() -> SetStore { } /// The vector of this name in this group. +#[cfg(feature = "author")] pub fn find(group: &str, name: &str) -> Option { all_vectors().into_iter().find(|v| v.group == group && v.name == name) } @@ -264,6 +286,7 @@ pub fn form_of(bytes: &[u8]) -> String { } /// `index.json`: each vector's name and what it pins, one per line. +#[cfg(feature = "author")] fn index_text(vs: &[&Vector]) -> String { let mut m: BTreeMap<&str, &str> = BTreeMap::new(); for v in vs { @@ -285,9 +308,11 @@ fn index_text(vs: &[&Vector]) -> String { } /// An entry of the tree-wide digest sidecar: its key, its buffer, and its digest fields. +#[cfg(feature = "author")] type SidecarEntry = (String, String, Vec<(&'static str, Vec)>); /// The tree-wide digest sidecar: entries `(key, buffer, [(field, digests)])`. +#[cfg(feature = "author")] fn sidecar_text(entries: &[SidecarEntry]) -> String { let mut s = String::from("{\n"); for (i, (k, buffer, fields)) in entries.iter().enumerate() { @@ -322,6 +347,7 @@ fn sidecar_text(entries: &[SidecarEntry]) -> String { } /// Every file of the corpus and of the fuzz seeds, as `(path relative to wai/, bytes)`. +#[cfg(feature = "author")] pub fn generate() -> Result)>, String> { let mut out: Vec<(String, Vec)> = Vec::new(); let mut store = SetStore::new(); @@ -329,6 +355,9 @@ pub fn generate() -> Result)>, String> { store.add(bytes.clone()); out.push((format!("{CORPUS}/sets/{name}.wos1"), bytes)); } + for (name, bytes) in sets::corpus_sources() { + out.push((format!("{CORPUS}/sources/{name}.wsf1"), bytes)); + } let vs = all_vectors(); let mut groups: Vec<&'static str> = Vec::new(); for v in &vs { diff --git a/wai/wai-vector/src/vectors/repertoire.rs b/wai/wai-vector/src/vectors/repertoire.rs index bfc04efcf..dee016304 100644 --- a/wai/wai-vector/src/vectors/repertoire.rs +++ b/wai/wai-vector/src/vectors/repertoire.rs @@ -24,7 +24,6 @@ //! one run those overlaps over-cover their partly covered edge pixels (the knot of //! §3.13). -use alloc::vec; use alloc::vec::Vec; use super::stroke::{Pen, RADIUS, UPEM2, glyph, source_notes}; @@ -77,7 +76,7 @@ pub fn anchor(base: u16) -> i32 { 0xFE91 | 0xFE97 | 0xFEF3 => 360, 0xFE92 | 0xFE98 | 0xFEF4 => 260, 0xFE8F | 0xFE90 | 0xFE95 | 0xFE96 => 480, - 0x0939 => 560, + 0x0939 => 760, 0x0926 => 480, _ => 300, } @@ -119,14 +118,13 @@ fn beh_like(forms: [u16; 4], n: i32, up: bool, yeh: bool) -> Vec { })); } // Initial and medial: the tooth. - let below = if yeh { 300 } else { 300 }; v.push(glyph(forms[2], 520, |p| { p.m(360, -440).l(360, J).l(0, J); - dots(p, 360, dy(below), n); + dots(p, 360, dy(300), n); })); v.push(glyph(forms[3], 520, |p| { p.m(520, J).l(0, J).m(260, -440).l(260, J); - dots(p, 260, dy(below), n); + dots(p, 260, dy(300), n); })); v } @@ -157,7 +155,7 @@ pub fn repertoire_glyphs() -> Vec { })); // Devanagari. v.push(glyph(0x0902, 0, |p| { - p.dot(0, HL - 280); + p.dot(0, HL - 240); })); v.push(glyph(0x0926, 940, |p| { p.m(0, HL).l(940, HL).m(400, HL).l(400, -880).arc(520, -560, 300, 320, 44, 80).l(280, -40); @@ -166,10 +164,10 @@ pub fn repertoire_glyphs() -> Vec { p.m(0, HL).l(1000, HL).m(380, HL).l(380, -980).l(640, -980).arc(640, -820, 180, 160, 48, 80).l(360, -660).l(740, -200).arc(620, -200, 120, 140, 0, 30); })); v.push(glyph(0x093F, 400, |p| { - p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(560, HL, 360, 300, 32, 60); + p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(520, HL, 320, 300, 32, 58); })); v.push(glyph(0x0940, 400, |p| { - p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(-160, HL, 360, 300, 64, 36); + p.m(0, HL).l(400, HL).m(200, 0).l(200, HL).arc(-120, HL, 320, 300, 64, 38); })); v.push(glyph(CONJUNCT_NDA, 1300, |p| { p.m(0, HL).l(1300, HL).m(320, HL).l(320, -760).l(120, -760); @@ -192,12 +190,12 @@ pub fn repertoire_glyphs() -> Vec { v.extend(beh_like(TEH, 2, true, false)); // Seen: three teeth, and the bowl in the isolated and final forms. v.push(glyph(SEEN[0], 1400, |p| { - p.m(1240, -360).l(1240, J).l(760, J).arc(400, -60, 320, 300, 0, 36); + p.m(1240, -360).l(1240, J).l(720, J).arc(400, J, 320, 260, 0, 36); p.m(1000, -360).l(1000, J); p.m(760, -360).l(760, J); })); v.push(glyph(SEEN[1], 1500, |p| { - p.m(1500, J).l(760, J).arc(400, -60, 320, 300, 0, 36); + p.m(1500, J).l(720, J).arc(400, J, 320, 260, 0, 36); p.m(1300, -360).l(1300, J); p.m(1060, -360).l(1060, J); p.m(820, -360).l(820, J); @@ -444,6 +442,7 @@ pub fn lay_out(set: &Wsf1, shaped: &[Shaped], clusters: &Clusters, rtl: bool, by #[cfg(test)] mod tests { use super::*; + use alloc::vec; #[test] fn the_joining_table_picks_the_forms_of_the_test_words() { diff --git a/wai/wai-vector/src/vectors/sets.rs b/wai/wai-vector/src/vectors/sets.rs index 482e8bb22..363a915e8 100644 --- a/wai/wai-vector/src/vectors/sets.rs +++ b/wai/wai-vector/src/vectors/sets.rs @@ -88,13 +88,28 @@ pub fn set_bad_notes() -> Vec { w.write() } -/// The sets under `sets/`, by name. +/// The sets under `sets/`, by name: the synthetic sets drawn here from formulas, and +/// the two in-house sets expanded from their stroke sources by Annex A. +#[cfg(feature = "author")] pub fn corpus_sets() -> Vec<(&'static str, Vec)> { - vec![("synthetic-a", set_a().write()), ("synthetic-b", set_b().write()), ("synthetic-bad-notes", set_bad_notes())] + vec![ + ("stroke-ascii", super::stroke::ascii_set_bytes()), + ("synthetic-a", set_a().write()), + ("synthetic-b", set_b().write()), + ("synthetic-bad-notes", set_bad_notes()), + ("test-repertoire", super::repertoire::repertoire_set_bytes()), + ] +} + +/// The stroke sources under `sources/`, by name. +#[cfg(feature = "author")] +pub fn corpus_sources() -> Vec<(&'static str, Vec)> { + vec![("stroke-ascii", super::stroke::ascii_source_bytes()), ("test-repertoire", super::repertoire::repertoire_source_bytes())] } /// The fuzz seeds under `wai-vector/fuzz/seeds/`: a payload that uses every table, /// and an outline set with every point tag. +#[cfg(feature = "author")] pub fn fuzz_seeds() -> Vec<(String, Vec)> { vec![(String::from("every-table.wvg1"), super::canonical::every_table().bytes()), (String::from("set-a.wos1"), set_a().write())] } diff --git a/wai/wai-vector/src/vectors/text.rs b/wai/wai-vector/src/vectors/text.rs index 7c1eb19e1..ec444170f 100644 --- a/wai/wai-vector/src/vectors/text.rs +++ b/wai/wai-vector/src/vectors/text.rs @@ -12,7 +12,6 @@ //! Both sets are held, found by SHA-256 among `sets/`. Every string here is written //! here. -use alloc::format; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -233,7 +232,7 @@ pub fn vectors() -> Vec { let (sh, cl) = shape_arabic(word); let (g, c) = lay_out(&rep, &sh, &cl, true, 0); let u = us(10); - let origin = (9 * PX, 26 * PX); + let origin = (9 * PX, 30 * PX); let mut s = frame_doc(96, 48); s.sets = vec![tests.clone()]; s.texts = vec![block(7, "ar", 1, word)]; @@ -285,7 +284,8 @@ pub fn vectors() -> Vec { let (ng, nc, _) = latin(&latin_set, name, 0); let (rg, rc, _) = latin(&latin_set, role, 0); s.items.push(ItemDef::Begin { clip: None, t_on: 0, t_off: end, tx: Some(slide), ty: None, op: Some(fade) }); - s.items.push(ItemDef::Path { path: PathDef::rect((12 * PX) as i16, (128 * PX) as i16, (236 * PX) as i16, (170 * PX) as i16), rule: 0, paint: [20, 40, 120, 220], text: None, t_on: 0, t_off: end, unit_scale: 65_536, slots: NO_SLOTS }); + // The bar, in path units of one design pixel (unit_scale 2^24). + s.items.push(ItemDef::Path { path: PathDef::rect(12, 128, 236, 172), rule: 0, paint: [20, 40, 120, 220], text: None, t_on: 0, t_off: end, unit_scale: 1 << 24, slots: NO_SLOTS }); s.items.push(run(0, 0, place(&ng, (20 * PX, 148 * PX), u1), nc, u1, WHITE, (0, end), NO_SLOTS)); s.items.push(run(0, 1, place(&rg, (20 * PX, 164 * PX), u2), rc, u2, [200, 200, 200, 255], (0, end), NO_SLOTS)); s.items.push(ItemDef::End); @@ -315,8 +315,3 @@ pub fn test_words() -> Vec<(&'static str, Vec)> { ("\u{0628}\u{064A}\u{062A}", vec![BEH[2], YEH[3], TEH[1]]), ] } - -#[allow(dead_code)] -fn _unused() -> String { - format!("{}", 0) -} diff --git a/wai/wai-vector/src/wsf1.rs b/wai/wai-vector/src/wsf1.rs index 4fe506c1a..068140db8 100644 --- a/wai/wai-vector/src/wsf1.rs +++ b/wai/wai-vector/src/wsf1.rs @@ -342,7 +342,7 @@ pub fn isqrt(n: u64) -> u64 { return n; } let mut x = n; - let mut y = x / 2 + 1; + let mut y = x.div_ceil(2); while y < x { x = y; y = (x + n / x) / 2; diff --git a/wai/wai-vector/tests/stroke_sets.rs b/wai/wai-vector/tests/stroke_sets.rs new file mode 100644 index 000000000..c9e493221 --- /dev/null +++ b/wai/wai-vector/tests/stroke_sets.rs @@ -0,0 +1,135 @@ +//! 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 { + 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::>(), (0x20..=0x7E).collect::>()); +} + +#[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 = 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"); + } + } +} diff --git a/wai/wai-vector/wasm/Cargo.toml b/wai/wai-vector/wasm/Cargo.toml index dde479aef..6370e8a21 100644 --- a/wai/wai-vector/wasm/Cargo.toml +++ b/wai/wai-vector/wasm/Cargo.toml @@ -14,7 +14,8 @@ crate-type = ["cdylib"] path = "src/lib.rs" [dependencies] -wai-vector = { path = ".." } +# The recomputation needs no authoring code: a sink's build, with no stroker. +wai-vector = { path = "..", default-features = false } [profile.release] debug = false diff --git a/wai/wai-vector/wasm/verify.mjs b/wai/wai-vector/wasm/verify.mjs index 8369c2bc8..b915e74d7 100644 --- a/wai/wai-vector/wasm/verify.mjs +++ b/wai/wai-vector/wasm/verify.mjs @@ -1,8 +1,10 @@ // Recompute every expected file of vector-conformance under this engine's // WebAssembly, and require each to equal the committed file byte for byte; and the -// authoring form beside each payload. +// authoring form beside each payload. The default stroke set's SHA-256 is required to +// be the one Annex B of the extension states. // // node verify.mjs +import { createHash } from 'node:crypto'; import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -32,6 +34,21 @@ for (const f of readdirSync(join(root, 'sets')).filter((n) => n.endsWith('.wos1' } let vectors = 0, forms = 0, bad = 0; + +// Annex B: the default stroke set's digest. +{ + const doc = readFileSync(join(root, '..', 'extensions', 'graphics-vector.md'), 'utf8'); + const annex = doc.slice(doc.indexOf('## Annex B.')); + const line = annex.split('\n').find((l) => l.includes('the set (`WOS1`)')) || ''; + const stated = (line.match(/[0-9a-f]{64}/) || [''])[0]; + const have = createHash('sha256').update(readFileSync(join(root, 'sets', 'stroke-ascii.wos1'))).digest('hex'); + if (stated.length !== 64 || have !== stated) { + bad += 1; + console.error(`ANNEX B: sets/stroke-ascii.wos1 hashes to ${have}, Annex B states ${stated || 'nothing'}`); + } else { + console.log(`the default stroke set is Annex B's: ${have}`); + } +} for (const group of readdirSync(join(root, 'vectors')).sort()) { for (const f of readdirSync(join(root, 'vectors', group)).sort()) { const dot = f.lastIndexOf('.'); -- 2.54.0 (Apple Git-157) From 442047a526c9e2aff98ee01f05c80c406eb3aecd Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 17:46:35 -0400 Subject: [PATCH 3/6] wai-vector: test that each in-house glyph's expansion covers its pen's path and nothing else Co-Authored-By: Claude Opus 5.5 --- wai/wai-vector/tests/stroke_sets.rs | 86 +++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/wai/wai-vector/tests/stroke_sets.rs b/wai/wai-vector/tests/stroke_sets.rs index c9e493221..6601128a6 100644 --- a/wai/wai-vector/tests/stroke_sets.rs +++ b/wai/wai-vector/tests/stroke_sets.rs @@ -133,3 +133,89 @@ fn the_joining_table_picks_the_forms_of_the_test_words() { } } } + +/// 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::>()); + 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); + } + } +} -- 2.54.0 (Apple Git-157) From 11696ebc426e3e6b66611c80579862e7a518c5e2 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 17:56:24 -0400 Subject: [PATCH 4/6] wai-vector: the sink's build without author is warning-free; clippy-clean in the new modules Co-Authored-By: Claude Opus 5.5 --- wai/wai-vector/src/vectors.rs | 6 +++++- wai/wai-vector/src/vectors/repertoire.rs | 6 +++++- wai/wai-vector/src/vectors/stroke.rs | 1 + wai/wai-vector/src/vectors/text.rs | 7 +++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/wai/wai-vector/src/vectors.rs b/wai/wai-vector/src/vectors.rs index 226226381..20f4384ad 100644 --- a/wai/wai-vector/src/vectors.rs +++ b/wai/wai-vector/src/vectors.rs @@ -25,6 +25,7 @@ //! module keeps [`recompute`] and what it needs, and the generator's entry points //! are absent. +#[cfg_attr(not(feature = "author"), allow(dead_code))] mod arith_vectors; #[cfg_attr(not(feature = "author"), allow(dead_code))] mod canonical; @@ -44,6 +45,7 @@ pub mod stroke; #[cfg(feature = "author")] pub mod text; +#[cfg(feature = "author")] use alloc::collections::BTreeMap; use alloc::format; use alloc::string::String; @@ -51,7 +53,9 @@ use alloc::vec::Vec; use crate::clock; use crate::form; -use crate::json::{self, Value, hex, int, obj, string}; +#[cfg(feature = "author")] +use crate::json; +use crate::json::{Value, hex, int, obj, string}; use crate::sha256::sha256; use crate::wos1; use crate::wvg1::{self, Doc, SetStore}; diff --git a/wai/wai-vector/src/vectors/repertoire.rs b/wai/wai-vector/src/vectors/repertoire.rs index dee016304..90f716b89 100644 --- a/wai/wai-vector/src/vectors/repertoire.rs +++ b/wai/wai-vector/src/vectors/repertoire.rs @@ -288,6 +288,10 @@ pub struct Shaped { /// The clusters of a shaped string, in logical order: `(byte_first, byte_len)`. pub type Clusters = Vec<(u32, u32)>; +/// A run's glyph records, `(gid, x, y)`, and its clusters, `(glyph_count, byte_first, +/// byte_len)`, as a run item takes them. +pub type RunGlyphs = (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>); + fn joining(c: char) -> Option<(Joining, [u16; 4])> { JOINING.iter().find(|(l, _, _)| *l == c).map(|&(_, j, f)| (j, f)) } @@ -406,7 +410,7 @@ pub fn shape_devanagari(s: &str) -> (Vec, Clusters) { /// shaped glyphs in logical order. A right-to-left run reverses the clusters; within a /// cluster a mark keeps its base's position plus its anchor, and the other glyphs /// advance. -pub fn lay_out(set: &Wsf1, shaped: &[Shaped], clusters: &Clusters, rtl: bool, byte_offset: u32) -> (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>) { +pub fn lay_out(set: &Wsf1, shaped: &[Shaped], clusters: &Clusters, rtl: bool, byte_offset: u32) -> RunGlyphs { let mut order: Vec = (0..clusters.len()).collect(); if rtl { order.reverse(); diff --git a/wai/wai-vector/src/vectors/stroke.rs b/wai/wai-vector/src/vectors/stroke.rs index 2b779e91d..907cf37a9 100644 --- a/wai/wai-vector/src/vectors/stroke.rs +++ b/wai/wai-vector/src/vectors/stroke.rs @@ -59,6 +59,7 @@ impl Pen { } /// Ellipse points from direction `k0` to `k1` in steps of `step` table directions, /// `k1` always included; drawn on from the current point. + #[allow(clippy::too_many_arguments)] pub fn arc_s(&mut self, cx: i32, cy: i32, rx: i32, ry: i32, k0: i32, k1: i32, step: i32) -> &mut Pen { let s = if k1 >= k0 { step } else { -step }; let mut k = k0; diff --git a/wai/wai-vector/src/vectors/text.rs b/wai/wai-vector/src/vectors/text.rs index ec444170f..e77345a0e 100644 --- a/wai/wai-vector/src/vectors/text.rs +++ b/wai/wai-vector/src/vectors/text.rs @@ -36,9 +36,12 @@ fn place(glyphs: &[(u16, i32, i32)], origin: (i32, i32), unit_scale: u32) -> Vec glyphs.iter().map(|&(g, x, y)| (g, origin.0 + r_rat(i64::from(x) * s, 65_536) as i32, origin.1 + r_rat(i64::from(y) * s, 65_536) as i32)).collect() } +/// A Latin run's glyph records and clusters, and its width in outline units. +type Latin = (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>, i32); + /// A Latin string in the stroke set: one glyph per byte, its ID the byte, advancing -/// left to right; one cluster per glyph from `byte_offset`. -fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>, i32) { +/// left to right; one cluster per glyph from `byte_offset`; and its width. +fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> Latin { let mut pen = 0i32; let mut g = Vec::new(); let mut c = Vec::new(); -- 2.54.0 (Apple Git-157) From 93a8d5929b95cc853ae3ee25532ed9817f24a218 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 22:20:56 -0400 Subject: [PATCH 5/6] wai_vector_check: render the text group, which the corpus README names among the groups that render The checker's list of rendering groups is read from the README's layout. D7-5 adds the text group to the corpus and to that sentence of the README, so the list gains it. No computation changes. Co-Authored-By: Claude Opus 5.5 --- wai/tools/wai_vector_check.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wai/tools/wai_vector_check.py b/wai/tools/wai_vector_check.py index 72de2978c..737ead53d 100644 --- a/wai/tools/wai_vector_check.py +++ b/wai/tools/wai_vector_check.py @@ -2040,7 +2040,8 @@ def selftest(): # at which the `alpha` vector probes the quantisers are listed only in its expected # file, so they are read from there and the values at them recomputed. -RENDER_GROUPS = ("clock", "composite", "fills", "motion", "runs", "slicing") +# The groups the corpus README names as rendering: their expected files list frames. +RENDER_GROUPS = ("clock", "composite", "fills", "motion", "runs", "slicing", "text") RGBA8S_GROUPS = ("composite",) -- 2.54.0 (Apple Git-157) From 14d085682866098ed0b9299de0a2d5b3cbfd0bc6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 11 Oct 2026 22:28:12 -0400 Subject: [PATCH 6/6] wai-vector: the joining test expects Unicode's code points for each form, written out Falsifying it showed the expectations named the table's own constants, so a swapped form changed the table and its expectation together and the test stayed green. Co-Authored-By: Claude Opus 5.5 --- wai/wai-vector/src/vectors/repertoire.rs | 13 ++++--------- wai/wai-vector/src/vectors/text.rs | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/wai/wai-vector/src/vectors/repertoire.rs b/wai/wai-vector/src/vectors/repertoire.rs index 90f716b89..222560470 100644 --- a/wai/wai-vector/src/vectors/repertoire.rs +++ b/wai/wai-vector/src/vectors/repertoire.rs @@ -450,15 +450,10 @@ mod tests { #[test] fn the_joining_table_picks_the_forms_of_the_test_words() { - let gids = |s: &str| shape_arabic(s).0.iter().map(|g| g.gid).collect::>(); - // بَيْت: beh initial with fatha, yeh medial with sukun, teh final. - assert_eq!(gids("\u{0628}\u{064E}\u{064A}\u{0652}\u{062A}"), vec![BEH[2], FATHA, YEH[3], SUKUN, TEH[1]]); - // سلام: seen initial, lam-alef final, meem isolated (alef joins nothing after it). - assert_eq!(gids("\u{0633}\u{0644}\u{0627}\u{0645}"), vec![SEEN[2], LAM_ALEF[1], MEEM[0]]); - // A lam-alef that begins a word is isolated, and a lone beh is isolated. - assert_eq!(gids("\u{0644}\u{0627} \u{0628}"), vec![LAM_ALEF[0], SPACE, BEH[0]]); - // Medial and final forms in a longer join: beh beh beh. - assert_eq!(gids("\u{0628}\u{0628}\u{0628}"), vec![BEH[2], BEH[3], BEH[1]]); + // The expected IDs are Unicode's code points for each form, written out. + for (word, want) in super::super::text::test_words() { + assert_eq!(shape_arabic(word).0.iter().map(|g| g.gid).collect::>(), want, "{word}"); + } } #[test] diff --git a/wai/wai-vector/src/vectors/text.rs b/wai/wai-vector/src/vectors/text.rs index e77345a0e..aa2388795 100644 --- a/wai/wai-vector/src/vectors/text.rs +++ b/wai/wai-vector/src/vectors/text.rs @@ -309,12 +309,20 @@ pub fn vectors() -> Vec { } /// The test words' shaping, checked by the joining-table test: `(text, gids in logical -/// order)`. +/// order)`. The expected glyph IDs are written as the code points Unicode assigns to +/// each contextual form, never through the table's own constants, so a table that +/// names the wrong form cannot also make its own expectation. pub fn test_words() -> Vec<(&'static str, Vec)> { - use repertoire::*; vec![ - ("\u{0628}\u{064E}\u{064A}\u{0652}\u{062A}", vec![BEH[2], FATHA, YEH[3], SUKUN, TEH[1]]), - ("\u{0633}\u{0644}\u{0627}\u{0645}", vec![SEEN[2], LAM_ALEF[1], MEEM[0]]), - ("\u{0628}\u{064A}\u{062A}", vec![BEH[2], YEH[3], TEH[1]]), + // بَيْت: beh initial, fatha, yeh medial, sukun, teh final. + ("\u{0628}\u{064E}\u{064A}\u{0652}\u{062A}", vec![0xFE91, 0x064E, 0xFEF4, 0x0652, 0xFE96]), + // سلام: seen initial, lam-alef final, meem isolated. + ("\u{0633}\u{0644}\u{0627}\u{0645}", vec![0xFEB3, 0xFEFC, 0xFEE1]), + // بيت: beh initial, yeh medial, teh final. + ("\u{0628}\u{064A}\u{062A}", vec![0xFE91, 0xFEF4, 0xFE96]), + // A lam-alef that begins a word is isolated; a lone beh is isolated. + ("\u{0644}\u{0627} \u{0628}", vec![0xFEFB, 0x0020, 0xFE8F]), + // beh beh beh: initial, medial, final. + ("\u{0628}\u{0628}\u{0628}", vec![0xFE91, 0xFE92, 0xFE90]), ] } -- 2.54.0 (Apple Git-157)