Skip to main content

pdfrum_page/image/
rows.rs

1//! Image samples as a pull pipeline of rows.
2//!
3//! An image reaches the device as a sequence of *stages*, and a stage is a
4//! type. Each one yields the image one row at a time, at source width, into a
5//! buffer it owns and reuses; the next stage borrows that row and produces its
6//! own. The representation of a row is its type, so a stage can only accept
7//! what the previous one produces — and a full-size intermediate copy cannot
8//! exist, because no type in the chain asks for one.
9//!
10//! This is the shape PDFium's image path has had all along.
11//! `CStretchEngine::Continue` pulls `CPDF_DIB::GetScanline` per source row
12//! (`cstretchengine.cpp:346`), so unpacking, colour conversion and the
13//! horizontal stretch all happen inside one scanline's lifetime and the only
14//! full-height buffer is destination-width.
15//!
16//! # The stages
17//!
18//! ```text
19//! Pixels ──[Source]──> Row<'_, P>  ──[Converted]──> Row<'_, Rgba8>
20//! ```
21//!
22//! [`Converted`] is the one place a sample becomes a colour, and
23//! `convert_row` is the one function that does it. There is no
24//! per-pixel entry point beside it: a caller that wants a whole image walks
25//! the rows, and a caller that wants one pixel does not exist.
26//!
27//! # Where the packed samples come in
28//!
29//! [`Source`] takes [`Samples`], not [`Pixels`]: for a [`Samples::Packed`]
30//! image it drives an [`Unpacked`] stage that widens the row it is about to
31//! yield, and for a [`Samples::Whole`] one it borrows straight out of the
32//! decoded buffer. So the pipeline is `Unpacked -> Converted` for everything
33//! the filter chain left packed: there is no full-size widened buffer.
34
35use crate::color::{Rgb, adobe_cmyk_to_srgb};
36use crate::image::{BitImage, Pixels, Samples, Unpacked};
37
38/// A run of pixels in one representation.
39///
40/// The lifetime is the stage's own buffer: a row is borrowed for as long as
41/// the caller is looking at it and is overwritten by the next `next` call,
42/// which is what keeps the pipeline to one row of working memory per stage.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Row<'a, P>(&'a [P]);
45
46impl<'a, P> Row<'a, P> {
47    /// The pixels.
48    #[must_use]
49    pub const fn pixels(self) -> &'a [P] {
50        self.0
51    }
52}
53
54/// Whatever yields an image's samples one row at a time, at source width.
55///
56/// The one seam of the pipeline. `next` returns `None` at the end of the
57/// image and never afterwards yields anything again; a stage that cannot
58/// produce a row it expected yields the fallback its own documentation names
59/// rather than stopping early, because a malformed image must still paint.
60pub trait Rows {
61    /// What one pixel of a row this stage produces looks like.
62    type Pixel;
63
64    /// The next row, or `None` when the image is exhausted.
65    fn next(&mut self) -> Option<Row<'_, Self::Pixel>>;
66}
67
68/// Premultiplied RGBA, the representation the device buffer wants.
69///
70/// `#[repr(C)]` over an array so a row of them is a row of pixels to index,
71/// not a row of bytes to multiply an index by four.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73#[repr(C)]
74pub struct Rgba8(pub [u8; 4]);
75
76/// Eight-bit red, green, blue.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78#[repr(C)]
79pub struct Rgb8(pub [u8; 3]);
80
81/// A palette with every entry already in the representation a row wants.
82///
83/// A palette has at most 256 entries and an image has as many pixels as it
84/// has, so resolving it once and indexing it is the same answer as resolving
85/// per pixel for a fraction of the work. Built once when the pipeline is,
86/// never per row.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Palette(Box<[Rgb8]>);
89
90impl Palette {
91    /// Encode a space's resolved palette.
92    #[must_use]
93    pub fn new(entries: &[Rgb]) -> Self {
94        Self(entries.iter().map(|c| Rgb8(c.to_bytes())).collect())
95    }
96
97    /// The entry for an index, or black when the palette does not have one —
98    /// which is `color_at`'s own fallback, kept deliberately.
99    #[must_use]
100    pub fn get(&self, index: u8) -> Rgb8 {
101        self.0
102            .get(usize::from(index))
103            .copied()
104            .unwrap_or(Rgb8([0, 0, 0]))
105    }
106}
107
108/// The source stage: an image's [`Samples`] walked one row at a time.
109///
110/// A [`Samples::Whole`] arm borrows straight out of the decoded buffer, so it
111/// owns no memory at all — except for [`Pixels::Stencil`], whose bits have to
112/// be widened into something a row can borrow. A [`Samples::Packed`] one owns
113/// the [`Unpacked`] stage's single row buffer, and nothing more.
114#[derive(Debug)]
115pub struct Source<'a> {
116    kind: SourceKind<'a>,
117    width: usize,
118    height: u32,
119    y: u32,
120}
121
122/// Which representation a [`Source`] is walking.
123#[derive(Debug)]
124enum SourceKind<'a> {
125    /// A stencil widened one row at a time into `scratch`.
126    Stencil {
127        bits: &'a BitImage,
128        scratch: Vec<u8>,
129    },
130    /// Still-packed samples, widened one row at a time by [`Unpacked`].
131    ///
132    /// The component count decides which [`Samples`] arm the widened row is,
133    /// exactly as it decided which [`Pixels`] variant the eager pass built.
134    Packed {
135        rows: Unpacked<'a>,
136        components: usize,
137    },
138    Gray(&'a [u8]),
139    Rgb(&'a [u8]),
140    Cmyk(&'a [u8]),
141    Indexed(&'a [u8]),
142}
143
144/// One row of a [`Source`], in whichever representation the image has.
145///
146/// An enum rather than a generic parameter because the *image* decides which
147/// arm it is, at run time, and a caller that must handle all five would
148/// otherwise be five monomorphised copies of the same loop.
149#[derive(Debug, Clone, Copy)]
150pub(crate) enum SampleRow<'a> {
151    /// One grey component per pixel. A stencil arrives here too, already
152    /// widened: a set bit is ink, which is 0, and a clear one is 255.
153    Gray(&'a [u8]),
154    /// Three components per pixel, red then green then blue.
155    Rgb(&'a [u8]),
156    /// Four components per pixel, cyan, magenta, yellow, black.
157    Cmyk(&'a [u8]),
158    /// One palette index per pixel.
159    Indexed(&'a [u8]),
160}
161
162impl<'a> Source<'a> {
163    /// Start walking `samples`, an image `width` by `height`.
164    #[must_use]
165    pub fn new(samples: &'a Samples, width: u32, height: u32) -> Self {
166        let width = width as usize;
167        let kind = match samples {
168            Samples::Packed(p) => SourceKind::Packed {
169                rows: Unpacked::new(p),
170                components: p.components(),
171            },
172            Samples::Whole(Pixels::Stencil(bits)) => SourceKind::Stencil {
173                bits,
174                scratch: vec![0_u8; width],
175            },
176            Samples::Whole(Pixels::Gray8(d)) => SourceKind::Gray(d),
177            Samples::Whole(Pixels::Rgb8(d)) => SourceKind::Rgb(d),
178            Samples::Whole(Pixels::Cmyk8(d)) => SourceKind::Cmyk(d),
179            Samples::Whole(Pixels::Indexed { indices, .. }) => SourceKind::Indexed(indices),
180        };
181        Self {
182            kind,
183            width,
184            height,
185            y: 0,
186        }
187    }
188
189    /// A source positioned to yield exactly the one row `y`, and then stop.
190    ///
191    /// Tests reach for a single pixel — including one far past the end, to
192    /// check the fallback — and walking down to its row from the top would be
193    /// quadratic in `y`. The pipeline itself never wants this: the render path
194    /// walks every row in order, which is the whole point of it.
195    #[cfg(test)]
196    pub(crate) fn at_row(samples: &'a Samples, width: u32, y: u32) -> Self {
197        let mut source = Self::new(samples, width, y.saturating_add(1));
198        match &mut source.kind {
199            // A packed source has no index to skip to — its rows come out of
200            // a bit walk that has to run — so it is pulled forward instead.
201            // Only tests reach this, and only for small `y`.
202            SourceKind::Packed { rows, .. } => {
203                for _ in 0..y {
204                    if rows.next_row().is_none() {
205                        break;
206                    }
207                }
208                source.y = y;
209            }
210            _ => source.y = y,
211        }
212        source
213    }
214
215    /// The next row of samples, or `None` past the bottom of the image.
216    ///
217    /// A row the buffer only partly holds yields **the samples it has**, not
218    /// nothing: the per-pixel path this replaced read every sample with a
219    /// `get(..).unwrap_or(0)`, so a truncated stream painted the bytes that
220    /// were there and black beyond them. Dropping the row instead would move
221    /// that boundary, which `a_truncated_stream_is_zero_padded_rather_than_
222    /// rejected` and the corpus's damaged images both pin. [`Converted`]
223    /// clears the tail of its own buffer so the missing pixels read as black
224    /// rather than as the row before.
225    pub(crate) fn next_row(&mut self) -> Option<SampleRow<'_>> {
226        if self.y >= self.height {
227            return None;
228        }
229        let y = self.y as usize;
230        self.y += 1;
231        let width = self.width;
232        // The row's byte range for a `components`-wide representation,
233        // clipped to what the buffer actually holds and trimmed to a whole
234        // number of pixels.
235        let span = |components: usize, len: usize| -> Option<(usize, usize)> {
236            let stride = width.checked_mul(components)?;
237            let at = y.checked_mul(stride)?;
238            // `at` can be past the end entirely, for a row the buffer never
239            // reached: that is a zero-length span, not an absent row.
240            let end = at.checked_add(stride)?.min(len).max(at);
241            let whole = (end - at) / components * components;
242            Some((at.min(len), at.min(len).checked_add(whole)?))
243        };
244        match &mut self.kind {
245            SourceKind::Stencil { bits, scratch } => {
246                for (x, slot) in scratch.iter_mut().enumerate() {
247                    let set = u32::try_from(x).is_ok_and(|x| bits.pixel(x, self.y - 1));
248                    // A set bit is ink, which reads 0; a clear one is paper,
249                    // which reads 255. The stencil's *colour* is applied
250                    // later, so this stage only preserves that convention.
251                    *slot = if set { 0 } else { 255 };
252                }
253                Some(SampleRow::Gray(scratch))
254            }
255            SourceKind::Gray(d) => {
256                let (a, b) = span(1, d.len())?;
257                Some(SampleRow::Gray(d.get(a..b)?))
258            }
259            SourceKind::Rgb(d) => {
260                let (a, b) = span(3, d.len())?;
261                Some(SampleRow::Rgb(d.get(a..b)?))
262            }
263            SourceKind::Cmyk(d) => {
264                let (a, b) = span(4, d.len())?;
265                Some(SampleRow::Cmyk(d.get(a..b)?))
266            }
267            SourceKind::Indexed(d) => {
268                let (a, b) = span(1, d.len())?;
269                Some(SampleRow::Indexed(d.get(a..b)?))
270            }
271            // `Unpacked` yields exactly the row the eager pass would have
272            // written, already at a byte per component, so the representation
273            // is the component count and nothing else.
274            SourceKind::Packed { rows, components } => {
275                let row = rows.next_row()?;
276                Some(match *components {
277                    1 => SampleRow::Gray(row),
278                    4 => SampleRow::Cmyk(row),
279                    _ => SampleRow::Rgb(row),
280                })
281            }
282        }
283    }
284}
285
286/// Components to premultiplied RGBA, one row at a time.
287///
288/// The second and last stage of the pipeline: it takes whatever
289/// [`Source`] produced, runs it through `convert_row`, and joins
290/// the mask alpha, the matte and the transfer function — all per-pixel
291/// decisions that belong to this stage.
292#[derive(Debug)]
293pub struct Converted<'a> {
294    source: Source<'a>,
295    palette: Option<Palette>,
296    buf: Vec<Rgba8>,
297}
298
299impl<'a> Converted<'a> {
300    /// Convert the rows of `source`, resolving indices through `palette`.
301    ///
302    /// The palette is `Some` exactly when the source is [`Pixels::Indexed`];
303    /// an indexed row with no palette resolves every index to black, which is
304    /// the fallback the per-pixel path had.
305    #[must_use]
306    pub fn new(source: Source<'a>, palette: Option<Palette>) -> Self {
307        let width = source.width;
308        Self {
309            source,
310            palette,
311            buf: vec![Rgba8::default(); width],
312        }
313    }
314
315    /// The next row converted in place, handed to `finish` for the alpha, the
316    /// matte and the transfer function before anyone else sees it.
317    ///
318    /// The conversion itself produces *opaque* RGBA — alpha is not the colour
319    /// space's business — and `finish` is where the caller's own per-pixel
320    /// business goes. Passing it in rather than exposing the buffer keeps the
321    /// row's mutable lifetime inside this stage, which is what lets the next
322    /// stage borrow the finished row immutably straight afterwards.
323    pub fn next_row_with(&mut self, finish: impl FnOnce(&mut [Rgba8])) -> Option<Row<'_, Rgba8>> {
324        let samples = self.source.next_row()?;
325        convert_row(samples, self.palette.as_ref(), &mut self.buf);
326        finish(&mut self.buf);
327        Some(Row(&self.buf))
328    }
329}
330
331impl Rows for Converted<'_> {
332    type Pixel = Rgba8;
333
334    fn next(&mut self) -> Option<Row<'_, Rgba8>> {
335        self.next_row_with(|_| ())
336    }
337}
338
339/// The one conversion: a row of samples becomes a row of opaque RGBA.
340///
341/// Every arm is the arithmetic `Pixels::sample_bytes` ran per pixel, hoisted
342/// to a row: the same table for CMYK, the same palette fallback for indices,
343/// the same widening for grey. What changed is that the match on the image's
344/// representation happens once per row instead of once per pixel, and the
345/// index arithmetic is a walk rather than a multiply and two bounds checks.
346///
347/// A `dst` shorter than the row converts as much as it holds. A `dst` longer
348/// than it — which is a row the source could only partly supply — takes
349/// **opaque black** in the tail, because that is what the per-pixel path
350/// produced: it read each missing component through `get(..).unwrap_or(0)`
351/// and then made a colour of the zeroes.
352fn convert_row(samples: SampleRow<'_>, palette: Option<&Palette>, dst: &mut [Rgba8]) {
353    let converted = match samples {
354        SampleRow::Gray(src) => {
355            for (slot, &v) in dst.iter_mut().zip(src) {
356                *slot = Rgba8([v, v, v, 255]);
357            }
358            src.len()
359        }
360        SampleRow::Rgb(src) => {
361            for (slot, px) in dst.iter_mut().zip(src.as_chunks::<3>().0) {
362                let [red, green, blue] = *px;
363                *slot = Rgba8([red, green, blue, 255]);
364            }
365            src.len() / 3
366        }
367        SampleRow::Cmyk(src) => {
368            for (slot, px) in dst.iter_mut().zip(src.as_chunks::<4>().0) {
369                let [cyan, magenta, yellow, black] = *px;
370                let rgb = adobe_cmyk_to_srgb(cyan, magenta, yellow, black);
371                *slot = Rgba8([rgb[0], rgb[1], rgb[2], 255]);
372            }
373            src.len() / 4
374        }
375        SampleRow::Indexed(src) => {
376            for (slot, &index) in dst.iter_mut().zip(src) {
377                let Rgb8(rgb) = palette.map_or(Rgb8([0, 0, 0]), |p| p.get(index));
378                *slot = Rgba8([rgb[0], rgb[1], rgb[2], 255]);
379            }
380            src.len()
381        }
382    };
383    // The tail the source could not supply. Without this it would show the
384    // previous row, since the buffer is reused across rows.
385    if let Some(tail) = dst.get_mut(converted..) {
386        let black = match samples {
387            // An absent *index* is 0, and index 0's palette entry is a real
388            // colour — the same ladder the per-pixel path walked.
389            SampleRow::Indexed(_) => {
390                let Rgb8(rgb) = palette.map_or(Rgb8([0, 0, 0]), |p| p.get(0));
391                Rgba8([rgb[0], rgb[1], rgb[2], 255])
392            }
393            SampleRow::Cmyk(_) => {
394                let rgb = adobe_cmyk_to_srgb(0, 0, 0, 0);
395                Rgba8([rgb[0], rgb[1], rgb[2], 255])
396            }
397            SampleRow::Gray(_) | SampleRow::Rgb(_) => Rgba8([0, 0, 0, 255]),
398        };
399        tail.fill(black);
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    fn gray(data: &[u8]) -> Samples {
408        Samples::Whole(Pixels::Gray8(data.into()))
409    }
410
411    /// The still-packed form of the same grey samples: eight bits, one
412    /// component, the identity decode.
413    fn packed_gray(data: &[u8], width: u32, height: u32) -> Samples {
414        Samples::Packed(crate::image::Packed::new(
415            data.into(),
416            crate::image::Depth::Eight,
417            1,
418            width as usize,
419            width,
420            height,
421            &crate::color::ColorSpace::DeviceGray,
422            None,
423        ))
424    }
425
426    #[test]
427    fn a_grey_row_widens_to_opaque_rgba() {
428        let px = gray(&[0, 128, 255, 7]);
429        let mut c = Converted::new(Source::new(&px, 2, 2), None);
430        let first = c.next().expect("first row").pixels().to_vec();
431        assert_eq!(
432            first,
433            vec![Rgba8([0, 0, 0, 255]), Rgba8([128, 128, 128, 255])]
434        );
435        let second = c.next().expect("second row").pixels().to_vec();
436        assert_eq!(
437            second,
438            vec![Rgba8([255, 255, 255, 255]), Rgba8([7, 7, 7, 255])]
439        );
440        assert!(c.next().is_none());
441    }
442
443    #[test]
444    fn an_rgb_row_keeps_its_component_order() {
445        let px = Samples::Whole(Pixels::Rgb8(Box::new([1, 2, 3, 4, 5, 6])));
446        let mut c = Converted::new(Source::new(&px, 2, 1), None);
447        let row = c.next().expect("row").pixels().to_vec();
448        assert_eq!(row, vec![Rgba8([1, 2, 3, 255]), Rgba8([4, 5, 6, 255])]);
449    }
450
451    #[test]
452    fn an_indexed_row_reads_its_palette_and_falls_back_to_black() {
453        let px = Samples::Whole(Pixels::Indexed {
454            indices: Box::new([0, 1, 9]),
455            palette: Box::new([]),
456        });
457        let palette = Palette::new(&[
458            Rgb {
459                r: 1.0,
460                g: 0.0,
461                b: 0.0,
462            },
463            Rgb {
464                r: 0.0,
465                g: 1.0,
466                b: 0.0,
467            },
468        ]);
469        let mut c = Converted::new(Source::new(&px, 3, 1), Some(palette));
470        let row = c.next().expect("row").pixels().to_vec();
471        assert_eq!(
472            row,
473            vec![
474                Rgba8([255, 0, 0, 255]),
475                Rgba8([0, 255, 0, 255]),
476                // Index 9 is past the end of a two-entry palette: black.
477                Rgba8([0, 0, 0, 255]),
478            ]
479        );
480    }
481
482    #[test]
483    fn a_stencils_set_bit_is_ink_and_its_clear_bit_is_paper() {
484        // One byte holds a row of up to eight bits, MSB first: `0b1010_0000`
485        // is set, clear, set, clear across four pixels.
486        let bits = BitImage {
487            width: 4,
488            height: 1,
489            row_bytes: 1,
490            bits: vec![0b1010_0000],
491        };
492        let px = Samples::Whole(Pixels::Stencil(bits));
493        let mut c = Converted::new(Source::new(&px, 4, 1), None);
494        let row = c.next().expect("row").pixels().to_vec();
495        assert_eq!(
496            row,
497            vec![
498                Rgba8([0, 0, 0, 255]),
499                Rgba8([255, 255, 255, 255]),
500                Rgba8([0, 0, 0, 255]),
501                Rgba8([255, 255, 255, 255]),
502            ]
503        );
504    }
505
506    /// A buffer too short for the dimensions paints the samples it has and
507    /// black beyond them, for every row the image declares.
508    ///
509    /// This is the per-pixel path's own degradation — every sample was read
510    /// through `get(..).unwrap_or(0)` — and moving that boundary would change
511    /// every damaged image in the corpus. A partial row keeps its samples; a
512    /// wholly absent one is still yielded, all black.
513    #[test]
514    fn a_short_buffer_paints_black_past_its_end() {
515        // Five bytes of a 2x4 grey image: rows 0 and 1 whole, row 2 half, row
516        // 3 absent.
517        let px = gray(&[1, 2, 3, 4, 5]);
518        let mut c = Converted::new(Source::new(&px, 2, 4), None);
519        let black = Rgba8([0, 0, 0, 255]);
520        let row = |v: &[u8]| -> Vec<Rgba8> { v.iter().map(|&b| Rgba8([b, b, b, 255])).collect() };
521        assert_eq!(c.next().expect("row 0").pixels(), row(&[1, 2]));
522        assert_eq!(c.next().expect("row 1").pixels(), row(&[3, 4]));
523        // The half row keeps its one sample; the missing one is black, not a
524        // leftover of the row before.
525        assert_eq!(
526            c.next().expect("row 2").pixels(),
527            vec![Rgba8([5, 5, 5, 255]), black]
528        );
529        assert_eq!(c.next().expect("row 3").pixels(), vec![black, black]);
530        // And the image ends where it said it would.
531        assert!(c.next().is_none());
532    }
533
534    /// The lazy arm and the eager one are the same rows.
535    ///
536    /// `Unpacked` widens as the pipeline pulls; the eager `unpack` widened
537    /// first and `Source` walked the result. For eight-bit identity-decoded
538    /// samples the two are the same bytes, which is what makes step 5 a move
539    /// of work rather than a change of arithmetic.
540    #[test]
541    fn a_packed_source_yields_what_an_unpacked_one_does() {
542        let data: Vec<u8> = (0..12u8).map(|i| i.wrapping_mul(23)).collect();
543        let eager = gray(&data);
544        let lazy = packed_gray(&data, 3, 4);
545        let mut a = Converted::new(Source::new(&eager, 3, 4), None);
546        let mut b = Converted::new(Source::new(&lazy, 3, 4), None);
547        for _ in 0..4 {
548            let want = a.next().expect("an eager row").pixels().to_vec();
549            let got = b.next().expect("a lazy row").pixels().to_vec();
550            assert_eq!(want, got);
551        }
552        assert!(a.next().is_none());
553        assert!(b.next().is_none());
554    }
555}