zune_qoi/
decoder.rs

1/*
2 * Copyright (c) 2023.
3 *
4 * This software is free software; You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
5 */
6
7#![allow(clippy::identity_op)]
8
9use alloc::vec::Vec;
10use alloc::{format, vec};
11
12use zune_core::bit_depth::BitDepth;
13use zune_core::bytestream::{ZByteReaderTrait, ZReader};
14use zune_core::colorspace::ColorSpace;
15use zune_core::log::{error, trace};
16use zune_core::options::DecoderOptions;
17
18use crate::constants::{
19    QOI_MASK_2, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN
20};
21use crate::errors::QoiErrors;
22
23#[allow(non_camel_case_types)]
24enum QoiColorspace {
25    sRGB,
26    // SRGB with Linear alpha
27    Linear
28}
29
30/// A Quite OK Image decoder
31///
32/// The decoder is initialized by calling `new`
33/// and either of [`decode_headers`] to decode headers
34/// or [`decode`] to return uncompressed pixels
35///
36/// Additional methods are provided that give more
37/// details of the compressed image like width and height
38/// are accessible after decoding headers
39///
40/// [`decode_headers`]:QoiDecoder::decode_headers
41/// [`decode`]:QoiDecoder::decode
42pub struct QoiDecoder<T>
43where
44    T: ZByteReaderTrait
45{
46    width:             usize,
47    height:            usize,
48    colorspace:        ColorSpace,
49    colorspace_layout: QoiColorspace,
50    decoded_headers:   bool,
51    stream:            ZReader<T>,
52    options:           DecoderOptions
53}
54
55impl<T> QoiDecoder<T>
56where
57    T: ZByteReaderTrait
58{
59    /// Create a new QOI format decoder with the default options
60    ///
61    /// # Arguments
62    /// - `data`: The compressed qoi data
63    ///
64    /// # Returns
65    /// - A decoder instance which will on calling `decode` will decode
66    /// data
67    /// # Example
68    ///
69    /// ```no_run
70    /// use zune_core::bytestream::ZCursor;
71    /// let mut decoder = zune_qoi::QoiDecoder::new(ZCursor::new(&[]));
72    /// // additional code
73    /// ```
74    pub fn new(data: T) -> QoiDecoder<T> {
75        QoiDecoder::new_with_options(data, DecoderOptions::default())
76    }
77    /// Create a new QOI format decoder that obeys specified restrictions
78    ///
79    /// E.g can be used to set width and height limits to prevent OOM attacks
80    ///
81    /// # Arguments
82    /// - `data`: The compressed qoi data
83    /// - `options`: Decoder options that the decoder should respect
84    ///
85    /// # Example
86    /// ```
87    /// use zune_core::bytestream::ZCursor;
88    /// use zune_core::options::DecoderOptions;
89    /// use zune_qoi::{QoiDecoder};
90    /// // only decode images less than 10 in both width and height
91    ///
92    /// let  options = DecoderOptions::default().set_max_width(10).set_max_height(10);
93    ///
94    /// let mut decoder=QoiDecoder::new_with_options(ZCursor::new([]),options);
95    /// ```
96    #[allow(clippy::redundant_field_names)]
97    pub fn new_with_options(data: T, options: DecoderOptions) -> QoiDecoder<T> {
98        QoiDecoder {
99            width:             0,
100            height:            0,
101            colorspace:        ColorSpace::RGB,
102            colorspace_layout: QoiColorspace::Linear,
103            decoded_headers:   false,
104            stream:            ZReader::new(data),
105            options:           options
106        }
107    }
108    /// Decode a QOI header storing needed information into
109    /// the decoder instance
110    ///
111    ///
112    /// # Returns
113    ///
114    /// - On success: Nothing
115    /// - On error: The error encountered when decoding headers
116    ///     error type will be an instance of [QoiErrors]
117    ///
118    /// [QoiErrors]:crate::errors::QoiErrors
119    pub fn decode_headers(&mut self) -> Result<(), QoiErrors> {
120        //let header_bytes = 4/*magic*/ + 8/*Width+height*/ + 1/*channels*/ + 1 /*colorspace*/;
121
122        // match magic bytes.
123        let magic = self.stream.read_fixed_bytes_or_error::<4>()?;
124
125        if &magic != b"qoif" {
126            return Err(QoiErrors::WrongMagicBytes);
127        }
128
129        // these were confirmed to be inbounds by has so use the non failing
130        // routines
131        let width = self.stream.get_u32_be() as usize;
132        let height = self.stream.get_u32_be() as usize;
133        let colorspace = self.stream.read_u8();
134        let colorspace_layout = self.stream.read_u8();
135
136        if width > self.options.max_width() {
137            let msg = format!(
138                "Width {} greater than max configured width {}",
139                width,
140                self.options.max_width()
141            );
142            return Err(QoiErrors::Generic(msg));
143        }
144
145        if height > self.options.max_height() {
146            let msg = format!(
147                "Height {} greater than max configured height {}",
148                height,
149                self.options.max_height()
150            );
151            return Err(QoiErrors::Generic(msg));
152        }
153        if width == 0 {
154            return Err(QoiErrors::GenericStatic("Image Width is zero"));
155        }
156        if height == 0 {
157            return Err(QoiErrors::GenericStatic("Image Height is zero"));
158        }
159
160        self.colorspace = match colorspace {
161            3 => ColorSpace::RGB,
162            4 => ColorSpace::RGBA,
163            _ => return Err(QoiErrors::UnknownChannels(colorspace))
164        };
165        self.colorspace_layout = match colorspace_layout {
166            0 => QoiColorspace::sRGB,
167            1 => QoiColorspace::Linear,
168            _ => {
169                if self.options.strict_mode() {
170                    return Err(QoiErrors::UnknownColorspace(colorspace_layout));
171                } else {
172                    error!("Unknown/invalid colorspace value {colorspace_layout}, expected 0 or 1");
173                    QoiColorspace::sRGB
174                }
175            }
176        };
177        self.width = width;
178        self.height = height;
179
180        trace!("Image width: {:?}", self.width);
181        trace!("Image height: {:?}", self.height);
182        trace!("Image colorspace:{:?}", self.colorspace);
183        self.decoded_headers = true;
184
185        Ok(())
186    }
187    /// Return the number of bytes required to hold a decoded image frame
188    /// decoded using the given input transformations
189    ///
190    /// # Returns
191    ///  - `Some(usize)`: Minimum size for a buffer needed to decode the image
192    ///  - `None`: Indicates the image was not decoded.
193    ///
194    /// # Panics
195    /// In case `width*height*colorspace` calculation may overflow a usize
196    pub fn output_buffer_size(&self) -> Option<usize> {
197        if self.decoded_headers {
198            self.width
199                .checked_mul(self.height)
200                .unwrap()
201                .checked_mul(self.colorspace.num_components())
202        } else {
203            None
204        }
205    }
206
207    /// Decode the bytes of a QOI image data, returning the
208    /// uncompressed bytes or  the error encountered during decoding
209    ///
210    /// Additional details about the encoded image can be found after calling this/[`decode_headers`]
211    ///
212    /// i.e the width and height. can be accessed by [`get_dimensions`] method.
213    ///
214    /// # Returns
215    /// - On success: The decoded bytes. The length of the bytes will be
216    /// - On error: An instance of [QoiErrors] which gives a reason why the image could not
217    /// be decoded
218    ///
219    /// [`decode_headers`]:Self::decode_headers
220    /// [`get_dimensions`]:Self::dimensions
221    /// [QoiErrors]:crate::errors::QoiErrors
222    pub fn decode(&mut self) -> Result<Vec<u8>, QoiErrors> {
223        if !self.decoded_headers {
224            self.decode_headers()?;
225        }
226        let mut output = vec![0; self.output_buffer_size().unwrap()];
227
228        self.decode_into(&mut output)?;
229
230        Ok(output)
231    }
232
233    /// Decode a compressed Qoi image and store the contents
234    /// into the output buffer
235    ///
236    /// Returns an error if the buffer cannot hold the contents
237    /// of the buffer
238    ///
239    /// # Arguments
240    ///
241    /// * `pixels`: Output buffer for which we will write decoded
242    /// pixels
243    ///
244    /// returns: Result<(), QoiErrors>
245    #[allow(clippy::identity_op)]
246    pub fn decode_into(&mut self, pixels: &mut [u8]) -> Result<(), QoiErrors> {
247        if !self.decoded_headers {
248            self.decode_headers()?;
249        }
250
251        if pixels.len() < self.output_buffer_size().unwrap() {
252            return Err(QoiErrors::InsufficientData(
253                self.output_buffer_size().unwrap(),
254                pixels.len()
255            ));
256        }
257
258        match self.colorspace.num_components() {
259            3 => self.decode_inner_generic::<3>(pixels)?,
260            4 => self.decode_inner_generic::<4>(pixels)?,
261            _ => unreachable!()
262        }
263        Ok(())
264    }
265    fn decode_inner_generic<const SIZE: usize>(
266        &mut self, pixels: &mut [u8]
267    ) -> Result<(), QoiErrors> {
268        const LAST_BYTES: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 1];
269
270        let mut index = [[0_u8; 4]; 64];
271        // starting pixel
272        let mut px = [0, 0, 0, 255];
273
274        let mut run = 0;
275
276        for pix_chunk in pixels.chunks_exact_mut(SIZE) {
277            if run > 0 {
278                run -= 1;
279                pix_chunk.copy_from_slice(&px[0..SIZE]);
280            } else {
281                let chunk = self.stream.read_u8();
282
283                if chunk == QOI_OP_RGB {
284                    let packed_bytes = self.stream.read_fixed_bytes_or_zero::<3>();
285
286                    px[0] = packed_bytes[0];
287                    px[1] = packed_bytes[1];
288                    px[2] = packed_bytes[2];
289                } else if chunk == QOI_OP_RGBA {
290                    let packed_bytes = self.stream.read_fixed_bytes_or_zero::<4>();
291                    px.copy_from_slice(&packed_bytes);
292                } else if (chunk & QOI_MASK_2) == QOI_OP_INDEX {
293                    px.copy_from_slice(&index[usize::from(chunk) & 63]);
294                } else if (chunk & QOI_MASK_2) == QOI_OP_DIFF {
295                    px[0] = px[0].wrapping_add(((chunk >> 4) & 0x03).wrapping_sub(2));
296                    px[1] = px[1].wrapping_add(((chunk >> 2) & 0x03).wrapping_sub(2));
297                    px[2] = px[2].wrapping_add(((chunk >> 0) & 0x03).wrapping_sub(2));
298                } else if (chunk & QOI_MASK_2) == QOI_OP_LUMA {
299                    let b2 = self.stream.read_u8();
300                    let vg = (chunk & 0x3f).wrapping_sub(32);
301
302                    px[0] = px[0].wrapping_add(vg.wrapping_sub(8).wrapping_add((b2 >> 4) & 0x0f));
303                    px[1] = px[1].wrapping_add(vg);
304                    px[2] = px[2].wrapping_add(vg.wrapping_sub(8).wrapping_add((b2 >> 0) & 0x0f));
305                } else if (chunk & QOI_MASK_2) == QOI_OP_RUN {
306                    run = usize::from(chunk & 0x3f);
307                }
308
309                // copy pixel
310                pix_chunk.copy_from_slice(&px[0..SIZE]);
311
312                let color_hash = {
313                    // faster hash function
314                    // Stolen from https://github.com/zakarumych/rapid-qoi/blob/c5359a53476001d8d170c3733e6ab22e8173f40f/src/lib.rs#L474-L478
315                    let v = u64::from(u32::from_ne_bytes(px));
316                    let s = ((v << 32) | v) & 0xFF00FF0000FF00FF;
317
318                    (s.wrapping_mul(0x030007000005000Bu64.to_le()).swap_bytes() as u8 & 63) as usize
319                };
320                index[color_hash] = px;
321            }
322        }
323        let remaining = self.stream.read_fixed_bytes_or_error()?;
324
325        if remaining != LAST_BYTES {
326            if self.options.strict_mode() {
327                return Err(QoiErrors::GenericStatic(
328                    "Last bytes do not match QOI signature"
329                ));
330            }
331            error!("Last bytes do not match QOI signature");
332        }
333
334        trace!("Finished decoding image");
335
336        Ok(())
337    }
338
339    /// Returns QOI colorspace or none if the headers haven't been
340    ///
341    /// Colorspace returned can either be [RGB] or [RGBA]
342    ///
343    /// # Returns
344    /// - `Some(Colorspace)`: The colorspace present
345    /// -  `None` : This indicates the image header wasn't decoded hence
346    ///   colorspace is unknown
347    ///
348    /// [RGB]: zune_core::colorspace::ColorSpace::RGB
349    /// [RGBA]: zune_core::colorspace::ColorSpace::RGB
350    pub const fn colorspace(&self) -> Option<ColorSpace> {
351        if self.decoded_headers {
352            Some(self.colorspace)
353        } else {
354            None
355        }
356    }
357    /// Return QOI default bit depth
358    ///
359    /// This is always 8
360    ///
361    /// # Returns
362    /// - [`BitDepth::U8`]
363    ///
364    /// # Example
365    ///
366    /// ```
367    /// use zune_core::bit_depth::BitDepth;
368    /// use zune_core::bytestream::ZCursor;
369    /// use zune_qoi::QoiDecoder;
370    /// let decoder = QoiDecoder::new(ZCursor::new(&[]));
371    /// assert_eq!(decoder.bit_depth(),BitDepth::Eight)
372    /// ```
373    ///
374    /// [`BitDepth::U8`]:zune_core::bit_depth::BitDepth::Eight
375    pub const fn bit_depth(&self) -> BitDepth {
376        BitDepth::Eight
377    }
378
379    /// Return the width and height of the image
380    ///
381    /// Or none if the headers haven't been decoded
382    ///
383    /// # Returns
384    /// - `Some(width,height)` - If headers are decoded, this will return the stored
385    /// width and height for that image
386    /// - `None`: This indicates the image headers weren't decoded or an error
387    /// occurred when decoding headers
388    /// # Example
389    ///
390    /// ```no_run
391    /// use zune_core::bytestream::ZCursor;
392    /// use zune_qoi::QoiDecoder;
393    /// let mut decoder = QoiDecoder::new(ZCursor::new(&[]));
394    ///
395    /// decoder.decode_headers().unwrap();
396    /// // get dimensions now.
397    /// let (w,h)=decoder.dimensions().unwrap();
398    /// ```
399    pub const fn dimensions(&self) -> Option<(usize, usize)> {
400        if self.decoded_headers {
401            return Some((self.width, self.height));
402        }
403        None
404    }
405}