open-standards/wai/wai-vector/tests/raster.rs
Transaction Science 82aee67cde
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no comparison (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no company or product (pull_request) Has been cancelled
standards workspaces build + test / jouleclaw-rs (pull_request) Has been cancelled
standards workspaces build + test / aac-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / joulecontract-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / map-rs (pull_request) Has been cancelled
standards workspaces build + test / mesh-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI section references / cited sections exist (pull_request) Has been cancelled
wai-vector vector graphics / linux-aarch64 (pull_request) Has been cancelled
wai-vector vector graphics / linux-x86_64 (pull_request) Has been cancelled
wai-vector vector graphics / macos-arm64 (pull_request) Has been cancelled
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
wai-vector vector graphics / wasm32 under Node (pull_request) Has been cancelled
wai-vector vector graphics / fuzz, peak allocation, comparison count (pull_request) Has been cancelled
workflow hygiene / every workflow supersedes its own queue (pull_request) Has been cancelled
Merge wai/d7-2-vector-formats into wai/d7-3-vector-raster; clippy-clean raster
The D7-2 cleanup (no generated byte changes), with the fuzz seed
conflict resolved to D7-2's literal; the raster's chunked loops on
as_chunks, collapsed conditionals, and no placeholder bindings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 04:31:54 -04:00

242 lines
11 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

//! The raster stage family RS1–RS9 and the frame procedure (§3.5–§3.11): the values
//! and properties each test names, on the vectors of `vector-conformance/` and on
//! shapes built here. Reference values for the negative-coordinate and fold cases come
//! from review A's independent implementation of §3.3–§3.8.
use wai_vector::arith::div255;
use wai_vector::cover::{self, Cov};
use wai_vector::frame::{render, render_request};
use wai_vector::geom::Pt;
use wai_vector::refuse::Code;
use wai_vector::vectors::raster::{comb, height_for_alpha, j3_triples};
use wai_vector::vectors::{corpus_store, find};
use wai_vector::wvg1::{self, Loaded};
fn load(group: &str, name: &str) -> Loaded {
let v = find(group, name).unwrap_or_else(|| panic!("no vector {group}/{name}"));
wvg1::load(&v.bytes, &mut corpus_store()).unwrap_or_else(|e| panic!("{group}/{name}: {e:?}"))
}
fn closed(pts: &[(i64, i64)]) -> Vec<Pt> {
let mut v: Vec<Pt> = pts.iter().map(|&(x, y)| [x, y]).collect();
v.push(v[0]);
v
}
fn dense(c: &Cov, w: i64, h: i64) -> Vec<Vec<i32>> {
(0..h).map(|r| (0..w).map(|x| c.at(x, r)).collect()).collect()
}
/// Rows and cells at negative coordinates: the crossings are floors of negative
/// rationals. The values are review A's `core.py` on the same polygons.
#[test]
fn rows_and_cells_at_negative_coordinates() {
let a = closed(&[(-700, -300), (900, 200), (300, 1100), (-1000, 800)]);
let want_a = vec![
vec![-131072, -111416, -70656, -18200, 0],
vec![-131072, -131072, -122332, -13254, 0],
vec![-131072, -131072, -48384, 0, 0],
vec![-131072, -91706, -117, 0, 0],
vec![-18176, -10004, 0, 0, 0],
];
assert_eq!(dense(&cover::accumulate(&[a], 5, 5, 4), 5, 5), want_a);
let c = closed(&[(-1500, -1300), (700, -200), (-301, 1333)]);
let want_c = vec![vec![-131072, -112482, -4959, 0], vec![-130379, -32558, 0, 0], vec![-77312, 0, 0, 0], vec![-6834, 0, 0, 0], vec![0; 4], vec![0; 4]];
assert_eq!(dense(&cover::accumulate(&[c], 4, 6, 3), 4, 6), want_c);
}
/// The left fold: a fill crossing the left edge has, on every canvas pixel, the
/// values of the same fill moved 8 pixels right onto a wider canvas, unclipped.
#[test]
fn the_left_fold() {
let d = [(-1800, 100), (600, 350), (200, 900), (-2600, 700)];
let folded = cover::accumulate(&[closed(&d)], 4, 4, 4);
let moved: Vec<(i64, i64)> = d.iter().map(|&(x, y)| (x + 8 * 256, y)).collect();
let wide = cover::accumulate(&[closed(&moved)], 12, 4, 4);
for r in 0..4 {
for c in 0..4 {
assert_eq!(folded.at(c, r), wide.at(c + 8, r), "pixel ({c}, {r})");
}
}
// review A's core.py on the same polygon.
assert_eq!(dense(&folded, 4, 4), vec![vec![0, 0, 0, 0], vec![-108288, -93460, -11440, 0], vec![-131072, -68096, 0, 0], vec![-60272, -2200, 0, 0]]);
// A contour wholly left of the canvas folds to zero sums: it draws nothing (P5).
let left = cover::accumulate(&[closed(&[(-30_000, 1_000), (-25_000, 1_000), (-26_000, 3_000)])], 4, 4, 3);
assert!(left.v.iter().all(|&x| x == 0));
}
/// A-F5's triangle pins `v_k`: the snapped polygon's area, `|v|` = 1280 and 512 in its
/// two pixels, not the outline's (1152 and 384).
#[test]
fn triangle_pins_v_to_the_snapped_polygon() {
let t = cover::accumulate(&[closed(&[(0, 0), (512, 3), (0, 3)])], 4, 1, 3);
let v = [t.at(0, 0).abs(), t.at(1, 0).abs()];
assert_eq!(v, [1280, 512]);
assert_ne!(v, [1152, 384], "an exact-area value");
// The vector renders the same: alpha 2 and 1 in a white fill.
let l = load("fills", "triangle-af5");
let px = render(&l, 0, 0, 0).unwrap();
assert_eq!(&px[0..8], &[2, 2, 2, 2, 1, 1, 1, 1]);
}
/// The glyph-wise union: three glyphs of one run abutting mid-pixel leave no seam; two
/// runs abutting composite by source-over and do.
#[test]
fn abutting_run_seam_pixel_is_255() {
let l = load("runs", "abutting");
let px = render(&l, 0, 0, 0).unwrap();
let w = 16;
// Glyph 1 spans x [0, 384) and y [512, 1536): rows 2..=5. The glyphs at 0, 384 and
// 768 meet mid-pixel at x = 384 (column 1); the last ends at 1152, half of column 4.
for r in 2..6 {
let at = 4 * (r * w + 1);
assert_eq!(&px[at..at + 4], &[255, 255, 255, 255], "the seam at (1, {r})");
let at = 4 * (r * w + 4);
assert_eq!(&px[at..at + 4], &[128, 128, 128, 128], "half of (4, {r})");
}
// The same two glyphs in two runs: source-over at the seam, 128 + div255(128·127).
let l = load("runs", "two-runs");
let px = render(&l, 0, 0, 0).unwrap();
let at = 4 * (2 * w + 1);
let over = 128 + div255(128 * 127) as u8;
assert_eq!(&px[at..at + 4], &[over; 4], "across runs the seam returns");
assert!(over < 255);
}
/// J3's discriminating triples: each pixel has the frozen fill order's bytes,
/// `div255(div255(p·o)·α)`, which the other order does not give.
#[test]
fn j3_triples_and_group_isolation() {
let l = load("composite", "j3-triples");
let px = render(&l, 0, 0, 0).unwrap();
for (i, (p, o, a)) in j3_triples().into_iter().enumerate() {
assert!(height_for_alpha(a) > 0);
let frozen = div255(div255(u32::from(p) * u32::from(o)) * u32::from(a)) as u8;
let other = div255(u32::from(p) * div255(u32::from(a) * u32::from(o))) as u8;
assert_ne!(frozen, other);
let at = 4 * (2 * i);
assert_eq!(&px[at..at + 4], &[frozen; 4], "triple {i}: ({p}, {o}, {a})");
}
// Isolation: the overlap of two opaque fills in a group at 128 is the top fill at
// half, div255(c·128), the group composited as one layer.
let l = load("composite", "isolation");
let px = render(&l, 0, 0, 0).unwrap();
let at = 4 * (16 * 3 + 7);
let green = [40u32, 180, 80, 255].map(|c| div255(c * 128) as u8);
assert_eq!(&px[at..at + 4], &green);
}
/// P1: reversing every contour of a fill changes no byte of any frame.
#[test]
fn whole_fill_reversal_changes_nothing() {
let mut n = 0;
for name in ["rect", "hole", "star-nonzero", "star-evenodd", "random-0", "random-1", "random-2", "random-3", "curves"] {
let (a, b) = (load("fills", name), load("fills", &format!("{name}-reversed")));
assert_ne!(a.doc.write(), b.doc.write());
for z in 0..a.doc.sizes.len() {
assert_eq!(render(&a, z, 0, 0).unwrap(), render(&b, z, 0, 0).unwrap(), "{name}");
n += 1;
}
}
assert_eq!(n, 9);
}
/// P2: a roll at an integer speed with keys on frame ticks: frame k is frame 0 moved
/// up 2k pixels at the design size and 4k at twice it.
#[test]
fn integer_speed_roll_shifts_whole_pixels() {
let l = load("motion", "roll-integer");
for (z, step) in [(0usize, 2usize), (1, 4)] {
let (w, h) = (l.doc.sizes[z].w as usize, l.doc.sizes[z].h as usize);
let f0 = render(&l, z, 0, 0).unwrap();
for k in 1..10u64 {
let fk = render(&l, z, 0, k).unwrap();
let s = step * k as usize;
for r in 0..h {
let want: &[u8] = if r + s < h { &f0[4 * w * (r + s)..4 * w * (r + s + 1)] } else { &[0u8; 0] };
let got = &fk[4 * w * r..4 * w * (r + 1)];
if want.is_empty() {
assert!(got.iter().all(|&x| x == 0), "size {z}, frame {k}, row {r}: below the moved content");
} else {
assert_eq!(got, want, "size {z}, frame {k}, row {r}");
}
}
}
}
}
/// P7: each piece's frames equal the whole's at the same frame number.
#[test]
fn slicing_piece_frames_equal_the_whole() {
let whole = load("slicing", "whole");
let mut n = 0;
for i in 0..3 {
let p = load("slicing", &format!("piece-{i}"));
for (r, rate) in p.doc.rates.iter().enumerate() {
let ticks = rate.ticks_per_frame(p.doc.header.tb);
for g in wai_vector::clock::frames(p.doc.header.t0, p.doc.header.dur, ticks) {
assert_eq!(render(&p, 0, r, g).unwrap(), render(&whole, 0, r, g).unwrap(), "piece {i}, rate {r}, frame {g}");
n += 1;
}
}
}
assert_eq!(n, 90);
}
/// §3.7: the comb of 16,382 points attains `D_r = 256·p`, and renders with every
/// value in `i32` (a debug build checks each addition).
#[test]
fn the_comb_attains_the_density_bound() {
let pts: Vec<Pt> = comb(16_382, 0, 1, 512, 768).iter().map(|p| [i64::from(p.x), i64::from(p.y)]).chain(std::iter::once([0, 512])).collect();
let d = cover::row_density(std::slice::from_ref(&pts), 0, 15);
assert_eq!(d[2], 256 * 16_382);
assert_eq!(d.iter().copied().max(), Some(256 * 16_382));
let c = cover::accumulate(&[pts], 64, 16, 16_382);
assert!(c.v.iter().all(|&x| x.unsigned_abs() <= 512 * 256 * 16_382));
let l = load("fills", "comb-16382");
render(&l, 0, 0, 0).unwrap();
let l = load("fills", "points-16383");
render(&l, 0, 0, 0).unwrap();
}
/// The density assertion is live: a comb of 16,384 points, built here past the cap a
/// payload enforces, attains `D_r = 2^22 > 2^22 − 256`, and a debug build stops it.
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "D_r = 4194304")]
fn the_density_assertion_fires_past_the_cap() {
let pts: Vec<Pt> = comb(16_384, 0, 1, 512, 768).iter().map(|p| [i64::from(p.x), i64::from(p.y)]).chain(std::iter::once([0, 512])).collect();
cover::accumulate(&[pts], 64, 16, 16_384);
}
/// The render request errors, and the frame range by division.
#[test]
fn render_requests() {
let l = load("motion", "roll-integer");
assert!(render_request(&l, 160, 90, 25, 1, 0).is_ok());
assert_eq!(render_request(&l, 161, 90, 25, 1, 0).unwrap_err().code, Code::SizeUnlisted);
assert_eq!(render_request(&l, 160, 90, 30, 1, 0).unwrap_err().code, Code::RateUnlisted);
assert_eq!(render_request(&l, 160, 90, 25, 1, 10).unwrap_err().code, Code::FrameRange);
assert_eq!(render_request(&l, 160, 90, 25, 1, u64::MAX).unwrap_err().code, Code::FrameRange);
}
/// RS9 and P4: every rendered pixel has `R, G, B ≤ A`, and the straight-alpha view is
/// `R_rat(255·c, A)`.
#[test]
fn premultiplied_closure_and_the_straight_view() {
for (g, n) in [("composite", "groups-depth-4"), ("composite", "overlap-opacity"), ("composite", "group-pop"), ("motion", "lower-third")] {
let l = load(g, n);
let px = render(&l, 0, 0, 0).unwrap();
assert!(px.as_chunks::<4>().0.iter().all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3]), "{n}");
let s = wai_vector::paint::unpremul_buffer(&px);
for (p, q) in px.as_chunks::<4>().0.iter().zip(s.as_chunks::<4>().0) {
if p[3] == 0 {
assert_eq!(q, &[0, 0, 0, 0]);
} else {
for ch in 0..3 {
assert_eq!(i64::from(q[ch]), wai_vector::arith::r_rat(255 * i64::from(p[ch]), i64::from(p[3])));
}
}
}
}
}