pdfrum_render/scanline.rs
1//! The analytic cell rasterizer: exact per-pixel area and cover, in the style
2//! the oracle's scan converter uses.
3//!
4//! **Part of the backend seam.** A backend that owns its pixels rasterizes
5//! with [`Rasterizer`] rather than with a rasterizer of its own, because the
6//! coverage a page's edges get is exactly what the cross-backend equality
7//! property compares. `pdfrum-raster-agg` is the in-tree caller.
8//!
9//! Coverage is *integrated* from the polygon's edges, not sampled: a pixel's
10//! coverage is the real number the geometry implies, quantised only by the
11//! output byte. The sweep is all-integer, so the same path always produces
12//! the same bytes on every machine.
13
14// A supersampler asks a fixed grid of sample points and counts hits, which
15// quantises coverage to the number of samples it took. `tiny-skia`
16// supersamples at four subsamples per axis, so a diagonal edge has seventeen
17// distinct coverage levels and a half-covered pixel lands on 8/16 of the
18// range, where the oracle's own edge writes the exact half. Over the corpus
19// that is a persistent few-count spread along every non-axis-aligned edge.
20//
21// # The representation
22//
23// Each pixel the polygon's boundary crosses gets a `Cell` carrying two
24// integers:
25//
26// - **`cover`** — the net signed vertical distance the boundary travelled
27// through this pixel, in `SUBPIXEL_SCALE`ths of a pixel. Summing `cover`
28// left to right along a scanline gives the winding number, scaled, at every
29// point to the right of the cell.
30// - **`area`** — twice the signed area the boundary swept *inside* this
31// pixel, in the same units squared. It is the correction that turns the
32// running `cover` into the exact coverage of the boundary pixel itself.
33//
34// A cell is therefore a *difference*: interior pixels between two boundaries
35// carry no cell at all, and their coverage falls out of the running sum. That
36// is what makes the sweep linear in the boundary rather than in the area.
37//
38// It lives in the engine rather than in a backend because `crate::glyph`
39// rasterizes every small glyph into an alpha bitmap that must be identical
40// under all three rasterizers — the oracle's own glyph bitmap comes from
41// FreeType rather than from whatever draws the page's paths. Same argument as
42// `crate::blend::composite_premultiplied`: a decision the *engine* takes has
43// one implementation the engine owns.
44
45use core::ops::Range;
46
47use store::CellStore;
48
49mod store;
50
51/// The subpixel grid the rasterizer works on: 256 steps per pixel per axis.
52///
53/// Coordinates arrive as `f64` device pixels and are scaled by this and
54/// truncated, so the rasterizer's whole interior is integer arithmetic. 256
55/// is the oracle's own `poly_base_size`, and it is load-bearing rather than a
56/// tunable: [`coverage_to_alpha`]'s mapping is derived from this scale, and
57/// changing it would change every antialiased edge byte in the corpus.
58pub(crate) const SUBPIXEL_SCALE: i32 = 256;
59
60/// `log2(SUBPIXEL_SCALE)`, the shift the coordinate conversion uses.
61pub(crate) const SUBPIXEL_SHIFT: u32 = 8;
62
63/// The low bits of a subpixel coordinate: its position within its pixel.
64pub(crate) const SUBPIXEL_MASK: i32 = SUBPIXEL_SCALE - 1;
65
66/// One pixel's accumulated boundary contribution.
67///
68/// `cover` and `area` are signed because a boundary crossing downward
69/// contributes the negative of one crossing upward, which is what makes the
70/// winding number fall out of a running sum.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub(crate) struct Cell {
73 /// Pixel column.
74 pub x: i32,
75 /// Pixel row.
76 pub y: i32,
77 /// Net signed vertical travel through this pixel, in subpixel units.
78 pub cover: i32,
79 /// Twice the signed swept area inside this pixel, in subpixel units
80 /// squared.
81 pub area: i32,
82}
83
84impl Cell {
85 /// A fresh, empty cell at a position.
86 const fn at(x: i32, y: i32) -> Self {
87 Self {
88 x,
89 y,
90 cover: 0,
91 area: 0,
92 }
93 }
94
95 /// Whether this cell would contribute nothing to the sweep.
96 const fn is_empty(self) -> bool {
97 self.cover == 0 && self.area == 0
98 }
99}
100
101/// Accumulates a path's boundary into cells.
102///
103/// The lifecycle is: [`Rasterizer::move_to`] and [`Rasterizer::line_to`] for
104/// each flattened subpath, [`Rasterizer::close_polygon`] to close it, then
105/// [`Rasterizer::sweep`] to turn the cells into spans. Curves are flattened by
106/// the caller — this type sees only straight segments, which is also all the
107/// oracle's scan converter sees.
108///
109/// A caller that will throw away the spans outside a band of rows should say
110/// which band with [`Rasterizer::keep_rows`], and pay for the rows it keeps
111/// rather than for the rows the path reaches.
112///
113/// ```
114/// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
115///
116/// let mut rasterizer = Rasterizer::new();
117/// rasterizer.move_to(0.0, 0.0);
118/// rasterizer.line_to(4.0, 0.0);
119/// rasterizer.line_to(4.0, 2.0);
120/// rasterizer.line_to(0.0, 2.0);
121/// rasterizer.close_polygon();
122///
123/// let mut spans = Vec::new();
124/// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, alpha| {
125/// spans.push((x, y, len, alpha));
126/// });
127///
128/// // A 4x2 rectangle: two full rows, no partial coverage anywhere.
129/// assert_eq!(spans, [(0, 0, 4, 255), (0, 1, 4, 255)]);
130/// ```
131#[derive(Debug, Default)]
132pub struct Rasterizer {
133 store: CellStore,
134 /// The cell currently being accumulated into, held out of the store so a
135 /// run of segments crossing one pixel costs no lookup.
136 current: Option<Cell>,
137 /// The rows the caller keeps, as a half-open range of pixel rows; `None`
138 /// keeps every row. See [`Rasterizer::keep_rows`].
139 keep: Option<Range<i32>>,
140 /// Where the pen is, in subpixel coordinates.
141 x: i32,
142 y: i32,
143 /// Where the current subpath started, for `close_polygon`.
144 start_x: i32,
145 start_y: i32,
146 /// Whether a subpath is open — a `line_to` without a preceding `move_to`
147 /// is ignored rather than treated as starting at the origin.
148 open: bool,
149}
150
151/// The largest device coordinate [`to_subpixel`] will carry, in pixels.
152///
153/// The subpixel grid must hold `coordinate * 256` without overflowing `i32`,
154/// and the sweep additionally sums covers across a scanline, so the bound is
155/// set well inside `i32::MAX / SUBPIXEL_SCALE` to leave headroom for both. The
156/// engine's own ±32000 clamp is two orders of magnitude tighter, so this is
157/// reached only by a coordinate that clamp did not see.
158const COORDINATE_LIMIT: f64 = (1i32 << 22) as f64;
159
160/// A device coordinate as a subpixel one.
161///
162/// Truncating toward zero, matching the oracle's `int(c * 256)` rather than
163/// rounding. The clamp keeps a coordinate the engine's own ±32000 bound
164/// somehow missed from overflowing the multiply; it can only be reached by a
165/// non-finite value, which becomes zero.
166#[must_use]
167pub(crate) fn to_subpixel(v: f64) -> i32 {
168 if !v.is_finite() {
169 return 0;
170 }
171 let scaled = v.clamp(-COORDINATE_LIMIT, COORDINATE_LIMIT) * f64::from(SUBPIXEL_SCALE);
172 #[expect(
173 clippy::cast_possible_truncation,
174 reason = "the clamp bounds the product to +/-2^30; truncation toward \
175 zero is the ported conversion, not an accident"
176 )]
177 let out = scaled as i32;
178 out
179}
180
181impl Rasterizer {
182 /// A rasterizer with no cells.
183 ///
184 /// ```
185 /// use pdfrum_render::scanline::Rasterizer;
186 ///
187 /// assert!(Rasterizer::new().is_empty());
188 /// ```
189 #[must_use]
190 pub fn new() -> Self {
191 Self::default()
192 }
193
194 /// Forget every cell, keeping the allocation and the kept row range.
195 ///
196 /// ```
197 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
198 ///
199 /// let mut rasterizer = Rasterizer::new();
200 /// rasterizer.move_to(0.0, 0.0);
201 /// rasterizer.line_to(4.0, 0.0);
202 /// rasterizer.line_to(4.0, 2.0);
203 /// rasterizer.line_to(0.0, 2.0);
204 /// rasterizer.close_polygon();
205 ///
206 /// let mut spans = Vec::new();
207 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, alpha| {
208 /// spans.push((x, y, len, alpha));
209 /// });
210 ///
211 /// // The allocation and the kept row range survive; the cells do not.
212 /// rasterizer.reset();
213 /// assert!(rasterizer.is_empty());
214 /// ```
215 pub fn reset(&mut self) {
216 self.store.clear();
217 self.current = None;
218 self.open = false;
219 }
220
221 /// Keep only the cells on rows in `rows`, a half-open range of pixel rows.
222 ///
223 /// This is a promise about the *caller*, not a change to the geometry: it
224 /// says the caller will discard every span outside `rows` anyway, so the
225 /// rasterizer may drop those cells rather than sort and sweep them. The
226 /// spans that do come out are byte-for-byte the ones an unrestricted
227 /// rasterizer emits, because [`Rasterizer::sweep`] resolves each row from
228 /// that row's cells alone — the running cover is reset at every row
229 /// boundary, so a dropped row can change no other.
230 ///
231 /// It is worth setting whenever the path may extend far outside the
232 /// target: a path whose bounding box is tens of thousands of rows tall on
233 /// an 842-row page otherwise pays for every row it crosses.
234 ///
235 /// Only rows are restricted. A span too far left or right still costs its
236 /// cells, because the horizontal extent is bounded by the path's own
237 /// segment count rather than by the rows it crosses.
238 ///
239 /// ```
240 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
241 ///
242 /// // A promise about the caller: it will discard every span outside
243 /// // row 0 anyway, so those cells need not be sorted or swept.
244 /// let mut rasterizer = Rasterizer::new();
245 /// rasterizer.keep_rows(0..1);
246 /// rasterizer.move_to(0.0, 0.0);
247 /// rasterizer.line_to(4.0, 0.0);
248 /// rasterizer.line_to(4.0, 2.0);
249 /// rasterizer.line_to(0.0, 2.0);
250 /// rasterizer.close_polygon();
251 ///
252 /// let mut rows = Vec::new();
253 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |_, _, y, _| rows.push(y));
254 /// // Byte-for-byte the spans an unrestricted rasterizer emits for row 0.
255 /// assert_eq!(rows, [0]);
256 /// ```
257 pub fn keep_rows(&mut self, rows: Range<i32>) {
258 self.keep = Some(rows);
259 }
260
261 /// Whether a row would survive [`Rasterizer::keep_rows`].
262 fn kept(&self, y: i32) -> bool {
263 self.keep.as_ref().is_none_or(|rows| rows.contains(&y))
264 }
265
266 /// Bank a cell, unless its row is one the caller discards.
267 fn bank(&mut self, cell: Cell) {
268 if !cell.is_empty() && self.kept(cell.y) {
269 self.store.push(cell);
270 }
271 }
272
273 /// Whether any boundary has been accumulated.
274 ///
275 /// A path entirely outside [`Rasterizer::keep_rows`]'s range is empty by
276 /// this test once it has been swept, which is what it means for the caller
277 /// to have said those rows do not matter.
278 ///
279 /// ```
280 /// use pdfrum_render::scanline::Rasterizer;
281 ///
282 /// let mut rasterizer = Rasterizer::new();
283 /// assert!(rasterizer.is_empty());
284 /// rasterizer.move_to(0.0, 0.0);
285 /// rasterizer.line_to(4.0, 2.0);
286 /// rasterizer.close_polygon();
287 /// assert!(!rasterizer.is_empty());
288 /// ```
289 #[must_use]
290 pub fn is_empty(&self) -> bool {
291 self.store.is_empty() && self.current.is_none_or(Cell::is_empty)
292 }
293
294 /// Start a new subpath at a device-space point.
295 ///
296 /// ```
297 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
298 ///
299 /// let mut rasterizer = Rasterizer::new();
300 /// rasterizer.move_to(0.0, 0.0);
301 /// rasterizer.line_to(4.0, 0.0);
302 /// rasterizer.line_to(4.0, 2.0);
303 /// rasterizer.line_to(0.0, 2.0);
304 /// rasterizer.close_polygon();
305 ///
306 /// let mut spans = Vec::new();
307 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, alpha| {
308 /// spans.push((x, y, len, alpha));
309 /// });
310 ///
311 /// assert_eq!(spans.len(), 2);
312 /// ```
313 pub fn move_to(&mut self, x: f64, y: f64) {
314 self.close_polygon();
315 let (x, y) = (to_subpixel(x), to_subpixel(y));
316 self.set_current(x >> SUBPIXEL_SHIFT, y >> SUBPIXEL_SHIFT);
317 self.x = x;
318 self.y = y;
319 self.start_x = x;
320 self.start_y = y;
321 self.open = true;
322 }
323
324 /// Extend the current subpath to a device-space point.
325 ///
326 /// ```
327 /// use pdfrum_render::scanline::Rasterizer;
328 ///
329 /// // A `line_to` with no `move_to` before it is ignored rather than
330 /// // treated as starting at the origin.
331 /// let mut rasterizer = Rasterizer::new();
332 /// rasterizer.line_to(4.0, 2.0);
333 /// assert!(rasterizer.is_empty());
334 /// ```
335 pub fn line_to(&mut self, x: f64, y: f64) {
336 if !self.open {
337 return;
338 }
339 let (x, y) = (to_subpixel(x), to_subpixel(y));
340 self.render_line(self.x, self.y, x, y);
341 self.x = x;
342 self.y = y;
343 }
344
345 /// Close the current subpath back to its start.
346 ///
347 /// A fill's boundary is closed by definition, so this runs implicitly
348 /// before every `move_to` and before the sweep: an unclosed subpath in the
349 /// input is filled as though the caller had closed it, which is what both
350 /// PDF's `f` and the oracle's scan converter do.
351 ///
352 /// ```
353 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
354 ///
355 /// // An unclosed subpath fills as though the caller had closed it,
356 /// // which is what `f` and the oracle both do -- so closing explicitly
357 /// // and leaving it open give the same spans.
358 /// let build = |close: bool| {
359 /// let mut rasterizer = Rasterizer::new();
360 /// rasterizer.move_to(0.0, 0.0);
361 /// rasterizer.line_to(4.0, 0.0);
362 /// rasterizer.line_to(4.0, 2.0);
363 /// rasterizer.line_to(0.0, 2.0);
364 /// if close {
365 /// rasterizer.close_polygon();
366 /// }
367 /// let mut spans = Vec::new();
368 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, a| {
369 /// spans.push((x, y, len, a));
370 /// });
371 /// spans
372 /// };
373 /// assert_eq!(build(true), build(false));
374 /// ```
375 pub fn close_polygon(&mut self) {
376 if !self.open {
377 return;
378 }
379 self.render_line(self.x, self.y, self.start_x, self.start_y);
380 self.x = self.start_x;
381 self.y = self.start_y;
382 self.open = false;
383 }
384
385 /// Add a whole flattened path, in device space.
386 ///
387 /// ```
388 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
389 ///
390 /// // Curves are flattened here; the rasterizer sees only segments.
391 /// use kurbo::Shape;
392 ///
393 /// let path = kurbo::Rect::new(0.0, 0.0, 4.0, 2.0).to_path(0.1);
394 /// let mut rasterizer = Rasterizer::new();
395 /// rasterizer.add_path(&path, 0.25);
396 ///
397 /// let mut spans = Vec::new();
398 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, a| {
399 /// spans.push((x, y, len, a));
400 /// });
401 /// assert_eq!(spans, [(0, 0, 4, 255), (0, 1, 4, 255)]);
402 /// ```
403 pub fn add_path(&mut self, path: &kurbo::BezPath, tolerance: f64) {
404 // `flatten` emits only MoveTo/LineTo/ClosePath, so the match below is
405 // total over what can actually arrive; the curve arms are unreachable
406 // and say so rather than silently dropping geometry.
407 kurbo::flatten(path.iter(), tolerance, |el| match el {
408 kurbo::PathEl::MoveTo(p) => self.move_to(p.x, p.y),
409 kurbo::PathEl::LineTo(p) => self.line_to(p.x, p.y),
410 kurbo::PathEl::ClosePath => self.close_polygon(),
411 kurbo::PathEl::QuadTo(..) | kurbo::PathEl::CurveTo(..) => {
412 debug_assert!(false, "kurbo::flatten emits no curves");
413 }
414 });
415 self.close_polygon();
416 }
417
418 /// Switch the cell being accumulated into, banking the old one.
419 fn set_current(&mut self, x: i32, y: i32) {
420 match self.current {
421 Some(cell) if cell.x == x && cell.y == y => {}
422 Some(cell) => {
423 self.bank(cell);
424 self.current = Some(Cell::at(x, y));
425 }
426 None => self.current = Some(Cell::at(x, y)),
427 }
428 }
429
430 /// Add cover and area to the cell being accumulated into.
431 fn add_cover(&mut self, cover: i32, area: i32) {
432 if let Some(cell) = self.current.as_mut() {
433 cell.cover = cell.cover.saturating_add(cover);
434 cell.area = cell.area.saturating_add(area);
435 }
436 }
437
438 /// Accumulate one segment that stays within a single scanline.
439 ///
440 /// `y1`/`y2` are *fractional* y within the row `ey`, so this integrates
441 /// the segment's horizontal travel across whatever pixels it crosses.
442 /// Splitting the general case into this makes the area integral a
443 /// trapezoid per pixel, which is where the exactness comes from: each
444 /// pixel's contribution is `(fx_in + fx_out) * dy`, twice the
445 /// trapezoid's area, with no sampling anywhere.
446 fn render_hline(&mut self, ey: i32, x1: i32, y1: i32, x2: i32, y2: i32) {
447 let ex1 = x1 >> SUBPIXEL_SHIFT;
448 let ex2 = x2 >> SUBPIXEL_SHIFT;
449 let fx1 = x1 & SUBPIXEL_MASK;
450 let fx2 = x2 & SUBPIXEL_MASK;
451
452 // Horizontal: no vertical travel, so no cover and no area — but the
453 // pen still moves, so the current cell follows it.
454 if y1 == y2 {
455 self.set_current(ex2, ey);
456 return;
457 }
458
459 // Within one pixel: one trapezoid, closed form.
460 if ex1 == ex2 {
461 let delta = y2 - y1;
462 self.add_cover(delta, (fx1 + fx2).saturating_mul(delta));
463 return;
464 }
465
466 // Crossing pixels: split the segment at each vertical pixel boundary
467 // and give each pixel its own trapezoid. The integer division carries
468 // its remainder forward (`mod`/`rem`) so the pieces sum to exactly the
469 // whole rather than drifting by a rounding step per pixel.
470 let (p, first, incr, dx) = if x2 > x1 {
471 (
472 (SUBPIXEL_SCALE - fx1) * (y2 - y1),
473 SUBPIXEL_SCALE,
474 1,
475 x2 - x1,
476 )
477 } else {
478 (fx1 * (y2 - y1), 0, -1, x1 - x2)
479 };
480 if dx == 0 {
481 return;
482 }
483
484 let mut delta = p / dx;
485 let mut modulo = p % dx;
486 if modulo < 0 {
487 delta -= 1;
488 modulo += dx;
489 }
490 self.add_cover(delta, (fx1 + first).saturating_mul(delta));
491
492 let mut ex = ex1 + incr;
493 self.set_current(ex, ey);
494 let mut y = y1 + delta;
495
496 if ex != ex2 {
497 let step = SUBPIXEL_SCALE * (y2 - y + delta);
498 let mut lift = step / dx;
499 let mut rem = step % dx;
500 if rem < 0 {
501 lift -= 1;
502 rem += dx;
503 }
504 modulo -= dx;
505 // A malformed segment cannot make this unbounded: `ex` steps
506 // toward `ex2` by one every iteration.
507 while ex != ex2 {
508 delta = lift;
509 modulo += rem;
510 if modulo >= 0 {
511 modulo -= dx;
512 delta += 1;
513 }
514 self.add_cover(delta, SUBPIXEL_SCALE.saturating_mul(delta));
515 y += delta;
516 ex += incr;
517 self.set_current(ex, ey);
518 }
519 }
520
521 delta = y2 - y;
522 self.add_cover(delta, (fx2 + SUBPIXEL_SCALE - first).saturating_mul(delta));
523 }
524
525 /// Accumulate one straight segment in subpixel coordinates.
526 ///
527 /// Splits the segment at every horizontal pixel boundary and hands each
528 /// piece to [`Rasterizer::render_hline`], so the whole integral is a sum
529 /// of per-pixel trapezoids.
530 fn render_line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) {
531 // A segment long enough to overflow the area products is bisected
532 // until it is not. The oracle does the same at the same threshold.
533 const DX_LIMIT: i32 = 16384 << SUBPIXEL_SHIFT;
534 let dx_total = x2.saturating_sub(x1);
535 if dx_total >= DX_LIMIT || dx_total <= -DX_LIMIT {
536 let cx = x1.saturating_add(x2) / 2;
537 let cy = y1.saturating_add(y2) / 2;
538 self.render_line(x1, y1, cx, cy);
539 self.render_line(cx, cy, x2, y2);
540 return;
541 }
542
543 let dy = y2 - y1;
544 let ey1 = y1 >> SUBPIXEL_SHIFT;
545 let ey2 = y2 >> SUBPIXEL_SHIFT;
546 let fy1 = y1 & SUBPIXEL_MASK;
547 let fy2 = y2 & SUBPIXEL_MASK;
548
549 // Within one scanline: the whole segment is one horizontal pass.
550 if ey1 == ey2 {
551 self.render_hline(ey1, x1, fy1, x2, fy2);
552 return;
553 }
554
555 // Exactly vertical: every scanline it crosses gets the same rectangle,
556 // so the loop writes a constant rather than dividing per row.
557 if dx_total == 0 {
558 let ex = x1 >> SUBPIXEL_SHIFT;
559 let two_fx = (x1 - (ex << SUBPIXEL_SHIFT)) << 1;
560 let (first, incr) = if dy < 0 { (0, -1) } else { (SUBPIXEL_SCALE, 1) };
561
562 let mut delta = first - fy1;
563 self.add_cover(delta, two_fx.saturating_mul(delta));
564 let mut ey = ey1 + incr;
565 self.set_current(ex, ey);
566
567 delta = first + first - SUBPIXEL_SCALE;
568 let area = two_fx.saturating_mul(delta);
569 while ey != ey2 {
570 if let Some(cell) = self.current.as_mut() {
571 cell.cover = delta;
572 cell.area = area;
573 }
574 ey += incr;
575 self.set_current(ex, ey);
576 }
577 delta = fy2 - SUBPIXEL_SCALE + first;
578 self.add_cover(delta, two_fx.saturating_mul(delta));
579 return;
580 }
581
582 // The general case: walk scanline by scanline, carrying the division
583 // remainder so the x positions of the row crossings sum exactly.
584 // The products are formed in `i64` because `(256 - fy) * dx` can
585 // exceed `i32` on a long near-horizontal segment; the quotients are
586 // bounded by `dx_total` and so fit back.
587 let (p, first, incr, dy_abs) = if dy < 0 {
588 (i64::from(fy1) * i64::from(dx_total), 0, -1, -dy)
589 } else {
590 (
591 i64::from(SUBPIXEL_SCALE - fy1) * i64::from(dx_total),
592 SUBPIXEL_SCALE,
593 1,
594 dy,
595 )
596 };
597 if dy_abs == 0 {
598 return;
599 }
600 let dy64 = i64::from(dy_abs);
601 let narrow = |v: i64| -> i32 { i32::try_from(v).unwrap_or(0) };
602
603 let mut delta = narrow(p / dy64);
604 let mut modulo = narrow(p % dy64);
605 if modulo < 0 {
606 delta -= 1;
607 modulo += dy_abs;
608 }
609
610 let mut x_from = x1.saturating_add(delta);
611 self.render_hline(ey1, x1, fy1, x_from, first);
612 let mut ey = ey1 + incr;
613 self.set_current(x_from >> SUBPIXEL_SHIFT, ey);
614
615 if ey != ey2 {
616 let step = i64::from(SUBPIXEL_SCALE) * i64::from(dx_total);
617 let mut lift = narrow(step / dy64);
618 let mut rem = narrow(step % dy64);
619 if rem < 0 {
620 lift -= 1;
621 rem += dy_abs;
622 }
623 modulo -= dy_abs;
624 while ey != ey2 {
625 delta = lift;
626 modulo += rem;
627 if modulo >= 0 {
628 modulo -= dy_abs;
629 delta += 1;
630 }
631 let x_to = x_from.saturating_add(delta);
632 self.render_hline(ey, x_from, SUBPIXEL_SCALE - first, x_to, first);
633 x_from = x_to;
634 ey += incr;
635 self.set_current(x_from >> SUBPIXEL_SHIFT, ey);
636 }
637 }
638 self.render_hline(ey, x_from, SUBPIXEL_SCALE - first, x2, fy2);
639 }
640
641 /// Bank the in-progress cell and sort, readying the sweep.
642 fn finish(&mut self) {
643 self.close_polygon();
644 if let Some(cell) = self.current.take() {
645 self.bank(cell);
646 }
647 self.store.sort();
648 }
649
650 /// Turn the accumulated cells into horizontal spans of coverage.
651 ///
652 /// `emit` receives `(x, len, alpha)` for each run of equal coverage, in
653 /// increasing y then increasing x. Only non-zero alphas are emitted, so a
654 /// consumer can blend unconditionally.
655 ///
656 /// Each row is resolved from its own cells: the running cover starts at
657 /// zero on every row and is not carried into the next, because a closed
658 /// boundary crosses each scanline an even number of times and so returns
659 /// the winding count to zero by the row's end. That is what makes
660 /// [`Rasterizer::keep_rows`] exact rather than approximate.
661 ///
662 /// ```
663 /// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
664 ///
665 /// let mut rasterizer = Rasterizer::new();
666 /// rasterizer.move_to(0.0, 0.0);
667 /// rasterizer.line_to(4.0, 0.0);
668 /// rasterizer.line_to(4.0, 2.0);
669 /// rasterizer.line_to(0.0, 2.0);
670 /// rasterizer.close_polygon();
671 ///
672 /// let mut spans = Vec::new();
673 /// rasterizer.sweep(FillRule::Winding, Coverage::Exact, |x, len, y, alpha| {
674 /// spans.push((x, y, len, alpha));
675 /// });
676 ///
677 /// // Increasing y then increasing x, and only non-zero alphas, so a
678 /// // consumer can blend unconditionally.
679 /// assert_eq!(spans, [(0, 0, 4, 255), (0, 1, 4, 255)]);
680 /// ```
681 pub fn sweep(
682 &mut self,
683 rule: FillRule,
684 coverage: Coverage,
685 mut emit: impl FnMut(i32, i32, i32, u8),
686 ) {
687 self.finish();
688 for (y, row) in self.store.rows() {
689 sweep_row(row, y, rule, coverage, &mut emit);
690 }
691 }
692}
693
694/// How an integrated coverage becomes an alpha byte — AGG's three modes.
695///
696/// See [`AntiAlias`](crate::device::AntiAlias), whose three variants these
697/// mirror one for one; this is the integrator's own spelling of the same
698/// choice, so `scanline` does not depend on the device vocabulary.
699///
700/// ```
701/// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
702///
703/// // A half-covered pixel: `Exact` keeps the partial alpha, `Full`
704/// // pushes any touched pixel to 255.
705/// let alpha = |coverage| {
706/// let mut rasterizer = Rasterizer::new();
707/// rasterizer.move_to(0.0, 0.0);
708/// rasterizer.line_to(0.5, 0.0);
709/// rasterizer.line_to(0.5, 1.0);
710/// rasterizer.line_to(0.0, 1.0);
711/// rasterizer.close_polygon();
712/// let mut out = 0u8;
713/// rasterizer.sweep(FillRule::Winding, coverage, |_, _, _, a| out = a);
714/// out
715/// };
716/// assert!(alpha(Coverage::Exact) < 255);
717/// assert_eq!(alpha(Coverage::Full), 255);
718/// ```
719#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
720pub enum Coverage {
721 /// Coverage becomes alpha: `min(255, floor(cov * 256))`.
722 #[default]
723 Exact,
724 /// `aliased_path`: the same coverage thresholded at its midpoint.
725 Thresholded,
726 /// `full_cover`: any touched pixel at 255, whatever its coverage.
727 Full,
728}
729
730/// The fill rule [`Rasterizer::sweep`] takes — the same
731/// [`FillRule`] the backend trait uses.
732///
733/// ```
734/// use pdfrum_render::scanline::{Coverage, FillRule, Rasterizer};
735///
736/// // Two nested squares wound the same way: winding fills the hole,
737/// // even-odd leaves it clear.
738/// let covered = |rule| {
739/// let mut rasterizer = Rasterizer::new();
740/// for (lo, hi) in [(0.0, 6.0), (2.0, 4.0)] {
741/// rasterizer.move_to(lo, lo);
742/// rasterizer.line_to(hi, lo);
743/// rasterizer.line_to(hi, hi);
744/// rasterizer.line_to(lo, hi);
745/// rasterizer.close_polygon();
746/// }
747/// let mut len = 0;
748/// rasterizer.sweep(rule, Coverage::Exact, |_, l, y, _| {
749/// if y == 3 {
750/// len += l;
751/// }
752/// });
753/// len
754/// };
755/// assert_eq!(covered(FillRule::Winding), 6);
756/// assert_eq!(covered(FillRule::EvenOdd), 4);
757/// ```
758pub use crate::FillRule;
759
760/// Sweep one scanline's cells into spans.
761///
762/// `row` is sorted by x. Cells sharing an x are merged; between two cells the
763/// running `cover` is constant, which is the span.
764fn sweep_row(
765 row: &[Cell],
766 y: i32,
767 rule: FillRule,
768 coverage: Coverage,
769 emit: &mut impl FnMut(i32, i32, i32, u8),
770) {
771 let mut cover = 0i32;
772 let mut i = 0usize;
773 while let Some(&first) = row.get(i) {
774 let x = first.x;
775 let mut area = first.area;
776 cover = cover.saturating_add(first.cover);
777 i += 1;
778 // Merge every cell at this x.
779 while let Some(&next) = row.get(i) {
780 if next.x != x {
781 break;
782 }
783 area = area.saturating_add(next.area);
784 cover = cover.saturating_add(next.cover);
785 i += 1;
786 }
787
788 // The boundary pixel itself: the running cover minus the area the
789 // boundary swept inside it. `cover << (SHIFT + 1)` is the full-pixel
790 // area in the same doubled units `area` is measured in.
791 let mut next_x = x;
792 if area != 0 {
793 let alpha = coverage_to_alpha(
794 (cover << (SUBPIXEL_SHIFT + 1)).saturating_sub(area),
795 rule,
796 coverage,
797 );
798 if alpha != 0 {
799 emit(x, 1, y, alpha);
800 }
801 next_x = x + 1;
802 }
803
804 // The interior run up to the next cell, at constant coverage.
805 if let Some(&next) = row.get(i)
806 && next.x > next_x
807 {
808 let alpha = coverage_to_alpha(cover << (SUBPIXEL_SHIFT + 1), rule, coverage);
809 if alpha != 0 {
810 emit(next_x, next.x - next_x, y, alpha);
811 }
812 }
813 }
814}
815
816/// The oracle's coverage-to-alpha mapping, measured and ported.
817///
818/// `area` is twice the covered area in subpixel units squared; shifting it
819/// down by `2*SHIFT + 1 - 8` renormalises it to 0..=256, and the clamp at 255
820/// is the only thing keeping 256 out. So the mapping is
821///
822/// ```text
823/// alpha = min(255, floor(coverage * 256))
824/// ```
825///
826/// — a **×256 scale clamped at the top**, not ×255, and truncating rather than
827/// rounding. It was measured before it was ported: a shallow-slope fill
828/// rendered through the oracle at 64x64 gives the edge ramp `223 159 95 31` on
829/// grays whose true coverages are exactly ⅛, ⅜, ⅝ and ⅞, which is that
830/// formula and no other. There is **no gamma table anywhere on this path**;
831/// the text path's is not reachable from here.
832///
833/// [`Coverage::Thresholded`] is the oracle's `aliased_path`, which does not
834/// turn the rasterizer off but thresholds the same coverage at the midpoint.
835/// That is why a hard-edged rect clip still goes through the identical
836/// integrator. [`Coverage::Full`] is `full_cover`, which keeps the same choice
837/// of covered pixels and discards the value: every pixel this integrator
838/// reaches at all is written opaque.
839#[must_use]
840pub(crate) fn coverage_to_alpha(area: i32, rule: FillRule, coverage: Coverage) -> u8 {
841 /// The renormalised coverage's full-cover value, 256.
842 const COVER_FULL: i32 = 1 << 8;
843 /// The largest alpha byte, 255 — the clamp that keeps 256 out.
844 const COVER_MASK: i32 = COVER_FULL - 1;
845
846 let mut cover = area >> (SUBPIXEL_SHIFT * 2 + 1 - 8);
847 if cover < 0 {
848 cover = cover.saturating_neg();
849 }
850 if rule == FillRule::EvenOdd {
851 // Fold the winding count into 0..=256: an odd number of crossings is
852 // covered, an even one is not, and the fold makes that continuous.
853 cover &= (COVER_FULL * 2) - 1;
854 if cover > COVER_FULL {
855 cover = COVER_FULL * 2 - cover;
856 }
857 }
858 match coverage {
859 Coverage::Exact => {}
860 Coverage::Thresholded => {
861 cover = if cover > COVER_MASK / 2 {
862 COVER_MASK
863 } else {
864 0
865 };
866 }
867 // Not a threshold: the test is against zero, so a pixel the span
868 // touches at all is opaque. Two cells that each half-cover a pixel
869 // both paint it, which is the seam suppression `full_cover` exists
870 // for; thresholding would drop it from both.
871 Coverage::Full => {
872 if cover > 0 {
873 cover = COVER_MASK;
874 }
875 }
876 }
877 if cover > COVER_MASK {
878 cover = COVER_MASK;
879 }
880 #[expect(
881 clippy::cast_possible_truncation,
882 clippy::cast_sign_loss,
883 reason = "the clamp above bounds cover to 0..=255"
884 )]
885 let byte = cover as u8;
886 byte
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 /// Fill a path into a width x height coverage plane.
894 ///
895 /// This is the *specification*: the rasterizer records every row the path
896 /// crosses and the callback discards the ones outside the plane, which is
897 /// what every consumer did before `keep_rows` existed.
898 /// [`banded_coverage`] is the same plane taken the cheap way, and
899 /// [`the_band_reproduces_the_unbanded_plane`] is what pins them together.
900 fn coverage(path: &kurbo::BezPath, w: i32, h: i32, rule: FillRule, mode: Coverage) -> Vec<u8> {
901 let cells = usize::try_from(w * h).expect("a test plane fits");
902 let mut out = vec![0u8; cells];
903 let mut raster = Rasterizer::new();
904 raster.add_path(path, 0.1);
905 raster.sweep(rule, mode, |x, len, y, alpha| {
906 if y < 0 || y >= h {
907 return;
908 }
909 for col in x.max(0)..(x + len).min(w) {
910 let Ok(index) = usize::try_from(y * w + col) else {
911 continue;
912 };
913 if let Some(slot) = out.get_mut(index) {
914 *slot = alpha;
915 }
916 }
917 });
918 out
919 }
920
921 /// The same plane as [`coverage`], with the rasterizer told the rows.
922 ///
923 /// The callback is byte-for-byte [`coverage`]'s, kept rather than
924 /// simplified: the point of the comparison is that the *only* difference
925 /// between the two is where the discard happens.
926 fn banded_coverage(
927 path: &kurbo::BezPath,
928 w: i32,
929 h: i32,
930 rule: FillRule,
931 mode: Coverage,
932 ) -> Vec<u8> {
933 let cells = usize::try_from(w * h).expect("a test plane fits");
934 let mut out = vec![0u8; cells];
935 let mut raster = Rasterizer::new();
936 raster.keep_rows(0..h);
937 raster.add_path(path, 0.1);
938 raster.sweep(rule, mode, |x, len, y, alpha| {
939 if y < 0 || y >= h {
940 return;
941 }
942 for col in x.max(0)..(x + len).min(w) {
943 let Ok(index) = usize::try_from(y * w + col) else {
944 continue;
945 };
946 if let Some(slot) = out.get_mut(index) {
947 *slot = alpha;
948 }
949 }
950 });
951 out
952 }
953
954 /// How many cells a path banks under a row range — the cost the band cuts.
955 fn banked(path: &kurbo::BezPath, rows: Option<core::ops::Range<i32>>) -> usize {
956 let mut raster = Rasterizer::new();
957 if let Some(rows) = rows {
958 raster.keep_rows(rows);
959 }
960 raster.add_path(path, 0.1);
961 let mut cells = 0usize;
962 raster.sweep(FillRule::Winding, Coverage::Exact, |_, _, _, _| {});
963 for (_, row) in raster.store.rows() {
964 cells += row.len();
965 }
966 cells
967 }
968
969 fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> kurbo::BezPath {
970 let mut p = kurbo::BezPath::new();
971 p.move_to((x0, y0));
972 p.line_to((x1, y0));
973 p.line_to((x1, y1));
974 p.line_to((x0, y1));
975 p.close_path();
976 p
977 }
978
979 #[test]
980 fn a_whole_pixel_rect_is_fully_covered() {
981 let cov = coverage(
982 &rect(1.0, 1.0, 3.0, 3.0),
983 4,
984 4,
985 FillRule::Winding,
986 Coverage::Exact,
987 );
988 assert_eq!(cov.first().copied(), Some(0), "outside");
989 assert_eq!(cov.get(5).copied(), Some(255), "inside (1,1)");
990 assert_eq!(cov.get(10).copied(), Some(255), "inside (2,2)");
991 assert_eq!(cov.get(15).copied(), Some(0), "outside (3,3)");
992 }
993
994 #[test]
995 fn a_half_covered_pixel_is_exactly_half() {
996 // The whole point of an analytic rasterizer: half a pixel is 128,
997 // not the nearest of seventeen supersampled levels.
998 let cov = coverage(
999 &rect(0.0, 0.0, 0.5, 1.0),
1000 1,
1001 1,
1002 FillRule::Winding,
1003 Coverage::Exact,
1004 );
1005 assert_eq!(cov.first().copied(), Some(128));
1006 }
1007
1008 #[test]
1009 fn the_alpha_mapping_is_times_256_truncating() {
1010 // The measured ramp: coverages of 1/8, 3/8, 5/8, 7/8 over a pixel
1011 // give 32, 96, 160, 224 -- floor(cov * 256), not round(cov * 255).
1012 for (num, expected) in [(1, 32u8), (3, 96), (5, 160), (7, 224)] {
1013 let frac = f64::from(num) / 8.0;
1014 let cov = coverage(
1015 &rect(0.0, 0.0, frac, 1.0),
1016 1,
1017 1,
1018 FillRule::Winding,
1019 Coverage::Exact,
1020 );
1021 assert_eq!(
1022 cov.first().copied(),
1023 Some(expected),
1024 "coverage {num}/8 must map to {expected}"
1025 );
1026 }
1027 }
1028
1029 #[test]
1030 fn full_coverage_clamps_to_255_not_256() {
1031 // `floor(1.0 * 256)` is 256, which does not fit a byte; the clamp is
1032 // the only thing keeping it out, and it is why full cover is 255.
1033 assert_eq!(
1034 coverage_to_alpha(1 << 17, FillRule::Winding, Coverage::Exact),
1035 255
1036 );
1037 }
1038
1039 #[test]
1040 fn even_odd_punches_out_an_overlap() {
1041 // Two concentric squares: even-odd leaves the inner one empty where
1042 // non-zero would fill it.
1043 let mut p = rect(0.0, 0.0, 6.0, 6.0);
1044 p.extend(rect(2.0, 2.0, 4.0, 4.0).iter());
1045 let eo = coverage(&p, 6, 6, FillRule::EvenOdd, Coverage::Exact);
1046 let nz = coverage(&p, 6, 6, FillRule::Winding, Coverage::Exact);
1047 // (3,3) is inside both squares.
1048 assert_eq!(eo.get(3 * 6 + 3).copied(), Some(0), "even-odd punches out");
1049 assert_eq!(nz.get(3 * 6 + 3).copied(), Some(255), "non-zero fills");
1050 // (1,1) is inside only the outer one, so both fill it.
1051 assert_eq!(eo.get(6 + 1).copied(), Some(255));
1052 assert_eq!(nz.get(6 + 1).copied(), Some(255));
1053 }
1054
1055 #[test]
1056 fn aliasing_thresholds_at_the_midpoint() {
1057 // `aa = false` is the oracle's aliased_path: it thresholds the same
1058 // analytic coverage rather than turning the integrator off, so a
1059 // just-over-half pixel is solid and a just-under-half one is empty.
1060 let over = coverage(
1061 &rect(0.0, 0.0, 0.6, 1.0),
1062 1,
1063 1,
1064 FillRule::Winding,
1065 Coverage::Thresholded,
1066 );
1067 let under = coverage(
1068 &rect(0.0, 0.0, 0.4, 1.0),
1069 1,
1070 1,
1071 FillRule::Winding,
1072 Coverage::Thresholded,
1073 );
1074 assert_eq!(over.first().copied(), Some(255));
1075 assert_eq!(under.first().copied(), Some(0));
1076 }
1077
1078 #[test]
1079 fn full_cover_tests_against_zero_rather_than_the_midpoint() {
1080 // `full_cover` keeps the integrator's choice of covered pixels and
1081 // discards the coverage *value*, so the test is `> 0` and not
1082 // `> 127`. The distinction is the whole point: two Coons cells that
1083 // each cover a shared pixel by 40% both paint it here, where
1084 // thresholding drops it from both and leaves a white pin-hole along
1085 // every internal seam of a subdivided patch.
1086 for frac in [0.4, 0.6, 0.05] {
1087 let cov = coverage(
1088 &rect(0.0, 0.0, frac, 1.0),
1089 1,
1090 1,
1091 FillRule::Winding,
1092 Coverage::Full,
1093 );
1094 assert_eq!(
1095 cov.first().copied(),
1096 Some(255),
1097 "coverage {frac} is non-zero, so full_cover writes it opaque"
1098 );
1099 }
1100 // A pixel the path does not reach at all stays empty — the mode does
1101 // not flood, it only flattens.
1102 let miss = coverage(
1103 &rect(2.0, 2.0, 3.0, 3.0),
1104 1,
1105 1,
1106 FillRule::Winding,
1107 Coverage::Full,
1108 );
1109 assert_eq!(miss.first().copied(), Some(0));
1110 }
1111
1112 #[test]
1113 fn an_exact_45_degree_edge_halves_every_boundary_pixel() {
1114 // A diagonal through pixel corners cuts each boundary pixel into two
1115 // equal triangles, so 128 is the *only* partial level and any other
1116 // value would be a bug. This is the case a supersampler also gets
1117 // right, which is why it is not the one the next test measures.
1118 let mut p = kurbo::BezPath::new();
1119 p.move_to((0.0, 0.0));
1120 p.line_to((32.0, 0.0));
1121 p.line_to((0.0, 32.0));
1122 p.close_path();
1123 let cov = coverage(&p, 32, 32, FillRule::Winding, Coverage::Exact);
1124 let mut levels: Vec<u8> = cov
1125 .iter()
1126 .copied()
1127 .filter(|&a| a != 0 && a != 255)
1128 .collect();
1129 levels.sort_unstable();
1130 levels.dedup();
1131 assert_eq!(levels, vec![128], "a 45 degree edge halves its pixels");
1132 }
1133
1134 #[test]
1135 fn a_shallow_edge_has_more_than_seventeen_levels() {
1136 // The band this backend exists to close. `tiny-skia` supersamples at
1137 // four subsamples per axis, so a shallow edge can only take one of
1138 // seventeen coverages and a run of pixels along it steps in jumps of
1139 // sixteen counts. An analytic integrator produces the continuum the
1140 // geometry actually implies, which is what the oracle writes.
1141 let mut p = kurbo::BezPath::new();
1142 p.move_to((0.0, 0.0));
1143 p.line_to((64.0, 0.0));
1144 p.line_to((64.0, 5.0));
1145 p.close_path();
1146 let cov = coverage(&p, 64, 8, FillRule::Winding, Coverage::Exact);
1147 let mut levels: Vec<u8> = cov
1148 .iter()
1149 .copied()
1150 .filter(|&a| a != 0 && a != 255)
1151 .collect();
1152 levels.sort_unstable();
1153 levels.dedup();
1154 assert!(
1155 levels.len() > 17,
1156 "only {} partial levels along a shallow edge",
1157 levels.len()
1158 );
1159 }
1160
1161 #[test]
1162 fn winding_direction_does_not_change_coverage() {
1163 // A clockwise and a counter-clockwise square fill identically under
1164 // non-zero: the cover sum's sign is taken as magnitude.
1165 let cw = coverage(
1166 &rect(0.0, 0.0, 4.0, 4.0),
1167 4,
1168 4,
1169 FillRule::Winding,
1170 Coverage::Exact,
1171 );
1172 let mut ccw = kurbo::BezPath::new();
1173 ccw.move_to((0.0, 0.0));
1174 ccw.line_to((0.0, 4.0));
1175 ccw.line_to((4.0, 4.0));
1176 ccw.line_to((4.0, 0.0));
1177 ccw.close_path();
1178 assert_eq!(cw, coverage(&ccw, 4, 4, FillRule::Winding, Coverage::Exact));
1179 }
1180
1181 #[test]
1182 fn an_unclosed_subpath_fills_as_though_closed() {
1183 // PDF's `f` closes every open subpath before filling, and so does the
1184 // oracle's scan converter.
1185 let mut open = kurbo::BezPath::new();
1186 open.move_to((0.0, 0.0));
1187 open.line_to((4.0, 0.0));
1188 open.line_to((4.0, 4.0));
1189 open.line_to((0.0, 4.0));
1190 let closed = coverage(
1191 &rect(0.0, 0.0, 4.0, 4.0),
1192 4,
1193 4,
1194 FillRule::Winding,
1195 Coverage::Exact,
1196 );
1197 assert_eq!(
1198 coverage(&open, 4, 4, FillRule::Winding, Coverage::Exact),
1199 closed
1200 );
1201 }
1202
1203 #[test]
1204 fn coordinates_truncate_toward_zero() {
1205 // `int(c * 256)`, not `round`: the oracle's own conversion.
1206 assert_eq!(to_subpixel(1.0), 256);
1207 assert_eq!(to_subpixel(1.5), 384);
1208 assert_eq!(to_subpixel(-1.5), -384);
1209 // 0.999... truncates down rather than rounding up to the next pixel.
1210 assert_eq!(to_subpixel(0.999), 255);
1211 }
1212
1213 #[test]
1214 fn a_non_finite_coordinate_becomes_zero_rather_than_panicking() {
1215 assert_eq!(to_subpixel(f64::NAN), 0);
1216 assert_eq!(to_subpixel(f64::INFINITY), 0);
1217 }
1218
1219 #[test]
1220 fn an_empty_path_sweeps_nothing() {
1221 let mut r = Rasterizer::new();
1222 r.add_path(&kurbo::BezPath::new(), 0.1);
1223 let mut spans = 0;
1224 r.sweep(FillRule::Winding, Coverage::Exact, |_, _, _, _| spans += 1);
1225 assert_eq!(spans, 0);
1226 }
1227
1228 #[test]
1229 fn a_line_to_without_a_move_to_is_ignored() {
1230 let mut r = Rasterizer::new();
1231 r.line_to(4.0, 4.0);
1232 assert!(r.is_empty());
1233 }
1234
1235 #[test]
1236 fn total_coverage_matches_the_area_of_a_slanted_quad() {
1237 // The integral property an analytic rasterizer must have: summed
1238 // coverage equals the geometric area, to within the byte quantisation.
1239 let mut p = kurbo::BezPath::new();
1240 p.move_to((2.0, 1.0));
1241 p.line_to((14.0, 3.0));
1242 p.line_to((13.0, 14.0));
1243 p.line_to((1.0, 12.0));
1244 p.close_path();
1245 let cov = coverage(&p, 16, 16, FillRule::Winding, Coverage::Exact);
1246 let painted: f64 = cov.iter().map(|&a| f64::from(a) / 256.0).sum();
1247 // Shoelace over the four vertices.
1248 let pts = [(2.0, 1.0), (14.0, 3.0), (13.0, 14.0), (1.0, 12.0)];
1249 let mut area = 0.0f64;
1250 for i in 0..4 {
1251 let (Some(&(x0, y0)), Some(&(x1, y1))) = (pts.get(i), pts.get((i + 1) % 4)) else {
1252 continue;
1253 };
1254 area += x0 * y1 - x1 * y0;
1255 }
1256 let area = (area / 2.0).abs();
1257 assert!(
1258 (painted - area).abs() < 1.0,
1259 "painted {painted:.3} vs geometric {area:.3}"
1260 );
1261 }
1262
1263 /// The paths a clip on a real page produces when its geometry leaves the
1264 /// target: above it, below it, both, and out to the engine's own clamp.
1265 ///
1266 /// The last is the case §18.3 measured — `hard_clip`'s +/-32000 is a
1267 /// deliberate artefact of the oracle's 16-bit truncation, so a path
1268 /// carrying 32000 device units of height reaches the integrator by design
1269 /// and must be answered rather than avoided.
1270 fn off_target_paths() -> Vec<(&'static str, kurbo::BezPath)> {
1271 vec![
1272 ("above", rect(2.0, -900.0, 6.0, 5.0)),
1273 ("below", rect(2.0, 3.0, 6.0, 900.0)),
1274 ("both", rect(2.0, -900.0, 6.0, 900.0)),
1275 ("clamped", rect(2.0, -32000.0, 6.0, 32000.0)),
1276 ("clamped-slanted", {
1277 let mut p = kurbo::BezPath::new();
1278 p.move_to((1.5, -32000.0));
1279 p.line_to((6.5, 32000.0));
1280 p.line_to((7.5, 32000.0));
1281 p.line_to((2.5, -32000.0));
1282 p.close_path();
1283 p
1284 }),
1285 ("left", rect(-32000.0, 2.0, 3.5, 6.0)),
1286 ("right", rect(4.5, 2.0, 32000.0, 6.0)),
1287 ("every-side", rect(-32000.0, -32000.0, 32000.0, 32000.0)),
1288 ]
1289 }
1290
1291 #[test]
1292 fn the_band_reproduces_the_unbanded_plane() {
1293 // The invariant the whole change rests on: telling the rasterizer
1294 // which rows survive changes no byte on a row that does. Both fill
1295 // rules and all three coverage modes, because the band is upstream of
1296 // every one of them.
1297 for (name, path) in off_target_paths() {
1298 for rule in [FillRule::Winding, FillRule::EvenOdd] {
1299 for mode in [Coverage::Exact, Coverage::Thresholded, Coverage::Full] {
1300 let spec = coverage(&path, 8, 8, rule, mode);
1301 let banded = banded_coverage(&path, 8, 8, rule, mode);
1302 assert_eq!(spec, banded, "{name} under {rule:?}/{mode:?}");
1303 }
1304 }
1305 }
1306 }
1307
1308 #[test]
1309 fn a_path_that_stays_inside_the_band_is_untouched_by_it() {
1310 // The control for the test above: a path with nothing to discard must
1311 // still agree, or the band would be hiding a difference behind the
1312 // rows it drops.
1313 let path = rect(1.25, 1.75, 6.5, 5.5);
1314 for rule in [FillRule::Winding, FillRule::EvenOdd] {
1315 for mode in [Coverage::Exact, Coverage::Thresholded, Coverage::Full] {
1316 assert_eq!(
1317 coverage(&path, 8, 8, rule, mode),
1318 banded_coverage(&path, 8, 8, rule, mode),
1319 "{rule:?}/{mode:?}"
1320 );
1321 }
1322 }
1323 }
1324
1325 #[test]
1326 fn the_band_keeps_the_targets_last_row() {
1327 // A half-open range read as closed, or closed as half-open, moves the
1328 // boundary by one and the last row is where that shows. The path is
1329 // painted across rows 6 and 7 of an eight-row plane, so row 7 is both
1330 // the last kept row and the one an off-by-one drops.
1331 let path = rect(1.0, 6.0, 7.0, 8.0);
1332 let banded = banded_coverage(&path, 8, 8, FillRule::Winding, Coverage::Exact);
1333 assert_eq!(banded.get(8 * 7 + 3).copied(), Some(255), "the last row");
1334 assert_eq!(
1335 coverage(&path, 8, 8, FillRule::Winding, Coverage::Exact),
1336 banded
1337 );
1338 }
1339
1340 #[test]
1341 fn the_band_keeps_the_targets_first_row() {
1342 // The other end, for the same reason.
1343 let path = rect(1.0, -4.0, 7.0, 1.0);
1344 let banded = banded_coverage(&path, 8, 8, FillRule::Winding, Coverage::Exact);
1345 assert_eq!(banded.first().copied(), Some(0), "column 0 is outside");
1346 assert_eq!(banded.get(3).copied(), Some(255), "the first row");
1347 assert_eq!(
1348 coverage(&path, 8, 8, FillRule::Winding, Coverage::Exact),
1349 banded
1350 );
1351 }
1352
1353 #[test]
1354 fn the_band_drops_the_rows_it_says_it_drops() {
1355 // The measurement the change exists for. A path 64 000 rows tall over
1356 // an eight-row target banks tens of thousands of cells unbanded and a
1357 // handful banded, which is the 530 073 rows of a real render in
1358 // miniature.
1359 let path = rect(2.0, -32000.0, 6.0, 32000.0);
1360 let unbanded = banked(&path, None);
1361 let banded = banked(&path, Some(0..8));
1362 assert!(
1363 unbanded > 60_000,
1364 "the unbanded store holds a cell per crossed row, got {unbanded}"
1365 );
1366 assert!(
1367 banded <= 32,
1368 "the banded store holds only the target's rows, got {banded}"
1369 );
1370 }
1371
1372 #[test]
1373 fn a_band_is_only_about_rows() {
1374 // Stated as a test because it is a limit of the fix rather than an
1375 // oversight: a path 64 000 columns wide costs its cells either way,
1376 // because the cells a segment banks are bounded by the pixels it
1377 // crosses and a horizontal one crosses them all on a single row.
1378 let path = rect(-32000.0, 2.0, 32000.0, 6.0);
1379 assert_eq!(banked(&path, None), banked(&path, Some(0..8)));
1380 }
1381
1382 #[test]
1383 fn a_band_outside_the_path_leaves_nothing() {
1384 // The degenerate end of the range: a target the path misses entirely
1385 // paints nothing, which is what the unbanded spelling's callback did
1386 // by returning on every row.
1387 let path = rect(2.0, -900.0, 6.0, -100.0);
1388 assert_eq!(
1389 banded_coverage(&path, 8, 8, FillRule::Winding, Coverage::Exact),
1390 vec![0u8; 64]
1391 );
1392 assert_eq!(banked(&path, Some(0..8)), 0);
1393 }
1394
1395 #[test]
1396 fn every_rows_cover_returns_to_zero_by_its_end() {
1397 // The proof `keep_rows` rests on, checked rather than argued: the
1398 // sweep starts each row's running cover at zero because a closed
1399 // boundary crosses a scanline an even number of times, so the cover
1400 // one row leaves behind is nothing the next one needs. If it were
1401 // not, dropping a row would corrupt the rows below it.
1402 for (name, path) in off_target_paths() {
1403 let mut raster = Rasterizer::new();
1404 raster.add_path(&path, 0.1);
1405 raster.sweep(FillRule::Winding, Coverage::Exact, |_, _, _, _| {});
1406 for (y, row) in raster.store.rows() {
1407 let cover: i32 = row.iter().map(|c| c.cover).sum();
1408 assert_eq!(cover, 0, "{name} leaves cover on row {y}");
1409 }
1410 }
1411 }
1412
1413 #[test]
1414 fn a_reset_keeps_the_band_the_caller_set() {
1415 // `AggDevice` sets the band once per sweep, but `reset` is what a
1416 // reused rasterizer calls between paths and it must not quietly widen
1417 // the range back to everything.
1418 let mut raster = Rasterizer::new();
1419 raster.keep_rows(0..8);
1420 raster.add_path(&rect(2.0, -900.0, 6.0, 900.0), 0.1);
1421 raster.sweep(FillRule::Winding, Coverage::Exact, |_, _, _, _| {});
1422 raster.reset();
1423 raster.add_path(&rect(2.0, -900.0, 6.0, 900.0), 0.1);
1424 raster.sweep(FillRule::Winding, Coverage::Exact, |_, _, _, _| {});
1425 let cells: usize = raster.store.rows().map(|(_, row)| row.len()).sum();
1426 assert!(cells <= 32, "the band survives a reset, got {cells}");
1427 }
1428}