Skip to main content

pdfrum_page/image/
packed.rs

1//! Samples that are decoded but **not yet unpacked**, and the stage that
2//! unpacks them one row at a time.
3//!
4//! The image ladder's fourth rung produces bytes: whatever the filter chain
5//! left, in the depth the dictionary declares. Widening those to eight bits
6//! per component is a row-at-a-time pass: [`Unpacked`] walks the packed
7//! samples and yields one widened row per call.
8//!
9//! [`Packed`] is the same samples in the state the codec left them, plus the
10//! geometry needed to walk them: a [`Depth`], a component count, a pitch and
11//! the `/Decode` mapping already collapsed into one byte table per component.
12//! [`Unpacked`] is the row stage that reads it. Nothing is materialized until
13//! a caller asks for whole-image [`Pixels`], which
14//! [`crate::image::Samples::to_pixels`] is the one function that does.
15//!
16//! # Why the decode is a table
17//!
18//! `DecodeMap::apply` is a float multiply-add, a clamp, a round and a cast,
19//! and the mapping's answer depends only on the component index and the raw
20//! sample. At every depth the sample space is at most 65 536 wide, so the
21//! whole mapping tabulates. This is the same reasoning — and the same
22//! rounding — the codec path's own `decode_table` already applies on that
23//! path; here it also means the row stage's inner loop is a lookup rather
24//! than arithmetic.
25
26use crate::image::decode_array::DecodeMap;
27use crate::image::scanline;
28
29/// Bits per component, as the sample stream carries them.
30///
31/// The five values `/BitsPerComponent` is allowed to take. A type rather than
32/// a `u32` because the unpack loop dispatches on it once per image and then
33/// runs a loop that cannot be handed a depth the extractor does not know.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Depth {
36    /// One bit: eight samples to the byte, MSB first.
37    One,
38    /// Two bits.
39    Two,
40    /// Four bits: two samples to the byte.
41    Four,
42    /// Eight bits: one sample to the byte.
43    Eight,
44    /// Sixteen bits, big-endian.
45    Sixteen,
46}
47
48impl Depth {
49    /// The depth `bpc` names, or `None` when it is not one PDF allows.
50    #[must_use]
51    pub const fn new(bpc: u32) -> Option<Self> {
52        match bpc {
53            1 => Some(Self::One),
54            2 => Some(Self::Two),
55            4 => Some(Self::Four),
56            8 => Some(Self::Eight),
57            16 => Some(Self::Sixteen),
58            _ => None,
59        }
60    }
61
62    /// Bits per sample.
63    #[must_use]
64    pub const fn bits(self) -> u32 {
65        match self {
66            Self::One => 1,
67            Self::Two => 2,
68            Self::Four => 4,
69            Self::Eight => 8,
70            Self::Sixteen => 16,
71        }
72    }
73
74    /// How many distinct raw values this depth can express — the width of one
75    /// row of the decode table.
76    #[must_use]
77    pub const fn levels(self) -> usize {
78        1usize << self.bits()
79    }
80}
81
82/// Decoded-but-not-unpacked samples, with everything needed to walk them.
83///
84/// This is the state the filter chain leaves an image in for every path that
85/// does not go through a codec of its own: raw samples at [`Depth`], in the
86/// colour space's own component order, still packed. The `/Decode` array is
87/// already folded into a byte table, so unpacking a sample is a lookup.
88#[derive(Debug, Clone, PartialEq)]
89pub struct Packed {
90    /// The sample bytes, exactly as the filter chain left them.
91    data: Box<[u8]>,
92    /// Bits per component.
93    depth: Depth,
94    /// Components per pixel.
95    components: usize,
96    /// Bytes per source row.
97    pitch: usize,
98    /// Samples across.
99    width: usize,
100    /// Rows down.
101    height: u32,
102    /// The `/Decode` mapping, one row of `depth.levels()` bytes per component.
103    table: Box<[u8]>,
104}
105
106impl Packed {
107    /// Hold `data` as `width` x `height` samples of `components` at `depth`,
108    /// with the `/Decode` mapping for `space` folded into the table.
109    ///
110    /// `pitch` is the dictionary's own row stride, which for a packed depth is
111    /// wider than `width * components * depth / 8` rounded down: a row is
112    /// byte-aligned, so the tail bits of the last byte belong to no pixel.
113    #[must_use]
114    #[expect(
115        clippy::too_many_arguments,
116        reason = "packed samples are exactly this geometry, and naming each \
117                  piece is what keeps the row walk from re-deriving any of it"
118    )]
119    pub fn new(
120        data: Box<[u8]>,
121        depth: Depth,
122        components: usize,
123        pitch: usize,
124        width: u32,
125        height: u32,
126        space: &crate::color::ColorSpace,
127        decode: Option<&pdfrum_object::Array>,
128    ) -> Self {
129        Self::with_map(
130            data,
131            depth,
132            components,
133            pitch,
134            width,
135            height,
136            &DecodeMap::new(Some(space), components, depth.bits(), decode),
137        )
138    }
139
140    /// The same, from a mapping the image ladder has already built.
141    pub(crate) fn with_map(
142        data: Box<[u8]>,
143        depth: Depth,
144        components: usize,
145        pitch: usize,
146        width: u32,
147        height: u32,
148        decode: &DecodeMap,
149    ) -> Self {
150        let levels = depth.levels();
151        let mut table = vec![0u8; components.saturating_mul(levels)].into_boxed_slice();
152        for component in 0..components {
153            for raw in 0..levels {
154                #[expect(
155                    clippy::cast_precision_loss,
156                    reason = "a table index below 65 536 is exact in f32"
157                )]
158                let value = decode.apply(component, raw as f32);
159                // Rounded, not truncated — the same encode `decode_table`
160                // uses, and for the same reason: at 1, 2, 4 and 8 bits the two
161                // agree on every raw value, and at 16 they diverge on 32 648
162                // of the 65 536 samples with truncation a count low on each.
163                #[expect(
164                    clippy::cast_possible_truncation,
165                    clippy::cast_sign_loss,
166                    reason = "the clamp bounds the product to 0..=255"
167                )]
168                let byte = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
169                if let Some(slot) = table.get_mut(component * levels + raw) {
170                    *slot = byte;
171                }
172            }
173        }
174        Self {
175            data,
176            depth,
177            components,
178            pitch,
179            width: width as usize,
180            height,
181            table,
182        }
183    }
184
185    /// Components per pixel.
186    #[must_use]
187    pub const fn components(&self) -> usize {
188        self.components
189    }
190
191    /// Bytes held: the packed samples plus the decode table.
192    #[must_use]
193    pub fn byte_size(&self) -> usize {
194        self.data.len() + self.table.len()
195    }
196
197    /// Whether any row of the declared height is missing or short.
198    ///
199    /// The eager pass recorded a truncated-stream diagnostic
200    /// while it walked; a lazy one has to answer the same question without
201    /// walking, which the stream length does.
202    #[must_use]
203    pub fn truncated(&self) -> bool {
204        self.pitch
205            .checked_mul(self.height as usize)
206            .is_none_or(|want| self.data.len() < want)
207    }
208}
209
210/// The unpack stage: [`Packed`] samples widened to a byte per component, one
211/// row at a time into a buffer it owns and reuses.
212///
213/// The row this yields is exactly the row the eager pass wrote into its
214/// full-size buffer — same table, same rounding, same treatment of a short or
215/// absent row — so putting the stage in front of
216/// [`crate::image::Converted`] changes when the arithmetic runs and not what
217/// it produces.
218#[derive(Debug)]
219pub struct Unpacked<'a> {
220    packed: &'a Packed,
221    /// One row of `width * components` bytes, reused.
222    buf: Vec<u8>,
223    /// The current row's packed bytes, zero-padded to the pitch. Held rather
224    /// than allocated per row: the eager pass allocated one `Vec` per
225    /// scanline, which on a tall image is as many allocations as rows.
226    line: Vec<u8>,
227    y: u32,
228}
229
230impl<'a> Unpacked<'a> {
231    /// Start walking `packed` from its first row.
232    #[must_use]
233    pub fn new(packed: &'a Packed) -> Self {
234        Self {
235            buf: vec![0u8; packed.width.saturating_mul(packed.components)],
236            line: vec![0u8; packed.pitch],
237            packed,
238            y: 0,
239        }
240    }
241
242    /// The next row widened to a byte per component, or `None` past the
243    /// bottom.
244    ///
245    /// A row the stream never reached yields **zeroes**, not the byte
246    /// `/Decode` maps a zero sample to: PDFium returns a zeroed *output*
247    /// buffer for an absent row without ever running the decode, so the pixels
248    /// are literal black -- the distinction the scanline reader's
249    /// `Availability` carries.
250    pub fn next_row(&mut self) -> Option<&[u8]> {
251        if self.y >= self.packed.height {
252            return None;
253        }
254        let y = self.y as usize;
255        self.y += 1;
256        let p = self.packed;
257        let availability = scanline::scanline_into(&p.data, y, p.pitch, &mut self.line);
258        if availability == scanline::Availability::Absent {
259            self.buf.fill(0);
260            return Some(&self.buf);
261        }
262        let levels = p.depth.levels();
263        // The component index cycles `0, 1, .. components-1` across the row,
264        // so it is a counter that wraps, not `i % components` on every sample.
265        // On a one-bit image the division was the larger half of the loop.
266        let table = &*p.table;
267        let line = &*self.line;
268        match p.depth {
269            // The byte-aligned depths are a walk, not a bit extraction: the
270            // sample *is* the byte, so the row is a zip through the table.
271            Depth::Eight => {
272                let mut base = 0usize;
273                let mut component = 0usize;
274                for (slot, &raw) in self.buf.iter_mut().zip(line) {
275                    *slot = table.get(base + usize::from(raw)).copied().unwrap_or(0);
276                    component += 1;
277                    base += levels;
278                    if component == p.components {
279                        component = 0;
280                        base = 0;
281                    }
282                }
283                // A row the stream could only partly supply keeps what it has
284                // and zeroes the rest, which is the eager pass's own tail.
285                if let Some(tail) = self.buf.get_mut(line.len()..) {
286                    tail.fill(0);
287                }
288            }
289            // Sixteen bits is two bytes per sample, big-endian, and the table
290            // is indexed by the whole word.
291            Depth::Sixteen => {
292                let mut base = 0usize;
293                let mut component = 0usize;
294                for (i, slot) in self.buf.iter_mut().enumerate() {
295                    let hi = line.get(i * 2).copied().unwrap_or(0);
296                    let lo = line.get(i * 2 + 1).copied().unwrap_or(0);
297                    let raw = usize::from(hi) * 256 + usize::from(lo);
298                    *slot = table.get(base + raw).copied().unwrap_or(0);
299                    component += 1;
300                    base += levels;
301                    if component == p.components {
302                        component = 0;
303                        base = 0;
304                    }
305                }
306            }
307            // The sub-byte depths pack several samples into a byte, MSB
308            // first. Walking the byte and shifting down through it reads each
309            // byte once, where a `get_bits` per sample re-derived the byte
310            // index and the shift every time. On the one-bit images that is
311            // the whole of this stage's cost.
312            Depth::One | Depth::Two | Depth::Four => {
313                let bits = p.depth.bits();
314                let per_byte = (8 / bits) as usize;
315                let mask = u32::from(u8::MAX) >> (8 - bits);
316                let mut base = 0usize;
317                let mut component = 0usize;
318                for (chunk, byte_index) in self.buf.chunks_mut(per_byte).zip(0usize..) {
319                    let byte = u32::from(line.get(byte_index).copied().unwrap_or(0));
320                    // `k` is a position within one byte, so at most 7.
321                    for (slot, k) in chunk.iter_mut().zip(0u32..) {
322                        let shift = 8 - bits - k * bits;
323                        let raw = ((byte >> shift) & mask) as usize;
324                        *slot = table.get(base + raw).copied().unwrap_or(0);
325                        component += 1;
326                        base += levels;
327                        if component == p.components {
328                            component = 0;
329                            base = 0;
330                        }
331                    }
332                }
333            }
334        }
335        Some(&self.buf)
336    }
337
338    /// The whole image unpacked, for the callers that genuinely want it.
339    ///
340    /// The one place the full-size buffer this stage exists to avoid is built:
341    /// [`crate::image::Samples::to_pixels`], reached by the CLI's image
342    /// export, the facade's edit path and the stencil check.
343    #[must_use]
344    pub fn collect_all(mut self) -> Box<[u8]> {
345        let p = self.packed;
346        let stride = p.width.saturating_mul(p.components);
347        let mut out =
348            Vec::with_capacity(stride.saturating_mul(usize::try_from(p.height).unwrap_or(0)));
349        while let Some(row) = self.next_row() {
350            out.extend_from_slice(row);
351        }
352        out.into_boxed_slice()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::color::ColorSpace;
360
361    fn map(components: usize, bpc: u32) -> DecodeMap {
362        DecodeMap::new(Some(&ColorSpace::DeviceGray), components, bpc, None)
363    }
364
365    fn packed(data: &[u8], bpc: u32, components: usize, width: u32, height: u32) -> Packed {
366        let depth = Depth::new(bpc).expect("a real depth");
367        let pitch = (width as usize * components * bpc as usize).div_ceil(8);
368        Packed::with_map(
369            data.into(),
370            depth,
371            components,
372            pitch,
373            width,
374            height,
375            &map(components, bpc),
376        )
377    }
378
379    #[test]
380    fn eight_bit_samples_pass_through_unchanged() {
381        let p = packed(&[0, 128, 255, 7], 8, 1, 2, 2);
382        let mut u = Unpacked::new(&p);
383        assert_eq!(u.next_row(), Some(&[0, 128][..]));
384        assert_eq!(u.next_row(), Some(&[255, 7][..]));
385        assert_eq!(u.next_row(), None);
386    }
387
388    /// A one-bit sample widens to the two ends of the range, and the tail bits
389    /// of a byte-aligned row belong to no pixel.
390    #[test]
391    fn one_bit_samples_widen_to_black_and_white() {
392        // `0b1010_0000` across four declared pixels: set, clear, set, clear.
393        let p = packed(&[0b1010_0000], 1, 1, 4, 1);
394        let mut u = Unpacked::new(&p);
395        assert_eq!(u.next_row(), Some(&[255, 0, 255, 0][..]));
396    }
397
398    #[test]
399    fn four_bit_samples_span_the_range() {
400        let p = packed(&[0x0F, 0x80], 4, 1, 4, 1);
401        let mut u = Unpacked::new(&p);
402        // 0 -> 0, 15 -> 255, 8 -> 136, 0 -> 0.
403        assert_eq!(u.next_row(), Some(&[0, 255, 136, 0][..]));
404    }
405
406    /// Sixteen bits keeps the high byte, which is what the decode table's
407    /// rounding over 65 536 levels comes to for the identity mapping.
408    #[test]
409    fn sixteen_bit_samples_keep_their_high_byte() {
410        let p = packed(&[0xFF, 0xFF, 0x00, 0x00], 16, 1, 2, 1);
411        let mut u = Unpacked::new(&p);
412        assert_eq!(u.next_row(), Some(&[255, 0][..]));
413    }
414
415    /// A row that begins past the end of the stream is zeroes, not whatever
416    /// the decode maps a zero sample to. A row that begins inside it keeps the
417    /// samples it has and zero-pads the rest, which then *does* go through the
418    /// decode.
419    #[test]
420    fn an_absent_row_is_zero_and_a_short_one_is_padded() {
421        // Five bytes of a 2x4 image: rows 0 and 1 whole, row 2 half, row 3
422        // absent.
423        let p = packed(&[1, 2, 3, 4, 5], 8, 1, 2, 4);
424        let mut u = Unpacked::new(&p);
425        assert_eq!(u.next_row(), Some(&[1, 2][..]));
426        assert_eq!(u.next_row(), Some(&[3, 4][..]));
427        assert_eq!(u.next_row(), Some(&[5, 0][..]));
428        assert_eq!(u.next_row(), Some(&[0, 0][..]));
429        assert_eq!(u.next_row(), None);
430        assert!(p.truncated());
431    }
432
433    /// A `/Decode [1 0]` inversion reaches the row through the table.
434    #[test]
435    fn a_decode_inversion_is_in_the_table() {
436        let decode = pdfrum_object::Array::of([
437            pdfrum_object::Object::Real(1.0),
438            pdfrum_object::Object::Real(0.0),
439        ]);
440        let p = Packed::new(
441            vec![0, 255].into(),
442            Depth::Eight,
443            1,
444            2,
445            2,
446            1,
447            &ColorSpace::DeviceGray,
448            Some(&decode),
449        );
450        let mut u = Unpacked::new(&p);
451        assert_eq!(u.next_row(), Some(&[255, 0][..]));
452    }
453
454    #[test]
455    fn collecting_every_row_is_the_whole_image() {
456        let p = packed(&[1, 2, 3, 4], 8, 1, 2, 2);
457        assert_eq!(&*Unpacked::new(&p).collect_all(), &[1, 2, 3, 4]);
458    }
459}