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}