Skip to main content

rpdfium_codec/
scanline.rs

1// Derived from PDFium's scanline decoder concepts
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Scanline-based image decoding — yields one row of pixels at a time.
7//!
8//! The [`ScanlineDecoder`] trait provides a row-by-row interface for
9//! decompressing image data, useful for large images where loading the
10//! entire bitmap into memory at once is undesirable.
11//!
12//! The trait mirrors PDFium's `ScanlineDecoder` base class, including
13//! `GetWidth()`, `GetHeight()`, `CountComps()`, and `GetBPC()` accessors
14//! so that any code holding only a `&dyn ScanlineDecoder` can query the
15//! full image geometry without consulting the PDF stream dictionary.
16
17use crate::error::DecodeError;
18
19/// Compute the byte stride (bytes per scanline row).
20///
21/// Matches PDFium's pitch formula: `(width * comps * bpc + 7) / 8`.
22fn compute_row_stride(width: u32, comps: u8, bpc: u8) -> usize {
23    (width as usize * comps as usize * bpc as usize).div_ceil(8)
24}
25
26/// A scanline-based image decoder that yields one row of pixels at a time.
27///
28/// After construction, call [`decode_scanline`](ScanlineDecoder::decode_scanline)
29/// repeatedly to get each row. Returns `Ok(None)` after the last row.
30/// Call [`reset`](ScanlineDecoder::reset) to restart from the first row.
31///
32/// Mirrors PDFium's `ScanlineDecoder` base class interface, including the
33/// image-geometry accessors `GetWidth()`, `GetHeight()`, `CountComps()`,
34/// and `GetBPC()`.
35pub trait ScanlineDecoder {
36    /// Image width in pixels. Mirrors PDFium's `GetWidth()`.
37    fn width(&self) -> u32;
38
39    /// Upstream alias for [`width()`](ScanlineDecoder::width). Mirrors PDFium's `GetWidth()`.
40    #[inline]
41    fn get_width(&self) -> u32 {
42        self.width()
43    }
44
45    /// Image height in pixels. Mirrors PDFium's `GetHeight()`.
46    fn height(&self) -> u32;
47
48    /// Upstream alias for [`height()`](ScanlineDecoder::height). Mirrors PDFium's `GetHeight()`.
49    #[inline]
50    fn get_height(&self) -> u32 {
51        self.height()
52    }
53
54    /// Number of color components per pixel (e.g. 1 for gray, 3 for RGB, 4
55    /// for CMYK). Mirrors PDFium's `CountComps()`.
56    fn comps_count(&self) -> u8;
57
58    /// Upstream alias for [`comps_count()`](ScanlineDecoder::comps_count). Mirrors PDFium's `CountComps()`.
59    #[inline]
60    fn count_comps(&self) -> u8 {
61        self.comps_count()
62    }
63
64    /// Bits per color component (e.g. 1, 8, 16). Mirrors PDFium's `GetBPC()`.
65    fn bpc(&self) -> u8;
66
67    /// Upstream alias for [`bpc()`](ScanlineDecoder::bpc). Mirrors PDFium's `GetBPC()`.
68    #[inline]
69    fn get_bpc(&self) -> u8 {
70        self.bpc()
71    }
72
73    /// Number of bytes per row: `(width * comps_count * bpc + 7) / 8`.
74    fn row_stride(&self) -> usize;
75
76    /// Decode and return the next scanline.
77    ///
78    /// Returns `Ok(Some(data))` for each row, `Ok(None)` when all rows are
79    /// done.
80    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError>;
81
82    /// Reset the decoder to the first scanline.
83    fn reset(&mut self) -> Result<(), DecodeError>;
84
85    /// Current line index (0-based), or `None` if no lines have been read yet.
86    fn current_line(&self) -> Option<usize>;
87
88    /// Skip ahead to scanline `target` by decoding and discarding intermediate lines.
89    ///
90    /// After this call, `current_line()` will be `>= Some(target)` if the decoder
91    /// has at least `target + 1` scanlines available.
92    ///
93    /// The upstream `SkipToScanline()` accepted a `PauseIndicatorIface` pause parameter;
94    /// rpdfium omits pause/resume since the library is synchronous.
95    fn skip_to_scanline(&mut self, target: usize) {
96        loop {
97            match self.current_line() {
98                Some(n) if n >= target => break,
99                _ => {}
100            }
101            if let Ok(None) | Err(_) = self.decode_scanline() {
102                break;
103            }
104        }
105    }
106
107    /// Returns the byte offset of the current source position in the compressed stream, if available.
108    ///
109    /// Upstream `GetSrcOffset()` is used for progressive decode bookkeeping not needed in rpdfium's
110    /// synchronous model.
111    fn src_offset(&self) -> Option<usize> {
112        None
113    }
114
115    /// Upstream alias for [`src_offset()`](ScanlineDecoder::src_offset). Mirrors PDFium's `GetSrcOffset()`.
116    #[inline]
117    fn get_src_offset(&self) -> Option<usize> {
118        self.src_offset()
119    }
120}
121
122/// Random-access scanline decoder wrapper.
123///
124/// Wraps any [`ScanlineDecoder`] and adds `get_scanline(line)` support with
125/// caching and rewind logic. Ported from upstream PDFium's
126/// `ScanlineDecoder::GetScanline(int line)`.
127pub struct RandomAccessDecoder<D: ScanlineDecoder> {
128    inner: D,
129    next_line: i32,
130    last_scanline: Vec<u8>,
131}
132
133impl<D: ScanlineDecoder> RandomAccessDecoder<D> {
134    /// Wrap a scanline decoder with random access capability.
135    pub fn new(inner: D) -> Self {
136        Self {
137            inner,
138            next_line: -1,
139            last_scanline: Vec::new(),
140        }
141    }
142
143    /// Skip to scanline `target` by warming the cache via `scanline()`.
144    ///
145    /// Overrides the sequential decode-and-discard loop from `ScanlineDecoder`
146    /// with the random-access cache path.  Mirrors PDFium's
147    /// `ScanlineDecoder::SkipToScanline()`.
148    ///
149    /// This is a Tier-1 primary: the body calls another rpdfium function
150    /// (`scanline()`) and discards its `Result` return value, so it is not a
151    /// pure Tier-2 delegation and must not carry `#[inline]`.
152    pub fn skip_to_scanline(&mut self, target: usize) {
153        let _ = self.scanline(target);
154    }
155
156    /// Get a specific scanline by line index (0-based).
157    ///
158    /// If the requested line is the one just decoded, returns a cached copy.
159    /// If the requested line is before the current position, rewinds and
160    /// re-reads sequentially. Matches upstream PDFium's caching pattern.
161    ///
162    /// Primary name; [`get_scanline`](RandomAccessDecoder::get_scanline) is the
163    /// upstream-style alias. Mirrors PDFium's `ScanlineDecoder::GetScanline()`.
164    pub fn scanline(&mut self, line: usize) -> Result<Option<&[u8]>, DecodeError> {
165        let line_i32 = line as i32;
166
167        // Quick return if we just decoded this line
168        if self.next_line == line_i32 + 1 && !self.last_scanline.is_empty() {
169            return Ok(Some(&self.last_scanline));
170        }
171
172        // Need to rewind if we're past the target or haven't started
173        if self.next_line < 0 || self.next_line > line_i32 {
174            self.inner.reset()?;
175            self.next_line = 0;
176        }
177
178        // Skip forward to target line
179        while self.next_line < line_i32 {
180            if self.inner.decode_scanline()?.is_none() {
181                return Ok(None);
182            }
183            self.next_line += 1;
184        }
185
186        // Fetch the target line
187        match self.inner.decode_scanline()? {
188            Some(data) => {
189                self.last_scanline.clear();
190                self.last_scanline.extend_from_slice(data);
191                self.next_line += 1;
192                Ok(Some(&self.last_scanline))
193            }
194            None => Ok(None),
195        }
196    }
197
198    /// Upstream alias for [`scanline()`](RandomAccessDecoder::scanline). Mirrors PDFium's `GetScanline()`.
199    #[inline]
200    pub fn get_scanline(&mut self, line: usize) -> Result<Option<&[u8]>, DecodeError> {
201        self.scanline(line)
202    }
203
204    /// Get a reference to the underlying decoder.
205    pub fn inner(&self) -> &D {
206        &self.inner
207    }
208
209    /// Image width in pixels.
210    pub fn width(&self) -> u32 {
211        self.inner.width()
212    }
213
214    /// Upstream alias for [`width()`](RandomAccessDecoder::width). Mirrors PDFium's `GetWidth()`.
215    #[inline]
216    pub fn get_width(&self) -> u32 {
217        self.width()
218    }
219
220    /// Image height in pixels.
221    pub fn height(&self) -> u32 {
222        self.inner.height()
223    }
224
225    /// Upstream alias for [`height()`](RandomAccessDecoder::height). Mirrors PDFium's `GetHeight()`.
226    #[inline]
227    pub fn get_height(&self) -> u32 {
228        self.height()
229    }
230
231    /// Number of color components per pixel. Mirrors PDFium's `CountComps()`.
232    pub fn comps_count(&self) -> u8 {
233        self.inner.comps_count()
234    }
235
236    /// Upstream alias for [`comps_count()`](RandomAccessDecoder::comps_count). Mirrors PDFium's `CountComps()`.
237    #[inline]
238    pub fn count_comps(&self) -> u8 {
239        self.comps_count()
240    }
241
242    /// Bits per color component.
243    pub fn bpc(&self) -> u8 {
244        self.inner.bpc()
245    }
246
247    /// Upstream alias for [`bpc()`](RandomAccessDecoder::bpc). Mirrors PDFium's `GetBPC()`.
248    #[inline]
249    pub fn get_bpc(&self) -> u8 {
250        self.bpc()
251    }
252
253    /// Number of bytes per row.
254    pub fn row_stride(&self) -> usize {
255        self.inner.row_stride()
256    }
257}
258
259/// Scanline decoder for FlateDecode (zlib/deflate) data.
260///
261/// Decompresses the full data internally on construction, then yields
262/// `row_stride` bytes per [`decode_scanline`](ScanlineDecoder::decode_scanline) call.
263pub struct FlateScanlineDecoder {
264    data: Vec<u8>,
265    width: u32,
266    height: u32,
267    comps: u8,
268    bpc: u8,
269    row_stride: usize,
270    offset: usize,
271}
272
273impl FlateScanlineDecoder {
274    /// Create a new scanline decoder from compressed Flate data.
275    ///
276    /// The row stride is computed automatically as
277    /// `(width * comps * bpc + 7) / 8`, matching PDFium's pitch formula.
278    pub fn new(
279        compressed: &[u8],
280        width: u32,
281        height: u32,
282        comps: u8,
283        bpc: u8,
284    ) -> Result<Self, DecodeError> {
285        let data = crate::flate::decode(compressed, None, None, None, None)?;
286        let row_stride = compute_row_stride(width, comps, bpc);
287        Ok(Self {
288            data,
289            width,
290            height,
291            comps,
292            bpc,
293            row_stride,
294            offset: 0,
295        })
296    }
297
298    /// Create from already-decompressed pixel data.
299    pub fn from_decoded(data: Vec<u8>, width: u32, height: u32, comps: u8, bpc: u8) -> Self {
300        let row_stride = compute_row_stride(width, comps, bpc);
301        Self {
302            data,
303            width,
304            height,
305            comps,
306            bpc,
307            row_stride,
308            offset: 0,
309        }
310    }
311}
312
313impl ScanlineDecoder for FlateScanlineDecoder {
314    fn width(&self) -> u32 {
315        self.width
316    }
317
318    fn height(&self) -> u32 {
319        self.height
320    }
321
322    fn comps_count(&self) -> u8 {
323        self.comps
324    }
325
326    fn bpc(&self) -> u8 {
327        self.bpc
328    }
329
330    fn row_stride(&self) -> usize {
331        self.row_stride
332    }
333
334    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError> {
335        if self.row_stride == 0 || self.offset >= self.data.len() {
336            return Ok(None);
337        }
338        let end = (self.offset + self.row_stride).min(self.data.len());
339        let row = &self.data[self.offset..end];
340        self.offset = end;
341        Ok(Some(row))
342    }
343
344    fn reset(&mut self) -> Result<(), DecodeError> {
345        self.offset = 0;
346        Ok(())
347    }
348
349    fn current_line(&self) -> Option<usize> {
350        if self.row_stride == 0 || self.offset == 0 {
351            None
352        } else {
353            Some(self.offset / self.row_stride - 1)
354        }
355    }
356}
357
358/// Scanline decoder for DCTDecode (JPEG) data.
359///
360/// Decodes the full JPEG internally on construction, then yields
361/// one scanline at a time based on the decoded pixel stride.
362pub struct DctScanlineDecoder {
363    data: Vec<u8>,
364    width: u32,
365    height: u32,
366    comps: u8,
367    bpc: u8,
368    row_stride: usize,
369    offset: usize,
370}
371
372impl DctScanlineDecoder {
373    /// Create a new scanline decoder from JPEG-compressed data.
374    ///
375    /// The row stride is computed automatically as
376    /// `(width * comps * bpc + 7) / 8`, matching PDFium's pitch formula.
377    pub fn new(
378        jpeg_data: &[u8],
379        width: u32,
380        height: u32,
381        comps: u8,
382        bpc: u8,
383    ) -> Result<Self, DecodeError> {
384        let data = crate::jpeg::decode(jpeg_data)?;
385        let row_stride = compute_row_stride(width, comps, bpc);
386        Ok(Self {
387            data,
388            width,
389            height,
390            comps,
391            bpc,
392            row_stride,
393            offset: 0,
394        })
395    }
396
397    /// Create from already-decoded pixel data.
398    pub fn from_decoded(data: Vec<u8>, width: u32, height: u32, comps: u8, bpc: u8) -> Self {
399        let row_stride = compute_row_stride(width, comps, bpc);
400        Self {
401            data,
402            width,
403            height,
404            comps,
405            bpc,
406            row_stride,
407            offset: 0,
408        }
409    }
410}
411
412impl ScanlineDecoder for DctScanlineDecoder {
413    fn width(&self) -> u32 {
414        self.width
415    }
416
417    fn height(&self) -> u32 {
418        self.height
419    }
420
421    fn comps_count(&self) -> u8 {
422        self.comps
423    }
424
425    fn bpc(&self) -> u8 {
426        self.bpc
427    }
428
429    fn row_stride(&self) -> usize {
430        self.row_stride
431    }
432
433    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError> {
434        if self.row_stride == 0 || self.offset >= self.data.len() {
435            return Ok(None);
436        }
437        let end = (self.offset + self.row_stride).min(self.data.len());
438        let row = &self.data[self.offset..end];
439        self.offset = end;
440        Ok(Some(row))
441    }
442
443    fn reset(&mut self) -> Result<(), DecodeError> {
444        self.offset = 0;
445        Ok(())
446    }
447
448    fn current_line(&self) -> Option<usize> {
449        if self.row_stride == 0 || self.offset == 0 {
450            None
451        } else {
452            Some(self.offset / self.row_stride - 1)
453        }
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use flate2::Compression;
461    use flate2::write::ZlibEncoder;
462    use std::io::Write;
463
464    fn zlib_compress(data: &[u8]) -> Vec<u8> {
465        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
466        encoder.write_all(data).unwrap();
467        encoder.finish().unwrap()
468    }
469
470    #[test]
471    fn test_flate_scanline_basic() {
472        // 3 rows × 4 bytes (width=4, comps=1, bpc=8)
473        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
474        let compressed = zlib_compress(&raw);
475        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 3, 1, 8).unwrap();
476
477        assert_eq!(decoder.row_stride(), 4);
478        assert_eq!(decoder.width(), 4);
479        assert_eq!(decoder.height(), 3);
480        assert_eq!(decoder.comps_count(), 1);
481        assert_eq!(decoder.bpc(), 8);
482
483        let row1 = decoder.decode_scanline().unwrap().unwrap();
484        assert_eq!(row1, &[1, 2, 3, 4]);
485
486        let row2 = decoder.decode_scanline().unwrap().unwrap();
487        assert_eq!(row2, &[5, 6, 7, 8]);
488
489        let row3 = decoder.decode_scanline().unwrap().unwrap();
490        assert_eq!(row3, &[9, 10, 11, 12]);
491
492        // No more rows
493        assert!(decoder.decode_scanline().unwrap().is_none());
494    }
495
496    #[test]
497    fn test_flate_scanline_count() {
498        // 10 rows × 10 bytes (width=10, comps=1, bpc=8)
499        let raw = vec![0u8; 100];
500        let compressed = zlib_compress(&raw);
501        let mut decoder = FlateScanlineDecoder::new(&compressed, 10, 10, 1, 8).unwrap();
502
503        let mut count = 0;
504        while decoder.decode_scanline().unwrap().is_some() {
505            count += 1;
506        }
507        assert_eq!(count, 10);
508    }
509
510    #[test]
511    fn test_flate_scanline_matches_batch() {
512        // 12 rows × 10 bytes (width=10, comps=1, bpc=8)
513        let raw: Vec<u8> = (0..120).map(|i| (i % 256) as u8).collect();
514        let compressed = zlib_compress(&raw);
515        let mut decoder = FlateScanlineDecoder::new(&compressed, 10, 12, 1, 8).unwrap();
516
517        let mut collected = Vec::new();
518        while let Some(row) = decoder.decode_scanline().unwrap() {
519            collected.extend_from_slice(row);
520        }
521        assert_eq!(collected, raw);
522    }
523
524    #[test]
525    fn test_flate_scanline_none_after_last() {
526        // 1 row × 4 bytes (width=4, comps=1, bpc=8)
527        let raw = vec![1u8; 4];
528        let compressed = zlib_compress(&raw);
529        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 1, 1, 8).unwrap();
530
531        assert!(decoder.decode_scanline().unwrap().is_some());
532        assert!(decoder.decode_scanline().unwrap().is_none());
533        assert!(decoder.decode_scanline().unwrap().is_none());
534    }
535
536    #[test]
537    fn test_flate_scanline_reset() {
538        // 2 rows × 4 bytes (width=4, comps=1, bpc=8)
539        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8];
540        let compressed = zlib_compress(&raw);
541        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 2, 1, 8).unwrap();
542
543        let row1 = decoder.decode_scanline().unwrap().unwrap().to_vec();
544        decoder.decode_scanline().unwrap(); // consume row2
545        assert!(decoder.decode_scanline().unwrap().is_none());
546
547        decoder.reset().unwrap();
548        let row1_again = decoder.decode_scanline().unwrap().unwrap();
549        assert_eq!(row1, row1_again);
550    }
551
552    #[test]
553    fn test_flate_scanline_row_stride_calc() {
554        // 640×480 RGB image: row_stride = (640 * 3 * 8 + 7) / 8 = 1920
555        let raw = vec![0u8; 1920 * 2];
556        let compressed = zlib_compress(&raw);
557        let decoder = FlateScanlineDecoder::new(&compressed, 640, 2, 3, 8).unwrap();
558        assert_eq!(decoder.row_stride(), 1920);
559        assert_eq!(decoder.width(), 640);
560        assert_eq!(decoder.comps_count(), 3);
561    }
562
563    #[test]
564    fn test_flate_scanline_zero_stride() {
565        // width=0 produces row_stride=0; decode_scanline must return None immediately
566        let decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3], 0, 1, 1, 8);
567        let mut decoder: Box<dyn ScanlineDecoder> = Box::new(decoder);
568        assert!(decoder.decode_scanline().unwrap().is_none());
569    }
570
571    #[test]
572    fn test_dct_scanline_from_decoded() {
573        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
574        let raw = vec![10, 20, 30, 40, 50, 60];
575        let mut decoder = DctScanlineDecoder::from_decoded(raw.clone(), 1, 2, 3, 8);
576
577        assert_eq!(decoder.width(), 1);
578        assert_eq!(decoder.height(), 2);
579        assert_eq!(decoder.comps_count(), 3);
580        assert_eq!(decoder.bpc(), 8);
581        assert_eq!(decoder.row_stride(), 3);
582
583        let row1 = decoder.decode_scanline().unwrap().unwrap();
584        assert_eq!(row1, &[10, 20, 30]);
585
586        let row2 = decoder.decode_scanline().unwrap().unwrap();
587        assert_eq!(row2, &[40, 50, 60]);
588
589        assert!(decoder.decode_scanline().unwrap().is_none());
590    }
591
592    #[test]
593    fn test_dct_scanline_reset() {
594        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
595        let raw = vec![1, 2, 3, 4, 5, 6];
596        let mut decoder = DctScanlineDecoder::from_decoded(raw, 1, 2, 3, 8);
597
598        decoder.decode_scanline().unwrap();
599        decoder.decode_scanline().unwrap();
600        assert!(decoder.decode_scanline().unwrap().is_none());
601
602        decoder.reset().unwrap();
603        let first = decoder.decode_scanline().unwrap().unwrap();
604        assert_eq!(first, &[1, 2, 3]);
605    }
606
607    #[test]
608    fn test_scanline_decoder_trait_object_safety() {
609        fn assert_obj_safe(_: &dyn ScanlineDecoder) {}
610        // width=2, height=1, comps=1, bpc=8 → row_stride=2
611        let decoder = FlateScanlineDecoder::from_decoded(vec![0; 4], 2, 1, 1, 8);
612        assert_obj_safe(&decoder);
613    }
614
615    #[test]
616    fn test_partial_last_row() {
617        // Data that doesn't fill the last row completely.
618        // width=3, height=2, comps=1, bpc=8 → row_stride=3, total expected=6 but data=5
619        let raw = vec![1, 2, 3, 4, 5];
620        let mut decoder = FlateScanlineDecoder::from_decoded(raw, 3, 2, 1, 8);
621
622        let row1 = decoder.decode_scanline().unwrap().unwrap();
623        assert_eq!(row1, &[1, 2, 3]);
624
625        let row2 = decoder.decode_scanline().unwrap().unwrap();
626        assert_eq!(row2, &[4, 5]); // partial last row
627
628        assert!(decoder.decode_scanline().unwrap().is_none());
629    }
630
631    #[test]
632    fn test_metadata_getters_flate() {
633        // Verify that width/height/count_comps/bpc survive a round-trip
634        let raw = vec![0u8; 8 * 6 * 2]; // 8px wide, 6 rows, 16bpc grayscale
635        let compressed = zlib_compress(&raw);
636        // row_stride = (8 * 1 * 16 + 7) / 8 = 16
637        let decoder = FlateScanlineDecoder::new(&compressed, 8, 6, 1, 16).unwrap();
638        assert_eq!(decoder.width(), 8);
639        assert_eq!(decoder.height(), 6);
640        assert_eq!(decoder.comps_count(), 1);
641        assert_eq!(decoder.bpc(), 16);
642        assert_eq!(decoder.row_stride(), 16);
643    }
644
645    #[test]
646    fn test_metadata_getters_dct() {
647        // Verify that metadata is stored correctly in DctScanlineDecoder
648        let data = vec![0u8; 10 * 4]; // 10px wide, 4 rows, 1 comp, 8bpc
649        let decoder = DctScanlineDecoder::from_decoded(data, 10, 4, 1, 8);
650        assert_eq!(decoder.width(), 10);
651        assert_eq!(decoder.height(), 4);
652        assert_eq!(decoder.comps_count(), 1);
653        assert_eq!(decoder.bpc(), 8);
654        assert_eq!(decoder.row_stride(), 10);
655    }
656
657    #[test]
658    fn test_compute_row_stride_1bpc() {
659        // 1bpc packed: 8 pixels → 1 byte, 9 pixels → 2 bytes
660        let d8 = FlateScanlineDecoder::from_decoded(vec![0; 1], 8, 1, 1, 1);
661        assert_eq!(d8.row_stride(), 1);
662        let d9 = FlateScanlineDecoder::from_decoded(vec![0; 2], 9, 1, 1, 1);
663        assert_eq!(d9.row_stride(), 2);
664    }
665
666    #[test]
667    fn test_compute_row_stride_16bpc() {
668        // 16bpc RGB: 4 pixels → 4 * 3 * 2 = 24 bytes
669        let decoder = FlateScanlineDecoder::from_decoded(vec![0; 24], 4, 1, 3, 16);
670        assert_eq!(decoder.row_stride(), 24);
671    }
672
673    // -----------------------------------------------------------------------
674    // RandomAccessDecoder tests
675    // -----------------------------------------------------------------------
676
677    #[test]
678    fn test_random_access_sequential() {
679        // 3 rows × 4 bytes (width=4, comps=1, bpc=8)
680        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
681        let decoder = FlateScanlineDecoder::from_decoded(raw, 4, 3, 1, 8);
682        let mut ra = RandomAccessDecoder::new(decoder);
683
684        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
685        assert_eq!(row0, &[1, 2, 3, 4]);
686
687        let row1 = ra.get_scanline(1).unwrap().unwrap().to_vec();
688        assert_eq!(row1, &[5, 6, 7, 8]);
689
690        let row2 = ra.get_scanline(2).unwrap().unwrap().to_vec();
691        assert_eq!(row2, &[9, 10, 11, 12]);
692    }
693
694    #[test]
695    fn test_random_access_cached() {
696        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
697        let raw = vec![10, 20, 30, 40, 50, 60];
698        let decoder = FlateScanlineDecoder::from_decoded(raw, 1, 2, 3, 8);
699        let mut ra = RandomAccessDecoder::new(decoder);
700
701        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
702        assert_eq!(row0, &[10, 20, 30]);
703
704        // Read same line again — should return cached copy
705        let row0_again = ra.get_scanline(0).unwrap().unwrap().to_vec();
706        assert_eq!(row0_again, &[10, 20, 30]);
707    }
708
709    #[test]
710    fn test_random_access_rewind() {
711        // 3 rows × 3 bytes (width=3, comps=1, bpc=8)
712        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
713        let decoder = FlateScanlineDecoder::from_decoded(raw, 3, 3, 1, 8);
714        let mut ra = RandomAccessDecoder::new(decoder);
715
716        // Read line 2 (skip 0, 1)
717        let row2 = ra.get_scanline(2).unwrap().unwrap().to_vec();
718        assert_eq!(row2, &[7, 8, 9]);
719
720        // Read line 0 — requires rewind
721        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
722        assert_eq!(row0, &[1, 2, 3]);
723
724        let row1 = ra.get_scanline(1).unwrap().unwrap().to_vec();
725        assert_eq!(row1, &[4, 5, 6]);
726    }
727
728    #[test]
729    fn test_random_access_skip_forward() {
730        // 4 rows × 10 bytes (width=10, comps=1, bpc=8)
731        let raw: Vec<u8> = (0..40).collect();
732        let decoder = FlateScanlineDecoder::from_decoded(raw, 10, 4, 1, 8);
733        let mut ra = RandomAccessDecoder::new(decoder);
734
735        // Jump to line 3 directly
736        let row3 = ra.get_scanline(3).unwrap().unwrap().to_vec();
737        assert_eq!(row3, &[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]);
738    }
739
740    #[test]
741    fn test_random_access_out_of_bounds() {
742        // 1 row × 3 bytes (width=3, comps=1, bpc=8)
743        let raw = vec![1, 2, 3];
744        let decoder = FlateScanlineDecoder::from_decoded(raw, 3, 1, 1, 8);
745        let mut ra = RandomAccessDecoder::new(decoder);
746
747        // Only 1 row exists; line 1 should return None
748        let result = ra.get_scanline(1).unwrap();
749        assert!(result.is_none());
750    }
751
752    #[test]
753    fn test_random_access_inner_ref() {
754        // 1 row × 2 bytes (width=2, comps=1, bpc=8)
755        let raw = vec![1, 2, 3, 4];
756        let decoder = FlateScanlineDecoder::from_decoded(raw, 2, 1, 1, 8);
757        let ra = RandomAccessDecoder::new(decoder);
758        assert_eq!(ra.inner().row_stride(), 2);
759        assert_eq!(ra.row_stride(), 2);
760        assert_eq!(ra.width(), 2);
761        assert_eq!(ra.height(), 1);
762        assert_eq!(ra.comps_count(), 1);
763        assert_eq!(ra.bpc(), 8);
764    }
765
766    #[test]
767    fn test_current_line_before_read() {
768        // width=2, height=2, comps=1, bpc=8 → row_stride=2
769        let decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
770        assert_eq!(decoder.current_line(), None);
771    }
772
773    #[test]
774    fn test_current_line_after_read() {
775        let mut decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
776        decoder.decode_scanline().unwrap();
777        assert_eq!(decoder.current_line(), Some(0));
778        decoder.decode_scanline().unwrap();
779        assert_eq!(decoder.current_line(), Some(1));
780    }
781
782    #[test]
783    fn test_current_line_after_reset() {
784        let mut decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
785        decoder.decode_scanline().unwrap();
786        decoder.reset().unwrap();
787        assert_eq!(decoder.current_line(), None);
788    }
789
790    // -----------------------------------------------------------------------
791    // skip_to_scanline / get_src_offset tests (Rec 2 + Rec 3)
792    // -----------------------------------------------------------------------
793
794    #[test]
795    fn test_skip_to_scanline_advances_position() {
796        // 6 rows × 2 bytes (width=2, comps=1, bpc=8)
797        let raw: Vec<u8> = (0..12).collect();
798        let compressed = zlib_compress(&raw);
799        let mut decoder = FlateScanlineDecoder::new(&compressed, 2, 6, 1, 8).unwrap();
800
801        // Before any read, current_line() is None; skip to line 3
802        decoder.skip_to_scanline(3);
803
804        // After skipping, current_line() must be >= Some(3)
805        assert!(
806            decoder.current_line().map(|l| l >= 3).unwrap_or(false),
807            "expected current_line() >= 3 after skip_to_scanline(3), got {:?}",
808            decoder.current_line()
809        );
810    }
811
812    #[test]
813    fn test_get_src_offset_returns_none() {
814        // Default implementation of get_src_offset() returns None.
815        let decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
816        // Call via trait object to exercise the default trait method
817        let trait_obj: &dyn ScanlineDecoder = &decoder;
818        assert_eq!(trait_obj.get_src_offset(), None);
819    }
820}