pdfrum-render 0.1.0

Rendering engine and the RenderDevice/RasterBackend seam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
//! Types 6 and 7, Coons and tensor patch meshes.
//!
//! Unlike the other five rasterizers this one draws *through* a path
//! rasterizer: it subdivides a patch until each cell is either smaller than
//! two device units or flat enough in colour, then fills the cell's twelve
//! outer control points as a closed path.
//!
//! # `full_cover`, and why the scratch pixmap is not optional
//!
//! AGG fills those cells with a `full_cover` flag that bypasses coverage
//! entirely, so abutting cells overpaint each other's antialiased edges to
//! full opacity and the patch shows no seams. It is [`AntiAlias::FullCover`],
//! and it is emphatically **not** hard-edging: `full_cover` keeps the
//! rasterizer's choice of which pixels a span covers and discards only their
//! coverage *value*, where hard-edging thresholds that value at the midpoint.
//! A pixel two abutting cells each cover by 40% is painted by both under the
//! first rule and dropped by both under the second — a white pin-hole through
//! every internal seam.
//!
//! The cells still go into a scratch buffer at **alpha 1.0**, and the
//! shading's alpha is applied exactly once, when that buffer is blitted. That
//! is a second and separate requirement: `vello_cpu` binarizes each path's
//! *own* coverage, so a pixel claimed by two cells is written at full alpha
//! **twice**, which is harmless only when both writes are the same opaque
//! value.
//!
//! # The depth cap
//!
//! Upstream has none: termination relies solely on the two-device-unit bbox
//! test and the colour threshold, so a patch with non-finite control points —
//! reachable from a crafted mesh stream — never terminates. We accept a
//! cap of 32 plus a non-finite check as additive safety; each level halves
//! the patch, so 32 levels covers any patch up to 2^32 device units.

#[cfg(test)]
use kurbo::Shape;
use kurbo::{Affine, BezPath, Point, Rect};
use pdfrum_page::Patch;
use pdfrum_page::Rgb;

use crate::color::Argb;
use crate::device::{AntiAlias, Brush, FillRule, RenderDevice};
use crate::shading::steps::{ColorSteps, component_to_shading_index};

/// The maximum per-component colour delta before a cell is flat-filled.
pub const COLOR_THRESHOLD: i32 = 4;

/// A patch smaller than this in both axes is filled rather than subdivided.
pub const SMALL_PATCH: f64 = 2.0;

/// The subdivision depth cap. Additive safety over
/// a non-terminating recursion, not a fidelity change.
pub const MAX_DEPTH: u32 = 32;

/// An integer colour triple, the space the patch interpolator works in.
type IntColor = [i32; 3];

/// A corner colour in the interpolator's integer space.
///
/// The two cases are genuinely different quantities, which is why upstream
/// writes them as two branches rather than one conversion:
///
/// - Without a ramp the mesh carries a real colour, and each channel is
///   `(int32_t)(c * 255)` — a **truncation**, the C++ cast.
/// - With one, the mesh carries a single parametric value in the red slot and
///   the other two are dead. `ComponentToShadingIndex` maps it into the
///   ramp's 0..255 across the mesh's own decode range, which is `[1, 2]` or
///   `[0, 255]` as readily as the unit interval.
fn to_int_color(c: Rgb, range: Option<[f32; 2]>) -> IntColor {
    if let Some([lo, hi]) = range {
        #[expect(
            clippy::cast_possible_truncation,
            reason = "the C++ takes `static_cast<int32_t>` of the same value; \
                      the index is clamped into 0..=255 where it is used as a \
                      ramp subscript"
        )]
        let index = component_to_shading_index(c.r, lo, hi) as i32;
        [index, 0, 0]
    } else {
        let [r, g, b] = c.to_bytes_truncating();
        [i32::from(r), i32::from(g), i32::from(b)]
    }
}

/// `Interpolate`: integer linear interpolation with overflow detection.
///
/// Any overflow aborts the whole cell — which is upstream's own guard and the
/// only thing standing between a crafted mesh and a runaway subdivision.
fn interpolate(c0: i32, c1: i32, delta1: i32, delta2: i32) -> Option<i32> {
    if delta2 == 0 {
        return Some(c0);
    }
    c1.checked_sub(c0)?
        .checked_mul(delta1)?
        .checked_div(delta2)?
        .checked_add(c0)
}

/// The bilinear blend of a patch's four corner colours at a cell position.
///
/// The four corners and their three components are destructured by pattern
/// rather than subscripted. Both shapes are fixed-size arrays, so every
/// subscript was statically in bounds and every `Option` it produced statically
/// `Some` — but the subdivider reaches this four times per recursion node and
/// tens of thousands of times per mesh, and a `None` arm the compiler must
/// still carry is a branch on a path that has none.
fn bilinear(
    colors: &[IntColor; 4],
    left: i32,
    bottom: i32,
    x_scale: i32,
    y_scale: i32,
) -> Option<IntColor> {
    let [[r0, g0, b0], [r1, g1, b1], [r2, g2, b2], [r3, g3, b3]] = *colors;
    let blend = |c0, c1, c2, c3| {
        let bottom_edge = interpolate(c0, c3, left, x_scale)?;
        let top_edge = interpolate(c1, c2, left, x_scale)?;
        interpolate(bottom_edge, top_edge, bottom, y_scale)
    };
    Some([
        blend(r0, r1, r2, r3)?,
        blend(g0, g1, g2, g3)?,
        blend(b0, b1, b2, b3)?,
    ])
}

/// `Distance`: the maximum per-component absolute difference.
fn distance(a: IntColor, b: IntColor) -> i32 {
    let ([ar, ag, ab], [br, bg, bb]) = (a, b);
    (ar - br).abs().max((ag - bg).abs()).max((ab - bb).abs())
}

/// A patch's sixteen (or twelve) control points, as the subdivider handles
/// them: four rows of four, the outer twelve being the boundary.
#[derive(Debug, Clone, Copy)]
struct Points {
    /// Row-major 4x4. A Coons patch's four interior points are derived.
    grid: [[Point; 4]; 4],
}

/// De Casteljau at `t = 0.5` on one cubic, returning both halves.
#[expect(
    clippy::manual_midpoint,
    reason = "`(a + b) / 2.0` is the De Casteljau step as PDFium writes it. \
              `f64::midpoint` is not the same function — it is correctly \
              rounded where this rounds twice — and swapping it in would move \
              subdivided patch cells off the oracle's pixels. The overflow \
              the lint warns about needs a coordinate near f64::MAX, which \
              `all_finite` on the resulting points already rejects."
)]
#[expect(
    clippy::many_single_char_names,
    reason = "a..f are De Casteljau's intermediate points in the order the \
              construction names them; p0..p3 are the input control points"
)]
fn split_cubic(p: [Point; 4]) -> ([Point; 4], [Point; 4]) {
    let mid = |a: Point, b: Point| Point::new((a.x + b.x) / 2.0, (a.y + b.y) / 2.0);
    let (p0, p1, p2, p3) = (p[0], p[1], p[2], p[3]);
    let a = mid(p0, p1);
    let b = mid(p1, p2);
    let c = mid(p2, p3);
    let d = mid(a, b);
    let e = mid(b, c);
    let f = mid(d, e);
    ([p0, a, d, f], [f, e, c, p3])
}

/// What one walk of a cell's control points establishes.
#[derive(Debug, Clone, Copy)]
struct Survey {
    /// False if any coordinate is NaN or infinite. Additive safety over
    /// upstream, which has no such test and no termination without one.
    finite: bool,
    x0: f64,
    y0: f64,
    x1: f64,
    y1: f64,
}

impl Survey {
    /// `IsSmall`: under two device units in both axes.
    fn is_small(self) -> bool {
        self.x1 - self.x0 < SMALL_PATCH && self.y1 - self.y0 < SMALL_PATCH
    }
}

impl Points {
    /// The twelve boundary control points, in the order the mesh stream
    /// gives them, plus the four derived interior ones for a Coons patch.
    fn from_boundary(boundary: &[Point]) -> Option<Self> {
        if boundary.len() < 12 {
            return None;
        }
        let g = |i: usize| boundary.get(i).copied();
        // The ISO 32000-2 §8.7.4.5.7 boundary walk: p1..p12 counterclockwise
        // from the lower-left corner. Laid into a 4x4 grid whose corners are
        // the patch corners and whose edges are the four cubics.
        // `coons_interior` returns the four derived points in the formula's
        // own order — p11, p12, p21, p22 — and each lands in the grid slot it
        // is named for. A tensor patch fills the same four slots from the
        // stream, which is what makes the two patch types one surface.
        let interior = pdfrum_page::coons_interior(boundary);
        let grid = [
            [g(0)?, g(1)?, g(2)?, g(3)?],
            [g(11)?, interior[0], interior[1], g(4)?],
            [g(10)?, interior[2], interior[3], g(5)?],
            [g(9)?, g(8)?, g(7)?, g(6)?],
        ];
        Some(Self { grid })
    }

    fn from_tensor(points: &[Point]) -> Option<Self> {
        if points.len() < 16 {
            return Self::from_boundary(points);
        }
        let g = |i: usize| points.get(i).copied();
        let grid = [
            [g(0)?, g(1)?, g(2)?, g(3)?],
            [g(11)?, g(12)?, g(13)?, g(4)?],
            [g(10)?, g(15)?, g(14)?, g(5)?],
            [g(9)?, g(8)?, g(7)?, g(6)?],
        ];
        Some(Self { grid })
    }

    /// One walk of the sixteen control points, answering both questions the
    /// subdivider asks of them at once: are they all finite, and what is their
    /// extent.
    ///
    /// The subdivider asks both at every recursion node, so they are one
    /// walk rather than two. They fuse only because the finiteness answer
    /// cannot be read off the extent: `f64::min` returns its non-NaN operand,
    /// so a NaN coordinate leaves no trace in a min/max fold and has to be
    /// tested for as it goes past.
    ///
    /// The extent itself is a fold rather than a chain of [`Rect::union`]s,
    /// which is the same arithmetic — `union` is four `min`/`max`es over a
    /// `Rect` whose corners are equal — without sixteen degenerate `Rect`s and
    /// an `Option` branch per point.
    fn survey(&self) -> Survey {
        let mut s = Survey {
            finite: true,
            x0: f64::INFINITY,
            y0: f64::INFINITY,
            x1: f64::NEG_INFINITY,
            y1: f64::NEG_INFINITY,
        };
        for p in self.grid.iter().flatten() {
            s.finite &= p.x.is_finite() && p.y.is_finite();
            s.x0 = s.x0.min(p.x);
            s.y0 = s.y0.min(p.y);
            s.x1 = s.x1.max(p.x);
            s.y1 = s.y1.max(p.y);
        }
        s
    }

    fn all_finite(&self) -> bool {
        self.survey().finite
    }

    fn bbox(&self) -> Rect {
        let s = self.survey();
        Rect::new(s.x0, s.y0, s.x1, s.y1)
    }

    /// Split every row at `t = 0.5`, halving the patch along its **second**
    /// grid index.
    ///
    /// This is the split the `bottom`/`y_scale` half of the colour lattice
    /// tracks: `colors[0]` sits at grid corner `(0, 0)` and `colors[1]` at
    /// `(0, 3)`, so walking the second index is walking `c0 → c1`.
    fn split_along_columns(&self) -> (Self, Self) {
        let mut first = self.grid;
        let mut second = self.grid;
        for (i, row) in self.grid.iter().enumerate() {
            let (l, r) = split_cubic(*row);
            if let (Some(ls), Some(rs)) = (first.get_mut(i), second.get_mut(i)) {
                *ls = l;
                *rs = r;
            }
        }
        (Self { grid: first }, Self { grid: second })
    }

    /// Split every column at `t = 0.5`, halving the patch along its **first**
    /// grid index — the axis `left`/`x_scale` tracks, `c0 → c3`.
    fn split_along_rows(&self) -> (Self, Self) {
        let mut bottom = self.grid;
        let mut top = self.grid;
        for col in 0..4 {
            let column = [
                self.grid
                    .first()
                    .and_then(|r| r.get(col))
                    .copied()
                    .unwrap_or(Point::ZERO),
                self.grid
                    .get(1)
                    .and_then(|r| r.get(col))
                    .copied()
                    .unwrap_or(Point::ZERO),
                self.grid
                    .get(2)
                    .and_then(|r| r.get(col))
                    .copied()
                    .unwrap_or(Point::ZERO),
                self.grid
                    .get(3)
                    .and_then(|r| r.get(col))
                    .copied()
                    .unwrap_or(Point::ZERO),
            ];
            let (b, t) = split_cubic(column);
            for i in 0..4 {
                if let (Some(slot), Some(&v)) =
                    (bottom.get_mut(i).and_then(|r| r.get_mut(col)), b.get(i))
                {
                    *slot = v;
                }
                if let (Some(slot), Some(&v)) =
                    (top.get_mut(i).and_then(|r| r.get_mut(col)), t.get(i))
                {
                    *slot = v;
                }
            }
        }
        (Self { grid: bottom }, Self { grid: top })
    }

    /// Write the closed path of the twelve outer control points — the
    /// boundary, never the interior ones — into `into`, replacing whatever it
    /// held.
    ///
    /// It takes a buffer rather than returning one because a single mesh
    /// reaches this tens of thousands of times: `shading_axial_radial` fills
    /// 28 368 cells, and a `BezPath` per cell is 28 368 allocate/free pairs
    /// whose contents are six elements long and identical in shape every
    /// time. `truncate(0)` keeps the capacity, so after the first cell the
    /// path costs six pushes into memory that is already warm.
    fn write_boundary_path(&self, into: &mut BezPath) {
        // The boundary walks row 0 left to right, the last column down, row 3
        // back, and the first column up — so the two middle rows contribute
        // only their ends, and the interior four points never appear.
        let [top, upper, lower, bottom] = self.grid;
        into.truncate(0);
        into.move_to(top[0]);
        into.curve_to(top[1], top[2], top[3]);
        into.curve_to(upper[3], lower[3], bottom[3]);
        into.curve_to(bottom[2], bottom[1], bottom[0]);
        into.curve_to(lower[0], upper[0], top[0]);
        into.close_path();
    }
}

/// What the whole subdivision of one patch shares: the device it fills into,
/// the patch's four corner colours, the ramp behind them, and the one path
/// buffer every cell is written through.
///
/// These four are constant down the recursion, where the lattice position and
/// the control points are what each level replaces. Separating them is what
/// lets the path buffer exist at all — a value that must outlive every cell
/// cannot be a parameter that each level re-derives.
struct Cells<'a> {
    dest: &'a mut dyn RenderDevice,
    colors: &'a [IntColor; 4],
    steps: Option<&'a ColorSteps>,
    /// Reused by every cell; see [`Points::write_boundary_path`].
    path: BezPath,
}

/// Where in the patch's colour lattice one cell sits.
///
/// `left`/`x_scale` walk the grid's first index (`c0 → c3`) and
/// `bottom`/`y_scale` its second (`c0 → c1`); each split doubles the scale it
/// halves and each half inherits the position that split assigns it. The four
/// travel together because no operation here reads one without the other
/// three.
#[derive(Debug, Clone, Copy)]
struct Lattice {
    x_scale: i32,
    y_scale: i32,
    left: i32,
    bottom: i32,
}

impl Lattice {
    /// The lattice the whole patch starts from: one cell, at the origin.
    const WHOLE: Self = Self {
        x_scale: 1,
        y_scale: 1,
        left: 0,
        bottom: 0,
    };

    /// The colour at this cell's own corner.
    fn color_at(self, colors: &[IntColor; 4]) -> Option<IntColor> {
        bilinear(colors, self.left, self.bottom, self.x_scale, self.y_scale)
    }

    /// The colour one lattice step away, `dx` cells right and `dy` up.
    fn color_offset(self, colors: &[IntColor; 4], dx: i32, dy: i32) -> Option<IntColor> {
        bilinear(
            colors,
            self.left.saturating_add(dx),
            self.bottom.saturating_add(dy),
            self.x_scale,
            self.y_scale,
        )
    }

    /// The two halves this lattice splits into along the `bottom` axis.
    fn halve_vertically(self) -> (Self, Self) {
        let ys = self.y_scale.saturating_mul(2);
        let bb = self.bottom.saturating_mul(2);
        (
            Self {
                y_scale: ys,
                bottom: bb,
                ..self
            },
            Self {
                y_scale: ys,
                bottom: bb.saturating_add(1),
                ..self
            },
        )
    }

    /// The two halves this lattice splits into along the `left` axis.
    fn halve_horizontally(self) -> (Self, Self) {
        let xs = self.x_scale.saturating_mul(2);
        let ll = self.left.saturating_mul(2);
        (
            Self {
                x_scale: xs,
                left: ll,
                ..self
            },
            Self {
                x_scale: xs,
                left: ll.saturating_add(1),
                ..self
            },
        )
    }
}

/// Subdivide one patch and fill its cells into `cells.dest`.
///
/// The destination is the scratch device: every cell is drawn at **alpha 1.0**
/// with antialiasing off, and the shading's alpha is applied by the caller
/// when the scratch is blitted.
#[expect(
    clippy::cast_sign_loss,
    reason = "every colour component is clamped to 0..=255 immediately before \
              its cast, so no negative value reaches one"
)]
fn subdivide(cells: &mut Cells<'_>, points: Points, at: Lattice, depth: u32) {
    let survey = points.survey();
    if !survey.finite {
        return; // Additive: a crafted mesh cannot spin the recursion forever.
    }
    let small = survey.is_small();
    let Some(c0) = at.color_at(cells.colors) else {
        return;
    };

    let flat = small || depth >= MAX_DEPTH || {
        let (Some(c1), Some(c2), Some(c3)) = (
            at.color_offset(cells.colors, 0, 1),
            at.color_offset(cells.colors, 1, 1),
            at.color_offset(cells.colors, 1, 0),
        ) else {
            return;
        };
        let d_bottom = distance(c3, c0);
        let d_left = distance(c1, c0);
        let d_top = distance(c1, c2);
        let d_right = distance(c2, c3);
        if d_bottom < COLOR_THRESHOLD
            && d_left < COLOR_THRESHOLD
            && d_top < COLOR_THRESHOLD
            && d_right < COLOR_THRESHOLD
        {
            true
        } else {
            // Subdivide along whichever axis still varies.
            let vertical_only = d_bottom < COLOR_THRESHOLD && d_top < COLOR_THRESHOLD;
            let horizontal_only = d_left < COLOR_THRESHOLD && d_right < COLOR_THRESHOLD;
            let next = depth.saturating_add(1);
            if vertical_only {
                let (b, t) = points.split_along_columns();
                let (lo, hi) = at.halve_vertically();
                subdivide(cells, b, lo, next);
                subdivide(cells, t, hi, next);
            } else if horizontal_only {
                let (l, r) = points.split_along_rows();
                let (lo, hi) = at.halve_horizontally();
                subdivide(cells, l, lo, next);
                subdivide(cells, r, hi, next);
            } else {
                // Both axes vary: halve along the columns, then halve each
                // half along the rows, so the four cells inherit the lattice
                // position each of their two splits assigns.
                let (near, far) = points.split_along_columns();
                let (below, above) = at.halve_vertically();
                for (half, band) in [(near, below), (far, above)] {
                    let (lo, hi) = half.split_along_rows();
                    let (l, r) = band.halve_horizontally();
                    subdivide(cells, lo, l, next);
                    subdivide(cells, hi, r, next);
                }
            }
            return;
        }
    };

    if !flat {
        return;
    }
    let color = match cells.steps {
        Some(ramp) => {
            let index = c0.first().copied().unwrap_or(0).clamp(0, 255) as usize;
            match ramp.entry(index) {
                Some(c) => c.with_alpha(255),
                None => return,
            }
        }
        None => Argb {
            a: 255,
            r: c0.first().copied().unwrap_or(0).clamp(0, 255) as u8,
            g: c0.get(1).copied().unwrap_or(0).clamp(0, 255) as u8,
            b: c0.get(2).copied().unwrap_or(0).clamp(0, 255) as u8,
        },
    };
    points.write_boundary_path(&mut cells.path);
    cells.dest.fill_path(
        &cells.path,
        Affine::IDENTITY,
        &Brush::Solid(color.to_peniko()),
        FillRule::Winding,
        // `full_cover`: every pixel the cell touches at all, at full alpha.
        // Thresholding instead would drop a pixel two abutting cells each
        // half-cover, which is a white pin-hole along every internal seam.
        AntiAlias::FullCover,
    );
}

/// Fill one patch's cells into a scratch device.
///
/// The patch's control points must already be in the scratch's pixel space.
pub fn draw_patch(
    dest: &mut dyn RenderDevice,
    patch: &Patch,
    steps: Option<&ColorSteps>,
    component_range: [f32; 2],
    to_bitmap: Affine,
    tensor: bool,
) {
    let transformed: Vec<Point> = patch.points.iter().map(|&p| to_bitmap * p).collect();
    let Some(points) = (if tensor {
        Points::from_tensor(&transformed)
    } else {
        Points::from_boundary(&transformed)
    }) else {
        return;
    };
    if !points.all_finite() {
        return;
    }
    // A patch entirely outside the destination is skipped before any
    // subdivision, as upstream's bbox reject does.
    let bbox = points.bbox();
    if bbox.x1 <= 0.0 || bbox.y1 <= 0.0 {
        return;
    }
    // A ramp is present exactly when the mesh carried parametric values, so
    // it is also what says which of `to_int_color`'s two conversions applies.
    let range = steps.map(|_| component_range);
    let colors = patch.colors.map(|c| to_int_color(c, range));
    let mut cells = Cells {
        dest,
        colors: &colors,
        steps,
        path: BezPath::new(),
    };
    subdivide(&mut cells, points, Lattice::WHOLE, 0);
}

/// Whether a patch's device-space bbox lies wholly outside a target.
#[must_use]
pub fn patch_is_offscreen(patch: &Patch, to_bitmap: Affine, width: u32, height: u32) -> bool {
    let mut bbox: Option<Rect> = None;
    for &p in &patch.points {
        let q = to_bitmap * p;
        let cell = Rect::new(q.x, q.y, q.x, q.y);
        bbox = Some(match bbox {
            Some(acc) => acc.union(cell),
            None => cell,
        });
    }
    let Some(b) = bbox else { return true };
    b.x1 <= 0.0 || b.x0 >= f64::from(width) || b.y1 <= 0.0 || b.y0 >= f64::from(height)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A closed path over a patch's twelve outer control points — the
    /// silhouette rather than the filled cells.
    ///
    /// The rasterizer never draws a patch this way: it subdivides and fills
    /// cells, writing each through the one reused buffer
    /// [`Points::write_boundary_path`] takes. This spelling exists so a test
    /// can pin [`Points::from_boundary`]'s corner ordering, which is the one
    /// relationship nothing else catches.
    fn patch_outline(patch: &Patch, to_bitmap: Affine) -> BezPath {
        let transformed: Vec<Point> = patch.points.iter().map(|&p| to_bitmap * p).collect();
        Points::from_boundary(&transformed)
            .map(|points| {
                let mut out = BezPath::new();
                points.write_boundary_path(&mut out);
                out
            })
            .unwrap_or_default()
    }

    /// A corner colour with no ramp behind it — the direct-colour conversion.
    fn to_int_color_plain(c: Rgb) -> IntColor {
        to_int_color(c, None)
    }

    fn square_patch(size: f64, colors: [Rgb; 4]) -> Patch {
        // A square whose edges are straight cubics: 12 boundary points
        // counterclockwise from the lower-left.
        let s = size;
        let t = s / 3.0;
        let pts = vec![
            Point::new(0.0, 0.0),
            Point::new(0.0, t),
            Point::new(0.0, 2.0 * t),
            Point::new(0.0, s),
            Point::new(t, s),
            Point::new(2.0 * t, s),
            Point::new(s, s),
            Point::new(s, 2.0 * t),
            Point::new(s, t),
            Point::new(s, 0.0),
            Point::new(2.0 * t, 0.0),
            Point::new(t, 0.0),
        ];
        Patch {
            points: pts.into_boxed_slice(),
            colors,
        }
    }

    /// The lattice axis each split advances, pinned against the grid.
    ///
    /// This is the one relationship the whole rasterizer rests on and the one
    /// nothing else catches: swapping the two splits leaves every cell the
    /// right shape and the right size, and reflects the *colour* field across
    /// the patch's anti-diagonal. On `2_shading_type_6_00` that was 98% of
    /// pixels differing at a mean of 41 counts, with the geometry perfect.
    ///
    /// `colors` are indexed `[c0, c1, c2, c3]` and `bilinear` reads them as
    /// `c0 -> c3` along `left`/`x_scale` and `c0 -> c1` along
    /// `bottom`/`y_scale`. So `left` must advance with the split that walks
    /// the grid's **first** index and `bottom` with the **second**.
    #[test]
    fn each_split_advances_the_lattice_axis_it_walks() {
        let p = Points::from_boundary(&square_patch(12.0, [Rgb::BLACK; 4]).points).expect("built");
        // Corner (0, 0) is grid[0][0] and stays put under both splits' first
        // half; the far corner grid[3][3] is what moves.
        let (rows_lo, rows_hi) = p.split_along_rows();
        let (cols_lo, cols_hi) = p.split_along_columns();

        // Splitting along the rows halves the first index, so the halves
        // differ in grid[3][*] and agree on grid[0][*].
        assert_eq!(rows_lo.grid[0], p.grid[0], "the first row is untouched");
        assert_eq!(rows_hi.grid[3], p.grid[3], "and so is the last");
        assert_ne!(rows_lo.grid[3], rows_hi.grid[3]);

        // Splitting along the columns halves the second index instead, so
        // every row's *ends* move and the two halves meet in the middle.
        assert_eq!(cols_lo.grid[0][0], p.grid[0][0]);
        assert_eq!(cols_hi.grid[0][3], p.grid[0][3]);
        assert_eq!(
            cols_lo.grid[0][3], cols_hi.grid[0][0],
            "the halves share the split point"
        );
        assert_eq!(
            rows_lo.grid[3][0], rows_hi.grid[0][0],
            "and so do the other axis's"
        );
    }

    /// The lattice halvings are the arithmetic the old parameter list wrote
    /// inline, unchanged.
    ///
    /// `Points::split_along_columns` walks the grid's second index, which is
    /// the axis `bottom`/`y_scale` tracks, so it must pair with
    /// `halve_vertically`; `split_along_rows` walks the first and pairs with
    /// `halve_horizontally`. `each_split_advances_the_lattice_axis_it_walks`
    /// pins the geometry half of that relationship and this pins the
    /// arithmetic half — together they are what stops the colour field being
    /// reflected across the patch's anti-diagonal.
    #[test]
    fn halving_a_lattice_doubles_the_scale_and_indexes_the_half() {
        let (lo, hi) = Lattice::WHOLE.halve_vertically();
        assert_eq!((lo.y_scale, lo.bottom), (2, 0));
        assert_eq!((hi.y_scale, hi.bottom), (2, 1));
        assert_eq!(
            (lo.x_scale, lo.left, hi.x_scale, hi.left),
            (1, 0, 1, 0),
            "the other axis is untouched"
        );

        let (lo, hi) = Lattice::WHOLE.halve_horizontally();
        assert_eq!((lo.x_scale, lo.left), (2, 0));
        assert_eq!((hi.x_scale, hi.left), (2, 1));
        assert_eq!(
            (lo.y_scale, lo.bottom, hi.y_scale, hi.bottom),
            (1, 0, 1, 0),
            "and so is this one"
        );

        // The four-way split is one halving of each, in that order, and the
        // four cells it produces are the lattice's four quadrants.
        let (below, above) = Lattice::WHOLE.halve_vertically();
        let quadrants: Vec<(i32, i32)> = [below, above]
            .into_iter()
            .flat_map(|band| {
                let (l, r) = band.halve_horizontally();
                [(l.left, l.bottom), (r.left, r.bottom)]
            })
            .collect();
        assert_eq!(quadrants, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
        assert_eq!(above.y_scale, 2);
    }

    /// A Coons patch's four derived points land in the slots a tensor patch
    /// reads from the stream, in the same order.
    ///
    /// The two patch types are one surface, and that identity *is* the
    /// derivation: feeding a tensor patch the interiors a Coons patch would
    /// derive must give the same grid. Transposing the middle two — which is
    /// the easy mistake, since `p12` and `p21` are mirror images of one
    /// formula — shears the surface in a way only a curved patch shows.
    #[test]
    fn coons_interiors_land_in_the_tensor_slots() {
        // A deliberately asymmetric boundary, so p12 and p21 differ.
        let pts: Vec<Point> = [
            (0.0, 0.0),
            (1.0, 4.0),
            (2.0, 8.0),
            (3.0, 12.0),
            (7.0, 13.0),
            (11.0, 14.0),
            (15.0, 15.0),
            (14.0, 11.0),
            (13.0, 7.0),
            (12.0, 3.0),
            (8.0, 2.0),
            (4.0, 1.0),
        ]
        .into_iter()
        .map(|(x, y)| Point::new(x, y))
        .collect();
        let coons = Points::from_boundary(&pts).expect("built");
        let interior = pdfrum_page::coons_interior(&pts);
        assert_ne!(
            interior[1], interior[2],
            "the fixture separates p12 and p21"
        );

        // The same twelve points plus those four interiors, in the stream's
        // tensor order: c12 = p11, c13 = p12, c14 = p22, c15 = p21.
        let mut tensor_pts = pts;
        tensor_pts.extend([interior[0], interior[1], interior[3], interior[2]]);
        let tensor = Points::from_tensor(&tensor_pts).expect("built");
        assert_eq!(coons.grid, tensor.grid);
    }

    /// A parametric corner colour is mapped into the ramp across the mesh's
    /// own decode range, not the unit interval.
    #[test]
    fn a_ramp_corner_maps_across_the_meshs_decode_range() {
        let t = |v: f32| Rgb {
            r: v,
            g: 0.0,
            b: 0.0,
        };
        // The unit interval is the easy case and the one that hid the bug.
        assert_eq!(to_int_color(t(0.0), Some([0.0, 1.0])), [0, 0, 0]);
        assert_eq!(to_int_color(t(1.0), Some([0.0, 1.0])), [255, 0, 0]);
        // `[1, 2]` and `[0, 255]` both occur in the corpus, and reading them
        // as `[0, 1]` pins every corner to one end of the ramp.
        assert_eq!(to_int_color(t(1.0), Some([1.0, 2.0])), [0, 0, 0]);
        assert_eq!(to_int_color(t(2.0), Some([1.0, 2.0])), [255, 0, 0]);
        assert_eq!(to_int_color(t(1.5), Some([1.0, 2.0])), [127, 0, 0]);
        assert_eq!(to_int_color(t(255.0), Some([0.0, 255.0])), [255, 0, 0]);
        // A degenerate range is upstream's divide-by-zero guard.
        assert_eq!(to_int_color(t(9.0), Some([3.0, 3.0])), [0, 0, 0]);
        // Without a ramp the value is a real colour, truncated as the C++
        // cast truncates.
        assert_eq!(
            to_int_color(
                Rgb {
                    r: 0.5,
                    g: 1.0,
                    b: 0.0
                },
                None
            ),
            [127, 255, 0]
        );
    }

    /// A NaN coordinate is caught by the survey's own flag, not by its extent.
    ///
    /// `f64::min` and `f64::max` return their non-NaN operand, so a NaN point
    /// passes through a min/max fold leaving the extent finite and plausible.
    /// Fusing the finiteness test into that fold is only correct because the
    /// flag is carried separately, and this is what says so.
    #[test]
    fn a_nan_control_point_leaves_the_extent_finite_and_the_flag_false() {
        let mut patch = square_patch(10.0, [Rgb::BLACK; 4]);
        #[expect(
            clippy::indexing_slicing,
            reason = "the fixture is a square patch with all twelve boundary \
                      points present, so index 5 exists by construction"
        )]
        {
            patch.points[5] = Point::new(f64::NAN, f64::NAN);
        }
        let s = Points::from_boundary(&patch.points)
            .expect("built")
            .survey();
        assert!(!s.finite, "the flag catches it");
        assert!(
            s.x0.is_finite() && s.y0.is_finite() && s.x1.is_finite() && s.y1.is_finite(),
            "and the extent does not: min/max swallowed the NaN"
        );
    }

    #[test]
    fn is_small_is_a_two_device_unit_bbox() {
        let p = Points::from_boundary(&square_patch(1.5, [Rgb::BLACK; 4]).points).expect("built");
        assert!(p.survey().is_small());
        let p = Points::from_boundary(&square_patch(3.0, [Rgb::BLACK; 4]).points).expect("built");
        assert!(!p.survey().is_small());
    }

    #[test]
    fn color_threshold_stops_subdivision() {
        // Four corner colours within three counts of each other never
        // subdivide, whatever the patch's size.
        let near = [
            Rgb {
                r: 0.0,
                g: 0.0,
                b: 0.0,
            },
            Rgb {
                r: 1.0 / 255.0,
                g: 0.0,
                b: 0.0,
            },
            Rgb {
                r: 2.0 / 255.0,
                g: 0.0,
                b: 0.0,
            },
            Rgb {
                r: 3.0 / 255.0,
                g: 0.0,
                b: 0.0,
            },
        ];
        let colors = near.map(|c| to_int_color(c, None));
        let d = distance(colors[0], colors[3]);
        assert!(d < COLOR_THRESHOLD, "delta {d} must be under the threshold");
    }

    #[test]
    fn integer_interpolate_detects_overflow() {
        assert_eq!(interpolate(0, 10, 1, 2), Some(5));
        assert_eq!(
            interpolate(7, 7, 5, 0),
            Some(7),
            "a zero span keeps the endpoint"
        );
        assert_eq!(
            interpolate(0, i32::MAX, i32::MAX, 1),
            None,
            "overflow aborts the cell"
        );
    }

    #[test]
    fn distance_is_the_max_component_delta() {
        assert_eq!(distance([0, 0, 0], [3, 9, 1]), 9);
        assert_eq!(distance([10, 10, 10], [10, 10, 10]), 0);
    }

    #[test]
    fn subdivision_axis_choice_follows_the_varying_edges() {
        // Colours varying only bottom-to-top subdivide vertically; only
        // left-to-right, horizontally. Verified through `bilinear`'s deltas,
        // which is what the choice reads.
        let vertical = [
            to_int_color_plain(Rgb {
                r: 0.0,
                g: 0.0,
                b: 0.0,
            }),
            to_int_color_plain(Rgb {
                r: 1.0,
                g: 1.0,
                b: 1.0,
            }),
            to_int_color_plain(Rgb {
                r: 1.0,
                g: 1.0,
                b: 1.0,
            }),
            to_int_color_plain(Rgb {
                r: 0.0,
                g: 0.0,
                b: 0.0,
            }),
        ];
        let c0 = bilinear(&vertical, 0, 0, 1, 1).expect("interpolates");
        let c1 = bilinear(&vertical, 0, 1, 1, 1).expect("interpolates");
        let c3 = bilinear(&vertical, 1, 0, 1, 1).expect("interpolates");
        assert!(
            distance(c3, c0) < COLOR_THRESHOLD,
            "the bottom edge is flat"
        );
        assert!(distance(c1, c0) >= COLOR_THRESHOLD, "the left edge is not");
    }

    #[test]
    fn non_finite_control_points_drop_the_patch() {
        let mut patch = square_patch(10.0, [Rgb::BLACK; 4]);
        #[expect(
            clippy::indexing_slicing,
            reason = "the fixture is a square patch with all twelve boundary \
                      points present, so index 3 exists by construction"
        )]
        {
            patch.points[3] = Point::new(f64::NAN, 0.0);
        }
        let outline = patch_outline(&patch, Affine::IDENTITY);
        // The outline still exists as a path, but the subdivider declines it.
        assert!(!outline.elements().is_empty());
        let points = Points::from_boundary(&patch.points).expect("built");
        assert!(!points.all_finite());
    }

    #[test]
    fn a_patch_left_of_the_target_is_offscreen() {
        let patch = square_patch(4.0, [Rgb::BLACK; 4]);
        assert!(patch_is_offscreen(
            &patch,
            Affine::translate((-50.0, 0.0)),
            20,
            20
        ));
        assert!(!patch_is_offscreen(&patch, Affine::IDENTITY, 20, 20));
    }

    #[test]
    fn split_cubic_halves_a_straight_line_at_its_midpoint() {
        let line = [
            Point::new(0.0, 0.0),
            Point::new(1.0, 0.0),
            Point::new(2.0, 0.0),
            Point::new(3.0, 0.0),
        ];
        let (l, r) = split_cubic(line);
        assert!((l[3].x - 1.5).abs() < 1e-9);
        assert!((r[0].x - 1.5).abs() < 1e-9);
        assert!((r[3].x - 3.0).abs() < 1e-9);
    }

    #[test]
    fn boundary_path_uses_the_twelve_outer_points_only() {
        let patch = square_patch(9.0, [Rgb::BLACK; 4]);
        let outline = patch_outline(&patch, Affine::IDENTITY);
        let bbox = outline.bounding_box();
        assert!((bbox.x0 - 0.0).abs() < 1e-9);
        assert!((bbox.x1 - 9.0).abs() < 1e-9);
        assert!(outline.area().abs() > 0.0);
    }
}