rapid_qoi/
encode.rs

1use super::*;
2
3#[cfg(feature = "alloc")]
4use alloc::{vec, vec::Vec};
5
6/// Errors that may occur during image encoding.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum EncodeError {
9    /// Pixels buffer is too small for the image.
10    NotEnoughPixelData,
11
12    /// Output buffer is too small to fit encoded image.
13    OutputIsTooSmall,
14}
15
16impl Display for EncodeError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            EncodeError::NotEnoughPixelData => f.write_str("Pixels buffer is too small for image"),
20            EncodeError::OutputIsTooSmall => {
21                f.write_str("Output buffer is too small to fit encoded image")
22            }
23        }
24    }
25}
26
27#[cfg(feature = "std")]
28impl std::error::Error for EncodeError {}
29
30impl Qoi {
31    /// Encode raw RGB or RGBA pixels into a QOI image.\
32    /// Encoded image is written into `output` slice.
33    ///
34    /// On success this function returns `Ok(())`.\
35    /// On failure this function returns `Err(err)` with `err` describing cause of the error.
36    #[inline]
37    pub fn encode(&self, pixels: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
38        if output.len() <= QOI_HEADER_SIZE {
39            return Err(EncodeError::OutputIsTooSmall);
40        }
41
42        output[0..4].copy_from_slice(&QOI_MAGIC.to_be_bytes());
43        output[4..8].copy_from_slice(&self.width.to_be_bytes());
44        output[8..12].copy_from_slice(&self.height.to_be_bytes());
45
46        let channels = match self.colors {
47            Colors::Rgb => {
48                output[12] = 3;
49                output[13] = 1;
50                3
51            }
52            Colors::Rgba => {
53                output[12] = 4;
54                output[13] = 1;
55                4
56            }
57            Colors::Srgb => {
58                output[12] = 3;
59                output[13] = 0;
60                3
61            }
62            Colors::SrgbLinA => {
63                output[12] = 4;
64                output[13] = 0;
65                4
66            }
67        };
68
69        let px_len = self.width as usize * self.height as usize * channels;
70
71        let pixels = match pixels.get(..px_len) {
72            None => {
73                cold();
74                return Err(EncodeError::NotEnoughPixelData);
75            }
76            Some(pixels) => pixels,
77        };
78
79        let size = match self.colors.has_alpha() {
80            true => Self::encode_range::<4>(
81                &mut [Pixel::new(); 64],
82                &mut Pixel::new_opaque(),
83                &mut 0,
84                pixels,
85                &mut output[QOI_HEADER_SIZE..],
86            )?,
87            false => Self::encode_range::<3>(
88                &mut [Pixel::new(); 64],
89                &mut Pixel::new_opaque(),
90                &mut 0,
91                pixels,
92                &mut output[QOI_HEADER_SIZE..],
93            )?,
94        };
95
96        if output.len() < size + QOI_PADDING + QOI_HEADER_SIZE {
97            return Err(EncodeError::OutputIsTooSmall);
98        }
99
100        output[QOI_HEADER_SIZE + size..][..QOI_PADDING - 1].fill(0);
101        output[QOI_HEADER_SIZE + size + QOI_PADDING - 1] = 1;
102
103        Ok(size + QOI_PADDING + QOI_HEADER_SIZE)
104    }
105
106    /// Encode range of pixels into output slice.
107    #[inline]
108    pub fn encode_range<const N: usize>(
109        index: &mut [[u8; N]; 64],
110        px_prev: &mut [u8; N],
111        run: &mut usize,
112        pixels: &[u8],
113        output: &mut [u8],
114    ) -> Result<usize, EncodeError>
115    where
116        [u8; N]: Pixel,
117    {
118        let mut rest = &mut *output;
119
120        assert_eq!(pixels.len() % N, 0);
121
122        // let mut chunks = pixels.chunks_exact(N);
123        let mut pixels = bytemuck::cast_slice::<_, [u8; N]>(pixels);
124
125        loop {
126            match pixels {
127                // Some(chunk) => {
128                [px, tail @ ..] => {
129                    pixels = tail;
130                    if likely(rest.len() > 7) {
131                        if *px == *px_prev {
132                            if *run == 61 || unlikely(pixels.is_empty()) {
133                                rest[0] = QOI_OP_RUN | (*run as u8);
134                                rest = &mut rest[1..];
135                                *run = 0;
136                            } else {
137                                *run += 1;
138                            }
139                        } else {
140                            match run {
141                                0 => {}
142                                1 => {
143                                    // While not following reference encoder
144                                    // this produces valid QOI and have the exactly same size.
145                                    // Decoding is slightly faster.
146                                    let index_pos = px_prev.hash();
147                                    if unlikely(index_pos == 0x35 && index[0x35] == [0; N]) {
148                                        rest[0] = QOI_OP_RUN;
149                                    } else {
150                                        rest[0] = QOI_OP_INDEX | index_pos as u8;
151                                    }
152                                    rest = &mut rest[1..];
153                                    *run = 0;
154                                }
155                                _ => {
156                                    rest[0] = QOI_OP_RUN | (*run - 1) as u8;
157                                    rest = &mut rest[1..];
158                                    *run = 0;
159                                }
160                            }
161
162                            match rest {
163                                [b1, b2, b3, b4, b5, ..] => {
164                                    let index_pos = px.hash();
165
166                                    if index[index_pos as usize] == *px {
167                                        *b1 = QOI_OP_INDEX | index_pos as u8;
168                                        rest = &mut rest[1..];
169                                    } else {
170                                        index[index_pos as usize] = *px;
171
172                                        if N == 4 && px_prev.a() != px.a() {
173                                            cold();
174                                            let [r, g, b, a] = px.rgba();
175                                            *b1 = QOI_OP_RGBA;
176                                            *b2 = r;
177                                            *b3 = g;
178                                            *b4 = b;
179                                            *b5 = a;
180                                            rest = &mut rest[5..];
181                                        } else {
182                                            let v = px.var(px_prev);
183
184                                            if let Some(diff) = v.diff() {
185                                                *b1 = diff;
186                                                rest = &mut rest[1..];
187                                            } else if let Some([lu, ma]) = v.luma() {
188                                                *b1 = lu;
189                                                *b2 = ma;
190                                                rest = &mut rest[2..];
191                                            } else {
192                                                let [r, g, b] = px.rgb();
193                                                *b1 = QOI_OP_RGB;
194                                                *b2 = r;
195                                                *b3 = g;
196                                                *b4 = b;
197                                                rest = &mut rest[4..];
198                                            }
199                                        }
200                                    }
201                                    *px_prev = *px;
202                                }
203                                _ => {
204                                    cold();
205                                    unreachable!()
206                                }
207                            }
208                        }
209                    } else {
210                        return Err(EncodeError::OutputIsTooSmall);
211                    }
212                }
213                // None => {
214                [] => {
215                    cold();
216                    break;
217                }
218            }
219        }
220
221        let tail = rest.len();
222
223        Ok(output.len() - tail)
224    }
225
226    /// Returns maximum size of the `Qoi::encode` output size.\
227    /// Using smaller slice may cause `Qoi::encode` to return `Err(EncodeError::OutputIsTooSmall)`.
228    #[inline]
229    pub fn encoded_size_limit(&self) -> usize {
230        self.width as usize * self.height as usize * (self.colors.has_alpha() as usize + 4)
231            + QOI_HEADER_SIZE
232            + QOI_PADDING
233    }
234
235    /// Encode raw RGB or RGBA pixels into a QOI image.\
236    /// Encoded image is written into allocated `Vec`.
237    ///
238    /// On success this function returns `Ok(vec)` with `vec` containing encoded image.\
239    /// On failure this function returns `Err(err)` with `err` describing cause of the error.
240    #[cfg(feature = "alloc")]
241    #[inline]
242    pub fn encode_alloc(&self, pixels: &[u8]) -> Result<Vec<u8>, EncodeError> {
243        let limit = self.encoded_size_limit();
244        let mut output = vec![0; limit];
245        match self.encode(pixels, &mut output) {
246            Ok(size) => {
247                output.truncate(size);
248                Ok(output)
249            }
250            Err(EncodeError::OutputIsTooSmall) => unreachable(),
251            Err(err) => Err(err),
252        }
253    }
254}