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    /// Image height in pixels. Mirrors PDFium's `GetHeight()`.
40    fn height(&self) -> u32;
41
42    /// Number of color components per pixel (e.g. 1 for gray, 3 for RGB, 4
43    /// for CMYK). Mirrors PDFium's `CountComps()`.
44    fn count_comps(&self) -> u8;
45
46    /// Bits per color component (e.g. 1, 8, 16). Mirrors PDFium's `GetBPC()`.
47    fn bpc(&self) -> u8;
48
49    /// Number of bytes per row: `(width * count_comps * bpc + 7) / 8`.
50    fn row_stride(&self) -> usize;
51
52    /// Decode and return the next scanline.
53    ///
54    /// Returns `Ok(Some(data))` for each row, `Ok(None)` when all rows are
55    /// done.
56    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError>;
57
58    /// Reset the decoder to the first scanline.
59    fn reset(&mut self) -> Result<(), DecodeError>;
60
61    /// Current line index (0-based), or `None` if no lines have been read yet.
62    fn current_line(&self) -> Option<usize>;
63}
64
65/// Random-access scanline decoder wrapper.
66///
67/// Wraps any [`ScanlineDecoder`] and adds `get_scanline(line)` support with
68/// caching and rewind logic. Ported from upstream PDFium's
69/// `ScanlineDecoder::GetScanline(int line)`.
70pub struct RandomAccessDecoder<D: ScanlineDecoder> {
71    inner: D,
72    next_line: i32,
73    last_scanline: Vec<u8>,
74}
75
76impl<D: ScanlineDecoder> RandomAccessDecoder<D> {
77    /// Wrap a scanline decoder with random access capability.
78    pub fn new(inner: D) -> Self {
79        Self {
80            inner,
81            next_line: -1,
82            last_scanline: Vec::new(),
83        }
84    }
85
86    /// Get a specific scanline by line index (0-based).
87    ///
88    /// If the requested line is the one just decoded, returns a cached copy.
89    /// If the requested line is before the current position, rewinds and
90    /// re-reads sequentially. Matches upstream PDFium's caching pattern.
91    pub fn get_scanline(&mut self, line: usize) -> Result<Option<&[u8]>, DecodeError> {
92        let line_i32 = line as i32;
93
94        // Quick return if we just decoded this line
95        if self.next_line == line_i32 + 1 && !self.last_scanline.is_empty() {
96            return Ok(Some(&self.last_scanline));
97        }
98
99        // Need to rewind if we're past the target or haven't started
100        if self.next_line < 0 || self.next_line > line_i32 {
101            self.inner.reset()?;
102            self.next_line = 0;
103        }
104
105        // Skip forward to target line
106        while self.next_line < line_i32 {
107            if self.inner.decode_scanline()?.is_none() {
108                return Ok(None);
109            }
110            self.next_line += 1;
111        }
112
113        // Fetch the target line
114        match self.inner.decode_scanline()? {
115            Some(data) => {
116                self.last_scanline.clear();
117                self.last_scanline.extend_from_slice(data);
118                self.next_line += 1;
119                Ok(Some(&self.last_scanline))
120            }
121            None => Ok(None),
122        }
123    }
124
125    /// Get a reference to the underlying decoder.
126    pub fn inner(&self) -> &D {
127        &self.inner
128    }
129
130    /// Image width in pixels.
131    pub fn width(&self) -> u32 {
132        self.inner.width()
133    }
134
135    /// Image height in pixels.
136    pub fn height(&self) -> u32 {
137        self.inner.height()
138    }
139
140    /// Number of color components per pixel.
141    pub fn count_comps(&self) -> u8 {
142        self.inner.count_comps()
143    }
144
145    /// Bits per color component.
146    pub fn bpc(&self) -> u8 {
147        self.inner.bpc()
148    }
149
150    /// Number of bytes per row.
151    pub fn row_stride(&self) -> usize {
152        self.inner.row_stride()
153    }
154}
155
156/// Scanline decoder for FlateDecode (zlib/deflate) data.
157///
158/// Decompresses the full data internally on construction, then yields
159/// `row_stride` bytes per [`decode_scanline`](ScanlineDecoder::decode_scanline) call.
160pub struct FlateScanlineDecoder {
161    data: Vec<u8>,
162    width: u32,
163    height: u32,
164    comps: u8,
165    bpc: u8,
166    row_stride: usize,
167    offset: usize,
168}
169
170impl FlateScanlineDecoder {
171    /// Create a new scanline decoder from compressed Flate data.
172    ///
173    /// The row stride is computed automatically as
174    /// `(width * comps * bpc + 7) / 8`, matching PDFium's pitch formula.
175    pub fn new(
176        compressed: &[u8],
177        width: u32,
178        height: u32,
179        comps: u8,
180        bpc: u8,
181    ) -> Result<Self, DecodeError> {
182        let data = crate::flate::decode(compressed, None, None, None, None)?;
183        let row_stride = compute_row_stride(width, comps, bpc);
184        Ok(Self {
185            data,
186            width,
187            height,
188            comps,
189            bpc,
190            row_stride,
191            offset: 0,
192        })
193    }
194
195    /// Create from already-decompressed pixel data.
196    pub fn from_decoded(data: Vec<u8>, width: u32, height: u32, comps: u8, bpc: u8) -> Self {
197        let row_stride = compute_row_stride(width, comps, bpc);
198        Self {
199            data,
200            width,
201            height,
202            comps,
203            bpc,
204            row_stride,
205            offset: 0,
206        }
207    }
208}
209
210impl ScanlineDecoder for FlateScanlineDecoder {
211    fn width(&self) -> u32 {
212        self.width
213    }
214
215    fn height(&self) -> u32 {
216        self.height
217    }
218
219    fn count_comps(&self) -> u8 {
220        self.comps
221    }
222
223    fn bpc(&self) -> u8 {
224        self.bpc
225    }
226
227    fn row_stride(&self) -> usize {
228        self.row_stride
229    }
230
231    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError> {
232        if self.row_stride == 0 || self.offset >= self.data.len() {
233            return Ok(None);
234        }
235        let end = (self.offset + self.row_stride).min(self.data.len());
236        let row = &self.data[self.offset..end];
237        self.offset = end;
238        Ok(Some(row))
239    }
240
241    fn reset(&mut self) -> Result<(), DecodeError> {
242        self.offset = 0;
243        Ok(())
244    }
245
246    fn current_line(&self) -> Option<usize> {
247        if self.row_stride == 0 || self.offset == 0 {
248            None
249        } else {
250            Some(self.offset / self.row_stride - 1)
251        }
252    }
253}
254
255/// Scanline decoder for DCTDecode (JPEG) data.
256///
257/// Decodes the full JPEG internally on construction, then yields
258/// one scanline at a time based on the decoded pixel stride.
259pub struct DctScanlineDecoder {
260    data: Vec<u8>,
261    width: u32,
262    height: u32,
263    comps: u8,
264    bpc: u8,
265    row_stride: usize,
266    offset: usize,
267}
268
269impl DctScanlineDecoder {
270    /// Create a new scanline decoder from JPEG-compressed data.
271    ///
272    /// The row stride is computed automatically as
273    /// `(width * comps * bpc + 7) / 8`, matching PDFium's pitch formula.
274    pub fn new(
275        jpeg_data: &[u8],
276        width: u32,
277        height: u32,
278        comps: u8,
279        bpc: u8,
280    ) -> Result<Self, DecodeError> {
281        let data = crate::jpeg::decode(jpeg_data)?;
282        let row_stride = compute_row_stride(width, comps, bpc);
283        Ok(Self {
284            data,
285            width,
286            height,
287            comps,
288            bpc,
289            row_stride,
290            offset: 0,
291        })
292    }
293
294    /// Create from already-decoded pixel data.
295    pub fn from_decoded(data: Vec<u8>, width: u32, height: u32, comps: u8, bpc: u8) -> Self {
296        let row_stride = compute_row_stride(width, comps, bpc);
297        Self {
298            data,
299            width,
300            height,
301            comps,
302            bpc,
303            row_stride,
304            offset: 0,
305        }
306    }
307}
308
309impl ScanlineDecoder for DctScanlineDecoder {
310    fn width(&self) -> u32 {
311        self.width
312    }
313
314    fn height(&self) -> u32 {
315        self.height
316    }
317
318    fn count_comps(&self) -> u8 {
319        self.comps
320    }
321
322    fn bpc(&self) -> u8 {
323        self.bpc
324    }
325
326    fn row_stride(&self) -> usize {
327        self.row_stride
328    }
329
330    fn decode_scanline(&mut self) -> Result<Option<&[u8]>, DecodeError> {
331        if self.row_stride == 0 || self.offset >= self.data.len() {
332            return Ok(None);
333        }
334        let end = (self.offset + self.row_stride).min(self.data.len());
335        let row = &self.data[self.offset..end];
336        self.offset = end;
337        Ok(Some(row))
338    }
339
340    fn reset(&mut self) -> Result<(), DecodeError> {
341        self.offset = 0;
342        Ok(())
343    }
344
345    fn current_line(&self) -> Option<usize> {
346        if self.row_stride == 0 || self.offset == 0 {
347            None
348        } else {
349            Some(self.offset / self.row_stride - 1)
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use flate2::Compression;
358    use flate2::write::ZlibEncoder;
359    use std::io::Write;
360
361    fn zlib_compress(data: &[u8]) -> Vec<u8> {
362        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
363        encoder.write_all(data).unwrap();
364        encoder.finish().unwrap()
365    }
366
367    #[test]
368    fn flate_scanline_basic() {
369        // 3 rows × 4 bytes (width=4, comps=1, bpc=8)
370        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
371        let compressed = zlib_compress(&raw);
372        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 3, 1, 8).unwrap();
373
374        assert_eq!(decoder.row_stride(), 4);
375        assert_eq!(decoder.width(), 4);
376        assert_eq!(decoder.height(), 3);
377        assert_eq!(decoder.count_comps(), 1);
378        assert_eq!(decoder.bpc(), 8);
379
380        let row1 = decoder.decode_scanline().unwrap().unwrap();
381        assert_eq!(row1, &[1, 2, 3, 4]);
382
383        let row2 = decoder.decode_scanline().unwrap().unwrap();
384        assert_eq!(row2, &[5, 6, 7, 8]);
385
386        let row3 = decoder.decode_scanline().unwrap().unwrap();
387        assert_eq!(row3, &[9, 10, 11, 12]);
388
389        // No more rows
390        assert!(decoder.decode_scanline().unwrap().is_none());
391    }
392
393    #[test]
394    fn flate_scanline_count() {
395        // 10 rows × 10 bytes (width=10, comps=1, bpc=8)
396        let raw = vec![0u8; 100];
397        let compressed = zlib_compress(&raw);
398        let mut decoder = FlateScanlineDecoder::new(&compressed, 10, 10, 1, 8).unwrap();
399
400        let mut count = 0;
401        while decoder.decode_scanline().unwrap().is_some() {
402            count += 1;
403        }
404        assert_eq!(count, 10);
405    }
406
407    #[test]
408    fn flate_scanline_matches_batch() {
409        // 12 rows × 10 bytes (width=10, comps=1, bpc=8)
410        let raw: Vec<u8> = (0..120).map(|i| (i % 256) as u8).collect();
411        let compressed = zlib_compress(&raw);
412        let mut decoder = FlateScanlineDecoder::new(&compressed, 10, 12, 1, 8).unwrap();
413
414        let mut collected = Vec::new();
415        while let Some(row) = decoder.decode_scanline().unwrap() {
416            collected.extend_from_slice(row);
417        }
418        assert_eq!(collected, raw);
419    }
420
421    #[test]
422    fn flate_scanline_none_after_last() {
423        // 1 row × 4 bytes (width=4, comps=1, bpc=8)
424        let raw = vec![1u8; 4];
425        let compressed = zlib_compress(&raw);
426        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 1, 1, 8).unwrap();
427
428        assert!(decoder.decode_scanline().unwrap().is_some());
429        assert!(decoder.decode_scanline().unwrap().is_none());
430        assert!(decoder.decode_scanline().unwrap().is_none());
431    }
432
433    #[test]
434    fn flate_scanline_reset() {
435        // 2 rows × 4 bytes (width=4, comps=1, bpc=8)
436        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8];
437        let compressed = zlib_compress(&raw);
438        let mut decoder = FlateScanlineDecoder::new(&compressed, 4, 2, 1, 8).unwrap();
439
440        let row1 = decoder.decode_scanline().unwrap().unwrap().to_vec();
441        decoder.decode_scanline().unwrap(); // consume row2
442        assert!(decoder.decode_scanline().unwrap().is_none());
443
444        decoder.reset().unwrap();
445        let row1_again = decoder.decode_scanline().unwrap().unwrap();
446        assert_eq!(row1, row1_again);
447    }
448
449    #[test]
450    fn flate_scanline_row_stride_calc() {
451        // 640×480 RGB image: row_stride = (640 * 3 * 8 + 7) / 8 = 1920
452        let raw = vec![0u8; 1920 * 2];
453        let compressed = zlib_compress(&raw);
454        let decoder = FlateScanlineDecoder::new(&compressed, 640, 2, 3, 8).unwrap();
455        assert_eq!(decoder.row_stride(), 1920);
456        assert_eq!(decoder.width(), 640);
457        assert_eq!(decoder.count_comps(), 3);
458    }
459
460    #[test]
461    fn flate_scanline_zero_stride() {
462        // width=0 produces row_stride=0; decode_scanline must return None immediately
463        let decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3], 0, 1, 1, 8);
464        let mut decoder: Box<dyn ScanlineDecoder> = Box::new(decoder);
465        assert!(decoder.decode_scanline().unwrap().is_none());
466    }
467
468    #[test]
469    fn dct_scanline_from_decoded() {
470        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
471        let raw = vec![10, 20, 30, 40, 50, 60];
472        let mut decoder = DctScanlineDecoder::from_decoded(raw.clone(), 1, 2, 3, 8);
473
474        assert_eq!(decoder.width(), 1);
475        assert_eq!(decoder.height(), 2);
476        assert_eq!(decoder.count_comps(), 3);
477        assert_eq!(decoder.bpc(), 8);
478        assert_eq!(decoder.row_stride(), 3);
479
480        let row1 = decoder.decode_scanline().unwrap().unwrap();
481        assert_eq!(row1, &[10, 20, 30]);
482
483        let row2 = decoder.decode_scanline().unwrap().unwrap();
484        assert_eq!(row2, &[40, 50, 60]);
485
486        assert!(decoder.decode_scanline().unwrap().is_none());
487    }
488
489    #[test]
490    fn dct_scanline_reset() {
491        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
492        let raw = vec![1, 2, 3, 4, 5, 6];
493        let mut decoder = DctScanlineDecoder::from_decoded(raw, 1, 2, 3, 8);
494
495        decoder.decode_scanline().unwrap();
496        decoder.decode_scanline().unwrap();
497        assert!(decoder.decode_scanline().unwrap().is_none());
498
499        decoder.reset().unwrap();
500        let first = decoder.decode_scanline().unwrap().unwrap();
501        assert_eq!(first, &[1, 2, 3]);
502    }
503
504    #[test]
505    fn scanline_decoder_trait_object_safety() {
506        fn assert_obj_safe(_: &dyn ScanlineDecoder) {}
507        // width=2, height=1, comps=1, bpc=8 → row_stride=2
508        let decoder = FlateScanlineDecoder::from_decoded(vec![0; 4], 2, 1, 1, 8);
509        assert_obj_safe(&decoder);
510    }
511
512    #[test]
513    fn partial_last_row() {
514        // Data that doesn't fill the last row completely.
515        // width=3, height=2, comps=1, bpc=8 → row_stride=3, total expected=6 but data=5
516        let raw = vec![1, 2, 3, 4, 5];
517        let mut decoder = FlateScanlineDecoder::from_decoded(raw, 3, 2, 1, 8);
518
519        let row1 = decoder.decode_scanline().unwrap().unwrap();
520        assert_eq!(row1, &[1, 2, 3]);
521
522        let row2 = decoder.decode_scanline().unwrap().unwrap();
523        assert_eq!(row2, &[4, 5]); // partial last row
524
525        assert!(decoder.decode_scanline().unwrap().is_none());
526    }
527
528    #[test]
529    fn metadata_getters_flate() {
530        // Verify that width/height/count_comps/bpc survive a round-trip
531        let raw = vec![0u8; 8 * 6 * 2]; // 8px wide, 6 rows, 16bpc grayscale
532        let compressed = zlib_compress(&raw);
533        // row_stride = (8 * 1 * 16 + 7) / 8 = 16
534        let decoder = FlateScanlineDecoder::new(&compressed, 8, 6, 1, 16).unwrap();
535        assert_eq!(decoder.width(), 8);
536        assert_eq!(decoder.height(), 6);
537        assert_eq!(decoder.count_comps(), 1);
538        assert_eq!(decoder.bpc(), 16);
539        assert_eq!(decoder.row_stride(), 16);
540    }
541
542    #[test]
543    fn metadata_getters_dct() {
544        // Verify that metadata is stored correctly in DctScanlineDecoder
545        let data = vec![0u8; 10 * 4]; // 10px wide, 4 rows, 1 comp, 8bpc
546        let decoder = DctScanlineDecoder::from_decoded(data, 10, 4, 1, 8);
547        assert_eq!(decoder.width(), 10);
548        assert_eq!(decoder.height(), 4);
549        assert_eq!(decoder.count_comps(), 1);
550        assert_eq!(decoder.bpc(), 8);
551        assert_eq!(decoder.row_stride(), 10);
552    }
553
554    #[test]
555    fn compute_row_stride_1bpc() {
556        // 1bpc packed: 8 pixels → 1 byte, 9 pixels → 2 bytes
557        let d8 = FlateScanlineDecoder::from_decoded(vec![0; 1], 8, 1, 1, 1);
558        assert_eq!(d8.row_stride(), 1);
559        let d9 = FlateScanlineDecoder::from_decoded(vec![0; 2], 9, 1, 1, 1);
560        assert_eq!(d9.row_stride(), 2);
561    }
562
563    #[test]
564    fn compute_row_stride_16bpc() {
565        // 16bpc RGB: 4 pixels → 4 * 3 * 2 = 24 bytes
566        let decoder = FlateScanlineDecoder::from_decoded(vec![0; 24], 4, 1, 3, 16);
567        assert_eq!(decoder.row_stride(), 24);
568    }
569
570    // -----------------------------------------------------------------------
571    // RandomAccessDecoder tests
572    // -----------------------------------------------------------------------
573
574    #[test]
575    fn random_access_sequential() {
576        // 3 rows × 4 bytes (width=4, comps=1, bpc=8)
577        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
578        let decoder = FlateScanlineDecoder::from_decoded(raw, 4, 3, 1, 8);
579        let mut ra = RandomAccessDecoder::new(decoder);
580
581        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
582        assert_eq!(row0, &[1, 2, 3, 4]);
583
584        let row1 = ra.get_scanline(1).unwrap().unwrap().to_vec();
585        assert_eq!(row1, &[5, 6, 7, 8]);
586
587        let row2 = ra.get_scanline(2).unwrap().unwrap().to_vec();
588        assert_eq!(row2, &[9, 10, 11, 12]);
589    }
590
591    #[test]
592    fn random_access_cached() {
593        // 2 rows × 3 bytes (width=1, comps=3, bpc=8)
594        let raw = vec![10, 20, 30, 40, 50, 60];
595        let decoder = FlateScanlineDecoder::from_decoded(raw, 1, 2, 3, 8);
596        let mut ra = RandomAccessDecoder::new(decoder);
597
598        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
599        assert_eq!(row0, &[10, 20, 30]);
600
601        // Read same line again — should return cached copy
602        let row0_again = ra.get_scanline(0).unwrap().unwrap().to_vec();
603        assert_eq!(row0_again, &[10, 20, 30]);
604    }
605
606    #[test]
607    fn random_access_rewind() {
608        // 3 rows × 3 bytes (width=3, comps=1, bpc=8)
609        let raw = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
610        let decoder = FlateScanlineDecoder::from_decoded(raw, 3, 3, 1, 8);
611        let mut ra = RandomAccessDecoder::new(decoder);
612
613        // Read line 2 (skip 0, 1)
614        let row2 = ra.get_scanline(2).unwrap().unwrap().to_vec();
615        assert_eq!(row2, &[7, 8, 9]);
616
617        // Read line 0 — requires rewind
618        let row0 = ra.get_scanline(0).unwrap().unwrap().to_vec();
619        assert_eq!(row0, &[1, 2, 3]);
620
621        let row1 = ra.get_scanline(1).unwrap().unwrap().to_vec();
622        assert_eq!(row1, &[4, 5, 6]);
623    }
624
625    #[test]
626    fn random_access_skip_forward() {
627        // 4 rows × 10 bytes (width=10, comps=1, bpc=8)
628        let raw: Vec<u8> = (0..40).collect();
629        let decoder = FlateScanlineDecoder::from_decoded(raw, 10, 4, 1, 8);
630        let mut ra = RandomAccessDecoder::new(decoder);
631
632        // Jump to line 3 directly
633        let row3 = ra.get_scanline(3).unwrap().unwrap().to_vec();
634        assert_eq!(row3, &[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]);
635    }
636
637    #[test]
638    fn random_access_out_of_bounds() {
639        // 1 row × 3 bytes (width=3, comps=1, bpc=8)
640        let raw = vec![1, 2, 3];
641        let decoder = FlateScanlineDecoder::from_decoded(raw, 3, 1, 1, 8);
642        let mut ra = RandomAccessDecoder::new(decoder);
643
644        // Only 1 row exists; line 1 should return None
645        let result = ra.get_scanline(1).unwrap();
646        assert!(result.is_none());
647    }
648
649    #[test]
650    fn random_access_inner_ref() {
651        // 1 row × 2 bytes (width=2, comps=1, bpc=8)
652        let raw = vec![1, 2, 3, 4];
653        let decoder = FlateScanlineDecoder::from_decoded(raw, 2, 1, 1, 8);
654        let ra = RandomAccessDecoder::new(decoder);
655        assert_eq!(ra.inner().row_stride(), 2);
656        assert_eq!(ra.row_stride(), 2);
657        assert_eq!(ra.width(), 2);
658        assert_eq!(ra.height(), 1);
659        assert_eq!(ra.count_comps(), 1);
660        assert_eq!(ra.bpc(), 8);
661    }
662
663    #[test]
664    fn current_line_before_read() {
665        // width=2, height=2, comps=1, bpc=8 → row_stride=2
666        let decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
667        assert_eq!(decoder.current_line(), None);
668    }
669
670    #[test]
671    fn current_line_after_read() {
672        let mut decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
673        decoder.decode_scanline().unwrap();
674        assert_eq!(decoder.current_line(), Some(0));
675        decoder.decode_scanline().unwrap();
676        assert_eq!(decoder.current_line(), Some(1));
677    }
678
679    #[test]
680    fn current_line_after_reset() {
681        let mut decoder = FlateScanlineDecoder::from_decoded(vec![1, 2, 3, 4], 2, 2, 1, 8);
682        decoder.decode_scanline().unwrap();
683        decoder.reset().unwrap();
684        assert_eq!(decoder.current_line(), None);
685    }
686}