Skip to main content

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}