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}