//! 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 { let mut v: Vec = pts.iter().map(|&(x, y)| [x, y]).collect(); v.push(v[0]); v } fn dense(c: &Cov, w: i64, h: i64) -> Vec> { (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 = 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 = 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]))); } } } } }