Skip to main content

libjpeg_turbo_rs/api/
scanline.rs

1// libjpeg-turbo-rs: alloc prelude (no_std support, issue #356)
2/// Scanline-level encode and decode API for row-by-row JPEG processing.
3///
4/// `ScanlineDecoder` wraps the existing `Decoder` and exposes a scanline-at-a-time
5/// read interface. Internally it performs a lazy full decode on first access, then
6/// serves individual rows from the decoded buffer.
7///
8/// `ScanlineEncoder` accumulates pixel rows one at a time, then delegates to the
9/// existing compression pipeline on `finish()`.
10use crate::common::error::{JpegError, Result};
11use crate::common::types::{ColorSpace, DctMethod, FrameHeader, PixelFormat, Subsampling};
12use crate::decode::pipeline::{Decoder, Image};
13#[allow(unused_imports)]
14use alloc::vec::Vec;
15#[allow(unused_imports)]
16use alloc::{format, vec};
17
18/// Row-by-row JPEG decoder.
19pub struct ScanlineDecoder<'a> {
20    decoder: Decoder<'a>,
21    decoded_image: Option<Image>,
22    current_line: usize,
23    crop_x: Option<(usize, usize)>,
24    bottom_up: bool,
25}
26
27impl<'a> ScanlineDecoder<'a> {
28    /// Create a new scanline decoder from raw JPEG data.
29    pub fn new(data: &'a [u8]) -> Result<Self> {
30        let decoder: Decoder<'a> = Decoder::new(data)?;
31        Ok(Self {
32            decoder,
33            decoded_image: None,
34            current_line: 0,
35            crop_x: None,
36            bottom_up: false,
37        })
38    }
39
40    /// Returns the JPEG frame header.
41    pub fn header(&self) -> &FrameHeader {
42        self.decoder.header()
43    }
44
45    /// Returns the number of scanlines read so far.
46    pub fn output_scanline(&self) -> usize {
47        self.current_line
48    }
49
50    /// Set the output pixel format before starting decode.
51    pub fn set_output_format(&mut self, format: PixelFormat) {
52        self.decoder.set_output_format(format);
53    }
54
55    /// Enable or disable fast (nearest-neighbor) upsampling.
56    pub fn set_fast_upsample(&mut self, fast: bool) {
57        self.decoder.fast_upsample = fast;
58    }
59
60    /// Enable or disable fast DCT for decoding.
61    pub fn set_fast_dct(&mut self, fast: bool) {
62        self.decoder.set_fast_dct(fast);
63    }
64
65    /// Set the DCT/IDCT method for decoding.
66    pub fn set_dct_method(&mut self, method: DctMethod) {
67        self.decoder.dct_method = method;
68    }
69
70    /// Enable or disable inter-block smoothing.
71    pub fn set_block_smoothing(&mut self, smooth: bool) {
72        self.decoder.block_smoothing = smooth;
73    }
74
75    /// Override the output color space.
76    pub fn set_output_colorspace(&mut self, cs: ColorSpace) {
77        self.decoder.output_colorspace = Some(cs);
78    }
79
80    /// Enable merged upsampling optimization (combines upsample + color convert).
81    ///
82    /// When enabled and subsampling is 4:2:0 or 4:2:2, uses a merged path that
83    /// avoids intermediate chroma buffers. Faster but uses box-filter replication
84    /// instead of fancy triangle-filter upsampling.
85    pub fn set_merged_upsample(&mut self, enabled: bool) {
86        self.decoder.merged_upsample = enabled;
87    }
88
89    /// Set horizontal crop region for scanline-level decoding.
90    pub fn set_crop_x(&mut self, x: usize, width: usize) {
91        self.crop_x = Some((x, width));
92    }
93
94    /// Enable or disable bottom-up row order.
95    ///
96    /// When true, the output rows are reversed after decompression so that
97    /// row 0 of the output corresponds to the last row of the image.
98    /// Matches libjpeg-turbo's `TJPARAM_BOTTOMUP` on the decode side.
99    pub fn set_bottom_up(&mut self, bottom_up: bool) {
100        self.bottom_up = bottom_up;
101    }
102
103    /// Decode and copy one scanline into `buf`.
104    pub fn read_scanline(&mut self, buf: &mut [u8]) -> Result<()> {
105        self.ensure_decoded()?;
106        let img: &Image = self.decoded_image.as_ref().unwrap();
107        let bpp: usize = img.pixel_format.bytes_per_pixel();
108        let row_bytes: usize = img.width * bpp;
109        if self.current_line >= img.height {
110            return Err(JpegError::Unsupported("no more scanlines".into()));
111        }
112        let start: usize = self.current_line * row_bytes;
113        buf[..row_bytes].copy_from_slice(&img.data[start..start + row_bytes]);
114        self.current_line += 1;
115        Ok(())
116    }
117
118    /// Skip scanlines without copying data.
119    pub fn skip_scanlines(&mut self, count: usize) -> Result<usize> {
120        self.ensure_decoded()?;
121        let img: &Image = self.decoded_image.as_ref().unwrap();
122        let remaining: usize = img.height - self.current_line;
123        let actual: usize = count.min(remaining);
124        self.current_line += actual;
125        Ok(actual)
126    }
127
128    /// Finalize and return the complete decoded Image.
129    pub fn finish(mut self) -> Result<Image> {
130        self.ensure_decoded()?;
131        Ok(self.decoded_image.take().unwrap())
132    }
133
134    fn ensure_decoded(&mut self) -> Result<()> {
135        if self.decoded_image.is_none() {
136            let mut img: Image = self.decoder.decode_image()?;
137            if let Some((crop_x_offset, crop_width)) = self.crop_x {
138                img = Self::apply_horizontal_crop(img, crop_x_offset, crop_width)?;
139            }
140            if self.bottom_up {
141                img = Self::flip_rows_image(img);
142            }
143            self.decoded_image = Some(img);
144        }
145        Ok(())
146    }
147
148    /// Reverse the row order of an image for bottom-up output.
149    fn flip_rows_image(mut img: Image) -> Image {
150        let bpp: usize = img.pixel_format.bytes_per_pixel();
151        let row_bytes: usize = img.width * bpp;
152        let mut flipped: Vec<u8> = Vec::with_capacity(img.data.len());
153        for row in (0..img.height).rev() {
154            let start: usize = row * row_bytes;
155            flipped.extend_from_slice(&img.data[start..start + row_bytes]);
156        }
157        img.data = flipped;
158        img
159    }
160
161    fn apply_horizontal_crop(img: Image, x: usize, width: usize) -> Result<Image> {
162        let bpp: usize = img.pixel_format.bytes_per_pixel();
163        if x + width > img.width {
164            return Err(JpegError::Unsupported(format!(
165                "crop region {}..{} exceeds image width {}",
166                x,
167                x + width,
168                img.width
169            )));
170        }
171        let src_row_bytes: usize = img.width * bpp;
172        let dst_row_bytes: usize = width * bpp;
173        let mut data: Vec<u8> = Vec::with_capacity(dst_row_bytes * img.height);
174        for y in 0..img.height {
175            let src_start: usize = y * src_row_bytes + x * bpp;
176            data.extend_from_slice(&img.data[src_start..src_start + dst_row_bytes]);
177        }
178        Ok(Image {
179            xmp_data: None,
180            iptc_data: None,
181            width,
182            height: img.height,
183            pixel_format: img.pixel_format,
184            precision: img.precision,
185            data,
186            icc_profile: img.icc_profile,
187            exif_data: img.exif_data,
188            comment: img.comment,
189            density: img.density,
190            saved_markers: img.saved_markers,
191            warnings: img.warnings,
192        })
193    }
194}
195
196/// Row-by-row JPEG encoder.
197pub struct ScanlineEncoder {
198    pixels: Vec<u8>,
199    width: usize,
200    height: usize,
201    pixel_format: PixelFormat,
202    quality: u8,
203    subsampling: Subsampling,
204    current_line: usize,
205}
206
207impl ScanlineEncoder {
208    /// Create a new scanline encoder.
209    pub fn new(width: usize, height: usize, pixel_format: PixelFormat) -> Self {
210        let bpp: usize = pixel_format.bytes_per_pixel();
211        Self {
212            pixels: vec![0u8; width * height * bpp],
213            width,
214            height,
215            pixel_format,
216            quality: 75,
217            subsampling: Subsampling::S420,
218            current_line: 0,
219        }
220    }
221
222    /// Set JPEG quality factor (1-100).
223    pub fn set_quality(&mut self, quality: u8) {
224        self.quality = quality;
225    }
226
227    /// Set chroma subsampling mode.
228    pub fn set_subsampling(&mut self, subsampling: Subsampling) {
229        self.subsampling = subsampling;
230    }
231
232    /// Returns the index of the next scanline to be written.
233    pub fn next_scanline(&self) -> usize {
234        self.current_line
235    }
236
237    /// Write one row of pixel data.
238    pub fn write_scanline(&mut self, row: &[u8]) -> Result<()> {
239        if self.current_line >= self.height {
240            return Err(JpegError::Unsupported("all scanlines written".into()));
241        }
242        let bpp: usize = self.pixel_format.bytes_per_pixel();
243        let row_bytes: usize = self.width * bpp;
244        let start: usize = self.current_line * row_bytes;
245        self.pixels[start..start + row_bytes].copy_from_slice(&row[..row_bytes]);
246        self.current_line += 1;
247        Ok(())
248    }
249
250    /// Compress all accumulated scanlines into a JPEG byte stream.
251    pub fn finish(self) -> Result<Vec<u8>> {
252        if self.current_line != self.height {
253            return Err(JpegError::Unsupported(format!(
254                "not all scanlines written: {} of {}",
255                self.current_line, self.height
256            )));
257        }
258        crate::encode::pipeline::compress(
259            &self.pixels,
260            self.width,
261            self.height,
262            self.pixel_format,
263            self.quality,
264            self.subsampling,
265            crate::common::types::DctMethod::IsLow,
266        )
267    }
268}