Skip to main content

azul_layout/
image.rs

1//! Image decoding and encoding utilities, wrapping the `image` crate
2//! with Azul's FFI-compatible types (`RawImage`, `RawImageFormat`).
3//!
4//! - [`decode`]: Decodes image bytes in any supported format into a [`RawImage`].
5//! - [`encode`]: Encodes a [`RawImage`] into various output formats (PNG, JPEG, BMP, etc.).
6
7#[cfg(feature = "std")]
8pub mod decode {
9    use core::fmt;
10
11    use azul_core::resources::{RawImage, RawImageFormat};
12    use azul_css::{impl_result, impl_result_inner, U8Vec};
13    use image::{
14        error::{ImageError, LimitError, LimitErrorKind},
15        DynamicImage,
16    };
17
18    /// Errors that can occur when decoding an image from raw bytes.
19    #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
20    #[repr(C)]
21    pub enum DecodeImageError {
22        InsufficientMemory,
23        DimensionError,
24        UnsupportedImageFormat,
25        Unknown,
26    }
27
28    impl fmt::Display for DecodeImageError {
29        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30            match self {
31                Self::InsufficientMemory => write!(
32                    f,
33                    "Error decoding image: Not enough memory available to perform encoding \
34                     operation"
35                ),
36                Self::DimensionError => {
37                    write!(f, "Error decoding image: Wrong dimensions")
38                }
39                Self::UnsupportedImageFormat => {
40                    write!(f, "Error decoding image: Invalid data format")
41                }
42                Self::Unknown => write!(f, "Error decoding image: Unknown error"),
43            }
44        }
45    }
46
47    fn translate_image_error_decode(i: ImageError) -> DecodeImageError {
48        match i {
49            ImageError::Limits(l) => match l.kind() {
50                LimitErrorKind::InsufficientMemory => DecodeImageError::InsufficientMemory,
51                LimitErrorKind::DimensionError => DecodeImageError::DimensionError,
52                _ => DecodeImageError::Unknown,
53            },
54            _ => DecodeImageError::Unknown,
55        }
56    }
57
58    impl_result!(
59        RawImage,
60        DecodeImageError,
61        ResultRawImageDecodeImageError,
62        copy = false,
63        [Debug, Clone]
64    );
65
66    /// Decodes image bytes in any supported format into a [`RawImage`].
67    ///
68    /// The image format is guessed from the byte contents. Returns the decoded
69    /// pixel data along with dimensions and format information.
70    #[must_use] pub fn decode_raw_image_from_any_bytes(image_bytes: &[u8]) -> ResultRawImageDecodeImageError {
71        use azul_core::resources::RawImageData;
72
73        let image_format = match image::guess_format(image_bytes) {
74            Ok(o) => o,
75            Err(e) => {
76                return ResultRawImageDecodeImageError::Err(translate_image_error_decode(e));
77            }
78        };
79
80        let decoded = match image::load_from_memory_with_format(image_bytes, image_format) {
81            Ok(o) => o,
82            Err(e) => {
83                return ResultRawImageDecodeImageError::Err(translate_image_error_decode(e));
84            }
85        };
86
87        let ((width, height), data_format, pixels) = match decoded {
88            DynamicImage::ImageLuma8(i) => (
89                i.dimensions(),
90                RawImageFormat::R8,
91                RawImageData::U8(i.into_vec().into()),
92            ),
93            DynamicImage::ImageLumaA8(i) => (
94                i.dimensions(),
95                RawImageFormat::RG8,
96                RawImageData::U8(i.into_vec().into()),
97            ),
98            DynamicImage::ImageRgb8(i) => (
99                i.dimensions(),
100                RawImageFormat::RGB8,
101                RawImageData::U8(i.into_vec().into()),
102            ),
103            DynamicImage::ImageRgba8(i) => (
104                i.dimensions(),
105                RawImageFormat::RGBA8,
106                RawImageData::U8(i.into_vec().into()),
107            ),
108            DynamicImage::ImageLuma16(i) => (
109                i.dimensions(),
110                RawImageFormat::R16,
111                RawImageData::U16(i.into_vec().into()),
112            ),
113            DynamicImage::ImageLumaA16(i) => (
114                i.dimensions(),
115                RawImageFormat::RG16,
116                RawImageData::U16(i.into_vec().into()),
117            ),
118            DynamicImage::ImageRgb16(i) => (
119                i.dimensions(),
120                RawImageFormat::RGB16,
121                RawImageData::U16(i.into_vec().into()),
122            ),
123            DynamicImage::ImageRgba16(i) => (
124                i.dimensions(),
125                RawImageFormat::RGBA16,
126                RawImageData::U16(i.into_vec().into()),
127            ),
128            DynamicImage::ImageRgb32F(i) => (
129                i.dimensions(),
130                RawImageFormat::RGBF32,
131                RawImageData::F32(i.into_vec().into()),
132            ),
133            DynamicImage::ImageRgba32F(i) => (
134                i.dimensions(),
135                RawImageFormat::RGBAF32,
136                RawImageData::F32(i.into_vec().into()),
137            ),
138            _ => {
139                return ResultRawImageDecodeImageError::Err(DecodeImageError::Unknown);
140            }
141        };
142
143        ResultRawImageDecodeImageError::Ok(RawImage {
144            tag: Vec::new().into(),
145            pixels,
146            width: width as usize,
147            height: height as usize,
148            premultiplied_alpha: false,
149            data_format,
150        })
151    }
152
153    #[cfg(test)]
154    mod autotest_generated {
155        use std::collections::BTreeSet;
156
157        use image::error::{
158            DecodingError, ImageFormatHint, ParameterError, ParameterErrorKind, UnsupportedError,
159            UnsupportedErrorKind,
160        };
161
162        use super::*;
163
164        const ALL_ERRORS: [DecodeImageError; 4] = [
165            DecodeImageError::InsufficientMemory,
166            DecodeImageError::DimensionError,
167            DecodeImageError::UnsupportedImageFormat,
168            DecodeImageError::Unknown,
169        ];
170
171        fn err_of(r: &ResultRawImageDecodeImageError) -> DecodeImageError {
172            match r.as_result() {
173                Ok(_) => panic!("expected Err, got Ok"),
174                Err(e) => *e,
175            }
176        }
177
178        // --- DecodeImageError::fmt (serializer) ---------------------------------
179
180        #[test]
181        fn display_every_variant_is_non_empty_and_unique() {
182            let rendered = ALL_ERRORS.iter().map(|e| e.to_string()).collect::<Vec<_>>();
183            for (e, s) in ALL_ERRORS.iter().zip(rendered.iter()) {
184                assert!(!s.is_empty(), "empty Display for {e:?}");
185                assert!(
186                    s.starts_with("Error decoding image: "),
187                    "unexpected Display prefix for {e:?}: {s}"
188                );
189            }
190            let unique = rendered.iter().collect::<BTreeSet<_>>();
191            assert_eq!(unique.len(), ALL_ERRORS.len(), "Display collides: {rendered:?}");
192        }
193
194        #[test]
195        fn display_text_is_stable() {
196            assert_eq!(
197                DecodeImageError::DimensionError.to_string(),
198                "Error decoding image: Wrong dimensions"
199            );
200            assert_eq!(
201                DecodeImageError::UnsupportedImageFormat.to_string(),
202                "Error decoding image: Invalid data format"
203            );
204            assert_eq!(
205                DecodeImageError::Unknown.to_string(),
206                "Error decoding image: Unknown error"
207            );
208            // NOTE: the InsufficientMemory arm says "encoding operation" inside a *decode*
209            // error. Pinned here because it is user-visible text, not because it is right.
210            assert_eq!(
211                DecodeImageError::InsufficientMemory.to_string(),
212                "Error decoding image: Not enough memory available to perform encoding operation"
213            );
214        }
215
216        #[test]
217        fn display_ignores_width_and_precision_specs_without_panicking() {
218            for e in ALL_ERRORS {
219                // The impl writes straight to the formatter, so padding/precision are no-ops.
220                let plain = format!("{e}");
221                assert_eq!(format!("{e:>200}"), plain);
222                assert_eq!(format!("{e:.1}"), plain);
223                assert_eq!(format!("{e:^0}"), plain);
224                assert!(!format!("{e:?}").is_empty());
225            }
226        }
227
228        #[test]
229        fn derived_ord_is_total_and_matches_declaration_order() {
230            for (i, a) in ALL_ERRORS.iter().enumerate() {
231                for (j, b) in ALL_ERRORS.iter().enumerate() {
232                    assert_eq!(a.cmp(b), i.cmp(&j), "Ord disagrees for {a:?} vs {b:?}");
233                    assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
234                    assert_eq!(a == b, i == j);
235                }
236            }
237        }
238
239        // --- translate_image_error_decode (other) -------------------------------
240
241        #[test]
242        fn translate_maps_limit_kinds() {
243            assert_eq!(
244                translate_image_error_decode(ImageError::Limits(LimitError::from_kind(
245                    LimitErrorKind::InsufficientMemory
246                ))),
247                DecodeImageError::InsufficientMemory
248            );
249            assert_eq!(
250                translate_image_error_decode(ImageError::Limits(LimitError::from_kind(
251                    LimitErrorKind::DimensionError
252                ))),
253                DecodeImageError::DimensionError
254            );
255            // The catch-all arm of the (non_exhaustive) LimitErrorKind match.
256            assert_eq!(
257                translate_image_error_decode(ImageError::Limits(LimitError::from_kind(
258                    LimitErrorKind::Unsupported {
259                        limits: image::Limits::default(),
260                        supported: image::LimitSupport::default(),
261                    }
262                ))),
263                DecodeImageError::Unknown
264            );
265        }
266
267        #[test]
268        fn translate_maps_every_other_variant_to_unknown() {
269            let cases = vec![
270                ImageError::IoError(std::io::Error::other("boom")),
271                ImageError::Decoding(DecodingError::new(ImageFormatHint::Unknown, "bad chunk")),
272                ImageError::Unsupported(UnsupportedError::from_format_and_kind(
273                    ImageFormatHint::Unknown,
274                    UnsupportedErrorKind::Format(ImageFormatHint::Unknown),
275                )),
276                // NOTE: a *parameter* dimension mismatch is NOT mapped to DimensionError --
277                // only `Limits` errors are inspected. Pinning the lossy mapping.
278                ImageError::Parameter(ParameterError::from_kind(
279                    ParameterErrorKind::DimensionMismatch,
280                )),
281                ImageError::Parameter(ParameterError::from_kind(ParameterErrorKind::NoMoreData)),
282            ];
283            for c in cases {
284                let debug = format!("{c:?}");
285                assert_eq!(
286                    translate_image_error_decode(c),
287                    DecodeImageError::Unknown,
288                    "unexpected mapping for {debug}"
289                );
290            }
291        }
292
293        // --- decode_raw_image_from_any_bytes (other / parser-ish) ---------------
294
295        #[test]
296        fn decode_empty_input_errors_without_panicking() {
297            let r = decode_raw_image_from_any_bytes(&[]);
298            assert!(r.is_err());
299            assert_eq!(err_of(&r), DecodeImageError::Unknown);
300        }
301
302        #[test]
303        fn decode_whitespace_and_text_bytes_error() {
304            for input in [
305                &b"   "[..],
306                &b"\t\n\r\n"[..],
307                &b"not an image at all"[..],
308                "\u{1F600} combining a\u{0301}\u{0308}".as_bytes(),
309                "\u{FEFF}\u{202E}\u{0000}".as_bytes(),
310            ] {
311                let r = decode_raw_image_from_any_bytes(input);
312                assert!(r.is_err(), "unexpectedly decoded {input:?}");
313                assert_eq!(err_of(&r), DecodeImageError::Unknown);
314            }
315        }
316
317        #[test]
318        fn decode_invalid_utf8_and_garbage_bytes_error() {
319            for input in [
320                &[0xFF, 0xFE, 0x00][..],
321                &[0xFF][..],
322                &[0x00, 0x00, 0x00, 0x00][..],
323                &[0xC0, 0x80, 0xED, 0xA0, 0x80][..],
324            ] {
325                let r = decode_raw_image_from_any_bytes(input);
326                assert!(r.is_err(), "unexpectedly decoded {input:?}");
327                assert_eq!(err_of(&r), DecodeImageError::Unknown);
328            }
329        }
330
331        #[test]
332        fn decode_every_single_byte_input_errors() {
333            for b in 0u8..=255 {
334                let r = decode_raw_image_from_any_bytes(&[b]);
335                assert!(r.is_err(), "single byte {b:#04X} decoded to an image");
336            }
337        }
338
339        #[test]
340        fn decode_extremely_long_garbage_does_not_hang_or_panic() {
341            let huge = vec![0xAAu8; 1_000_000];
342            let r = decode_raw_image_from_any_bytes(&huge);
343            assert!(r.is_err());
344            assert_eq!(err_of(&r), DecodeImageError::Unknown);
345        }
346
347        /// Valid magic bytes with a truncated / missing body: `guess_format` succeeds,
348        /// the actual decode must then fail cleanly (never panic), regardless of which
349        /// codec features are compiled in.
350        #[test]
351        fn decode_valid_magic_with_no_payload_errors() {
352            let png_magic = b"\x89PNG\r\n\x1a\n";
353            let gif_magic = b"GIF89a";
354            let bmp_magic = b"BM";
355            let jpeg_magic = b"\xFF\xD8\xFF";
356
357            for magic in [&png_magic[..], &gif_magic[..], &bmp_magic[..], &jpeg_magic[..]] {
358                let mut bytes = magic.to_vec();
359                let r = decode_raw_image_from_any_bytes(&bytes);
360                assert!(r.is_err(), "header-only input decoded: {magic:?}");
361
362                // ... and with a garbage payload appended.
363                bytes.extend(core::iter::repeat_n(0x7Fu8, 512));
364                let r = decode_raw_image_from_any_bytes(&bytes);
365                assert!(r.is_err(), "header + garbage decoded: {magic:?}");
366            }
367        }
368
369        #[cfg(feature = "png")]
370        fn png_of_2x2_rgba8(fill: u8) -> Vec<u8> {
371            use azul_core::resources::RawImageData;
372
373            let img = RawImage {
374                pixels: RawImageData::U8(vec![fill; 16].into()),
375                width: 2,
376                height: 2,
377                premultiplied_alpha: false,
378                data_format: RawImageFormat::RGBA8,
379                tag: Vec::<u8>::new().into(),
380            };
381            crate::image::encode::encode_png(&img)
382                .into_result()
383                .expect("png encode of a well-formed 2x2 RGBA8 image failed")
384                .as_slice()
385                .to_vec()
386        }
387
388        #[cfg(feature = "png")]
389        #[test]
390        fn decode_of_every_truncation_of_a_real_png_never_panics() {
391            let bytes = png_of_2x2_rgba8(1);
392            assert!(bytes.len() > 33, "PNG shorter than signature + IHDR");
393
394            for len in 0..bytes.len() {
395                let r = decode_raw_image_from_any_bytes(&bytes[..len]);
396                match r.as_result() {
397                    // A truncation that still decodes (the trailing IEND chunk is not load
398                    // bearing for every decoder) must at least not invent geometry or pixels.
399                    Ok(img) => {
400                        assert_eq!(
401                            (img.width, img.height),
402                            (2, 2),
403                            "truncation to {len} bytes resized the image"
404                        );
405                        assert_eq!(img.pixels.get_u8_vec_ref().expect("8-bit data").len(), 16);
406                    }
407                    // Anything cut off before the header is complete cannot possibly decode.
408                    Err(e) => {
409                        assert_eq!(*e, DecodeImageError::Unknown, "truncation to {len} bytes");
410                    }
411                }
412                if len <= 33 {
413                    assert!(r.is_err(), "a {len}-byte PNG prefix decoded to an image");
414                }
415            }
416        }
417
418        #[cfg(feature = "png")]
419        #[test]
420        fn decode_of_a_corrupted_png_never_panics_or_invents_geometry() {
421            let bytes = png_of_2x2_rgba8(9);
422
423            // Flip every byte of the stream in turn: CRC/zlib checks must reject, not panic.
424            for i in 0..bytes.len() {
425                let mut corrupted = bytes.clone();
426                corrupted[i] ^= 0xFF;
427                let r = decode_raw_image_from_any_bytes(&corrupted);
428                // Ok is acceptable in principle (e.g. a flipped byte in a padding field),
429                // the invariant under test is "no panic, and dimensions stay sane".
430                if let Ok(img) = r.as_result() {
431                    assert!(img.width <= 2 && img.height <= 2, "corrupt byte {i} grew the image");
432                }
433            }
434        }
435    }
436}
437#[cfg(feature = "std")]
438pub mod encode {
439    use alloc::vec::Vec;
440    use core::fmt;
441    use std::io::Cursor;
442
443    use azul_core::resources::{RawImage, RawImageFormat};
444    use azul_css::{impl_result, impl_result_inner, U8Vec};
445    #[cfg(feature = "bmp")]
446    use image::codecs::bmp::BmpEncoder;
447    #[cfg(feature = "gif")]
448    use image::codecs::gif::GifEncoder;
449#[cfg(feature = "jpeg")]
450    use image::codecs::jpeg::JpegEncoder;
451    #[cfg(feature = "png")]
452    use image::codecs::png::PngEncoder;
453    #[cfg(feature = "pnm")]
454    use image::codecs::pnm::PnmEncoder;
455    #[cfg(feature = "tga")]
456    use image::codecs::tga::TgaEncoder;
457    #[cfg(feature = "tiff")]
458    use image::codecs::tiff::TiffEncoder;
459    use image::error::{ImageError, LimitError, LimitErrorKind};
460
461    /// Errors that can occur when encoding a [`RawImage`] into a specific format.
462    #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
463    #[repr(C)]
464    pub enum EncodeImageError {
465        /// Crate was not compiled with the given encoder flags
466        EncoderNotAvailable,
467        InsufficientMemory,
468        DimensionError,
469        InvalidData,
470        Unknown,
471    }
472
473    impl fmt::Display for EncodeImageError {
474        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475            use self::EncodeImageError::{EncoderNotAvailable, InsufficientMemory, DimensionError, InvalidData, Unknown};
476            match self {
477                EncoderNotAvailable => write!(
478                    f,
479                    "Missing encoder (library was not compiled with given codec)"
480                ),
481                InsufficientMemory => write!(
482                    f,
483                    "Error encoding image: Not enough memory available to perform encoding \
484                     operation"
485                ),
486                DimensionError => write!(f, "Error encoding image: Wrong dimensions"),
487                InvalidData => write!(f, "Error encoding image: Invalid data format"),
488                Unknown => write!(f, "Error encoding image: Unknown error"),
489            }
490        }
491    }
492
493    const fn translate_rawimage_colortype(i: RawImageFormat) -> image::ColorType {
494        match i {
495            RawImageFormat::R8 => image::ColorType::L8,
496            RawImageFormat::RG8 => image::ColorType::La8,
497            RawImageFormat::RGB8 | RawImageFormat::BGR8 => image::ColorType::Rgb8,
498            RawImageFormat::RGBA8 | RawImageFormat::BGRA8 => image::ColorType::Rgba8,
499            RawImageFormat::R16 => image::ColorType::L16,
500            RawImageFormat::RG16 => image::ColorType::La16,
501            RawImageFormat::RGB16 => image::ColorType::Rgb16,
502            RawImageFormat::RGBA16 => image::ColorType::Rgba16,
503            RawImageFormat::RGBF32 => image::ColorType::Rgb32F,
504            RawImageFormat::RGBAF32 => image::ColorType::Rgba32F,
505        }
506    }
507
508    fn bgr_to_rgb_swap(pixels: &[u8], format: RawImageFormat) -> Option<Vec<u8>> {
509        match format {
510            RawImageFormat::BGR8 => {
511                let mut out = pixels.to_vec();
512                for chunk in out.chunks_exact_mut(3) {
513                    chunk.swap(0, 2);
514                }
515                Some(out)
516            }
517            RawImageFormat::BGRA8 => {
518                let mut out = pixels.to_vec();
519                for chunk in out.chunks_exact_mut(4) {
520                    chunk.swap(0, 2);
521                }
522                Some(out)
523            }
524            _ => None,
525        }
526    }
527
528    fn translate_image_error_encode(i: ImageError) -> EncodeImageError {
529        match i {
530            ImageError::Limits(l) => match l.kind() {
531                LimitErrorKind::InsufficientMemory => EncodeImageError::InsufficientMemory,
532                LimitErrorKind::DimensionError => EncodeImageError::DimensionError,
533                _ => EncodeImageError::Unknown,
534            },
535            _ => EncodeImageError::Unknown,
536        }
537    }
538
539    impl_result!(
540        U8Vec,
541        EncodeImageError,
542        ResultU8VecEncodeImageError,
543        copy = false,
544        [Debug, Clone]
545    );
546
547    macro_rules! encode_func {
548        ($func:ident, $encoder:ident, $feature:expr) => {
549            #[cfg(feature = $feature)]
550            pub fn $func(image: &RawImage) -> ResultU8VecEncodeImageError {
551                let width = match u32::try_from(image.width) {
552                    Ok(w) => w,
553                    Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
554                };
555                let height = match u32::try_from(image.height) {
556                    Ok(h) => h,
557                    Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
558                };
559                let mut result = Vec::<u8>::new();
560
561                {
562                    let mut cursor = Cursor::new(&mut result);
563                    let mut encoder = $encoder::new(&mut cursor);
564                    let pixels = match image.pixels.get_u8_vec_ref() {
565                        Some(s) => s,
566                        None => {
567                            return ResultU8VecEncodeImageError::Err(EncodeImageError::InvalidData);
568                        }
569                    };
570
571                    let swapped = bgr_to_rgb_swap(pixels.as_ref(), image.data_format);
572                    let pixel_bytes = swapped.as_deref().unwrap_or(pixels.as_ref());
573
574                    if let Err(e) = encoder.encode(
575                        pixel_bytes,
576                        width,
577                        height,
578                        translate_rawimage_colortype(image.data_format).into(),
579                    ) {
580                        return ResultU8VecEncodeImageError::Err(translate_image_error_encode(e));
581                    }
582                }
583
584                ResultU8VecEncodeImageError::Ok(result.into())
585            }
586
587            #[cfg(not(feature = $feature))]
588            #[must_use] pub const fn $func(image: &RawImage) -> ResultU8VecEncodeImageError {
589                ResultU8VecEncodeImageError::Err(EncodeImageError::EncoderNotAvailable)
590            }
591        };
592    }
593
594    encode_func!(encode_bmp, BmpEncoder, "bmp");
595    encode_func!(encode_tga, TgaEncoder, "tga");
596    encode_func!(encode_tiff, TiffEncoder, "tiff");
597    encode_func!(encode_gif, GifEncoder, "gif");
598    encode_func!(encode_pnm, PnmEncoder, "pnm");
599
600    #[cfg(feature = "png")]
601    pub fn encode_png(image: &RawImage) -> ResultU8VecEncodeImageError {
602        use image::ImageEncoder;
603
604        let width = match u32::try_from(image.width) {
605            Ok(w) => w,
606            Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
607        };
608        let height = match u32::try_from(image.height) {
609            Ok(h) => h,
610            Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
611        };
612        let mut result = Vec::<u8>::new();
613
614        {
615            let mut cursor = Cursor::new(&mut result);
616            let mut encoder = PngEncoder::new_with_quality(
617                &mut cursor,
618                image::codecs::png::CompressionType::Best,
619                image::codecs::png::FilterType::Adaptive,
620            );
621            let pixels = match image.pixels.get_u8_vec_ref() {
622                Some(s) => s,
623                None => {
624                    return ResultU8VecEncodeImageError::Err(EncodeImageError::InvalidData);
625                }
626            };
627
628            let swapped = bgr_to_rgb_swap(pixels.as_ref(), image.data_format);
629            let pixel_bytes = swapped.as_deref().unwrap_or(pixels.as_ref());
630
631            if let Err(e) = encoder.write_image(
632                pixel_bytes,
633                width,
634                height,
635                translate_rawimage_colortype(image.data_format).into(),
636            ) {
637                return ResultU8VecEncodeImageError::Err(translate_image_error_encode(e));
638            }
639        }
640
641        ResultU8VecEncodeImageError::Ok(result.into())
642    }
643
644    #[cfg(not(feature = "png"))]
645    #[must_use] pub const fn encode_png(image: &RawImage) -> ResultU8VecEncodeImageError {
646        ResultU8VecEncodeImageError::Err(EncodeImageError::EncoderNotAvailable)
647    }
648
649    #[cfg(feature = "jpeg")]
650    pub fn encode_jpeg(image: &RawImage, quality: u8) -> ResultU8VecEncodeImageError {
651        let width = match u32::try_from(image.width) {
652            Ok(w) => w,
653            Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
654        };
655        let height = match u32::try_from(image.height) {
656            Ok(h) => h,
657            Err(_) => return ResultU8VecEncodeImageError::Err(EncodeImageError::DimensionError),
658        };
659        let mut result = Vec::<u8>::new();
660
661        {
662            let mut cursor = Cursor::new(&mut result);
663            let mut encoder = JpegEncoder::new_with_quality(&mut cursor, quality);
664            let pixels = match image.pixels.get_u8_vec_ref() {
665                Some(s) => s,
666                None => {
667                    return ResultU8VecEncodeImageError::Err(EncodeImageError::InvalidData);
668                }
669            };
670
671            let swapped = bgr_to_rgb_swap(pixels.as_ref(), image.data_format);
672            let pixel_bytes = swapped.as_deref().unwrap_or(pixels.as_ref());
673
674            if let Err(e) = encoder.encode(
675                pixel_bytes,
676                width,
677                height,
678                translate_rawimage_colortype(image.data_format).into(),
679            ) {
680                return ResultU8VecEncodeImageError::Err(translate_image_error_encode(e));
681            }
682        }
683
684        ResultU8VecEncodeImageError::Ok(result.into())
685    }
686
687    #[cfg(not(feature = "jpeg"))]
688    #[must_use] pub const fn encode_jpeg(image: &RawImage, quality: u8) -> ResultU8VecEncodeImageError {
689        ResultU8VecEncodeImageError::Err(EncodeImageError::EncoderNotAvailable)
690    }
691
692    #[cfg(test)]
693    mod autotest_generated {
694        use std::collections::BTreeSet;
695
696        use azul_core::resources::RawImageData;
697        use image::error::{
698            DecodingError, EncodingError, ImageFormatHint, ParameterError, ParameterErrorKind,
699            UnsupportedError, UnsupportedErrorKind,
700        };
701
702        use super::*;
703
704        const ALL_ERRORS: [EncodeImageError; 5] = [
705            EncodeImageError::EncoderNotAvailable,
706            EncodeImageError::InsufficientMemory,
707            EncodeImageError::DimensionError,
708            EncodeImageError::InvalidData,
709            EncodeImageError::Unknown,
710        ];
711
712        const ALL_FORMATS: [RawImageFormat; 12] = [
713            RawImageFormat::R8,
714            RawImageFormat::RG8,
715            RawImageFormat::RGB8,
716            RawImageFormat::RGBA8,
717            RawImageFormat::R16,
718            RawImageFormat::RG16,
719            RawImageFormat::RGB16,
720            RawImageFormat::RGBA16,
721            RawImageFormat::BGR8,
722            RawImageFormat::BGRA8,
723            RawImageFormat::RGBF32,
724            RawImageFormat::RGBAF32,
725        ];
726
727        fn raw_u8(
728            width: usize,
729            height: usize,
730            data_format: RawImageFormat,
731            pixels: Vec<u8>,
732        ) -> RawImage {
733            RawImage {
734                pixels: RawImageData::U8(pixels.into()),
735                width,
736                height,
737                premultiplied_alpha: false,
738                data_format,
739                tag: Vec::<u8>::new().into(),
740            }
741        }
742
743        #[allow(dead_code)]
744        fn err_of(r: &ResultU8VecEncodeImageError) -> EncodeImageError {
745            match r.as_result() {
746                Ok(_) => panic!("expected Err, got Ok"),
747                Err(e) => *e,
748            }
749        }
750
751        // --- EncodeImageError::fmt (serializer) ---------------------------------
752
753        #[test]
754        fn display_every_variant_is_non_empty_and_unique() {
755            let rendered = ALL_ERRORS.iter().map(|e| e.to_string()).collect::<Vec<_>>();
756            for (e, s) in ALL_ERRORS.iter().zip(rendered.iter()) {
757                assert!(!s.is_empty(), "empty Display for {e:?}");
758            }
759            let unique = rendered.iter().collect::<BTreeSet<_>>();
760            assert_eq!(unique.len(), ALL_ERRORS.len(), "Display collides: {rendered:?}");
761        }
762
763        #[test]
764        fn display_text_is_stable() {
765            // The odd one out: no "Error encoding image" prefix.
766            assert_eq!(
767                EncodeImageError::EncoderNotAvailable.to_string(),
768                "Missing encoder (library was not compiled with given codec)"
769            );
770            assert_eq!(
771                EncodeImageError::InsufficientMemory.to_string(),
772                "Error encoding image: Not enough memory available to perform encoding operation"
773            );
774            assert_eq!(
775                EncodeImageError::DimensionError.to_string(),
776                "Error encoding image: Wrong dimensions"
777            );
778            assert_eq!(
779                EncodeImageError::InvalidData.to_string(),
780                "Error encoding image: Invalid data format"
781            );
782            assert_eq!(
783                EncodeImageError::Unknown.to_string(),
784                "Error encoding image: Unknown error"
785            );
786        }
787
788        #[test]
789        fn display_ignores_width_and_precision_specs_without_panicking() {
790            for e in ALL_ERRORS {
791                let plain = format!("{e}");
792                assert_eq!(format!("{e:>512}"), plain);
793                assert_eq!(format!("{e:.0}"), plain);
794                assert!(!format!("{e:?}").is_empty());
795            }
796        }
797
798        #[test]
799        fn derived_ord_is_total_and_matches_declaration_order() {
800            for (i, a) in ALL_ERRORS.iter().enumerate() {
801                for (j, b) in ALL_ERRORS.iter().enumerate() {
802                    assert_eq!(a.cmp(b), i.cmp(&j), "Ord disagrees for {a:?} vs {b:?}");
803                    assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
804                }
805            }
806        }
807
808        // --- translate_rawimage_colortype (other) -------------------------------
809
810        #[test]
811        fn colortype_mapping_is_exhaustive_and_stable() {
812            use image::ColorType::{L16, L8, La16, La8, Rgb16, Rgb32F, Rgb8, Rgba16, Rgba32F, Rgba8};
813
814            let expected = [
815                (RawImageFormat::R8, L8),
816                (RawImageFormat::RG8, La8),
817                (RawImageFormat::RGB8, Rgb8),
818                (RawImageFormat::RGBA8, Rgba8),
819                (RawImageFormat::R16, L16),
820                (RawImageFormat::RG16, La16),
821                (RawImageFormat::RGB16, Rgb16),
822                (RawImageFormat::RGBA16, Rgba16),
823                (RawImageFormat::BGR8, Rgb8),
824                (RawImageFormat::BGRA8, Rgba8),
825                (RawImageFormat::RGBF32, Rgb32F),
826                (RawImageFormat::RGBAF32, Rgba32F),
827            ];
828            assert_eq!(expected.len(), ALL_FORMATS.len(), "a RawImageFormat variant is untested");
829            for (format, want) in expected {
830                assert_eq!(
831                    translate_rawimage_colortype(format),
832                    want,
833                    "wrong ColorType for {format:?}"
834                );
835            }
836        }
837
838        #[test]
839        fn colortype_maps_bgr_onto_its_rgb_counterpart() {
840            // The BGR formats intentionally alias their RGB counterparts; the byte swap is
841            // handled separately by `bgr_to_rgb_swap`.
842            assert_eq!(
843                translate_rawimage_colortype(RawImageFormat::BGR8),
844                translate_rawimage_colortype(RawImageFormat::RGB8)
845            );
846            assert_eq!(
847                translate_rawimage_colortype(RawImageFormat::BGRA8),
848                translate_rawimage_colortype(RawImageFormat::RGBA8)
849            );
850        }
851
852        #[test]
853        fn colortype_is_usable_in_const_context() {
854            const C: image::ColorType = translate_rawimage_colortype(RawImageFormat::RGBA16);
855            assert_eq!(C, image::ColorType::Rgba16);
856        }
857
858        #[test]
859        fn colortype_channel_count_matches_the_source_format() {
860            for f in ALL_FORMATS {
861                let channels = translate_rawimage_colortype(f).channel_count();
862                let want = match f {
863                    RawImageFormat::R8 | RawImageFormat::R16 => 1,
864                    RawImageFormat::RG8 | RawImageFormat::RG16 => 2,
865                    RawImageFormat::RGB8
866                    | RawImageFormat::BGR8
867                    | RawImageFormat::RGB16
868                    | RawImageFormat::RGBF32 => 3,
869                    RawImageFormat::RGBA8
870                    | RawImageFormat::BGRA8
871                    | RawImageFormat::RGBA16
872                    | RawImageFormat::RGBAF32 => 4,
873                };
874                assert_eq!(channels, want, "channel count mismatch for {f:?}");
875            }
876        }
877
878        // --- bgr_to_rgb_swap (parser) -------------------------------------------
879
880        #[test]
881        fn swap_returns_none_for_every_non_bgr_format() {
882            for f in ALL_FORMATS {
883                if matches!(f, RawImageFormat::BGR8 | RawImageFormat::BGRA8) {
884                    continue;
885                }
886                assert_eq!(bgr_to_rgb_swap(&[1, 2, 3, 4, 5, 6], f), None, "{f:?} should not swap");
887                assert_eq!(bgr_to_rgb_swap(&[], f), None, "{f:?} should not swap (empty)");
888            }
889        }
890
891        #[test]
892        fn swap_of_empty_input_is_some_empty_not_none() {
893            // Empty input is *not* an error here: the format decides Some/None.
894            assert_eq!(bgr_to_rgb_swap(&[], RawImageFormat::BGR8), Some(Vec::new()));
895            assert_eq!(bgr_to_rgb_swap(&[], RawImageFormat::BGRA8), Some(Vec::new()));
896        }
897
898        #[test]
899        fn swap_bgr8_swaps_red_and_blue_only() {
900            assert_eq!(
901                bgr_to_rgb_swap(&[1, 2, 3, 4, 5, 6], RawImageFormat::BGR8),
902                Some(vec![3, 2, 1, 6, 5, 4])
903            );
904        }
905
906        #[test]
907        fn swap_bgra8_preserves_the_alpha_channel() {
908            assert_eq!(
909                bgr_to_rgb_swap(&[1, 2, 3, 200, 4, 5, 6, 201], RawImageFormat::BGRA8),
910                Some(vec![3, 2, 1, 200, 6, 5, 4, 201])
911            );
912        }
913
914        /// A pixel buffer whose length is not a whole number of pixels: `chunks_exact_mut`
915        /// silently drops the remainder, so the trailing bytes pass through unswapped.
916        #[test]
917        fn swap_leaves_a_trailing_partial_pixel_untouched() {
918            assert_eq!(bgr_to_rgb_swap(&[1], RawImageFormat::BGR8), Some(vec![1]));
919            assert_eq!(bgr_to_rgb_swap(&[1, 2], RawImageFormat::BGR8), Some(vec![1, 2]));
920            assert_eq!(
921                bgr_to_rgb_swap(&[1, 2, 3, 4], RawImageFormat::BGR8),
922                Some(vec![3, 2, 1, 4])
923            );
924            for len in 0..4usize {
925                let input = (0..len as u8).collect::<Vec<_>>();
926                assert_eq!(
927                    bgr_to_rgb_swap(&input, RawImageFormat::BGRA8),
928                    Some(input.clone()),
929                    "a partial BGRA8 pixel of {len} byte(s) must pass through unchanged"
930                );
931            }
932            assert_eq!(
933                bgr_to_rgb_swap(&[1, 2, 3, 4, 5, 6, 7], RawImageFormat::BGRA8),
934                Some(vec![3, 2, 1, 4, 5, 6, 7])
935            );
936        }
937
938        #[test]
939        fn swap_never_changes_the_length_and_is_its_own_inverse() {
940            for (format, stride) in [(RawImageFormat::BGR8, 3usize), (RawImageFormat::BGRA8, 4)] {
941                for len in 0..64usize {
942                    let input = (0..len).map(|i| (i % 251) as u8).collect::<Vec<_>>();
943                    let once = bgr_to_rgb_swap(&input, format).expect("BGR format must swap");
944                    assert_eq!(once.len(), input.len(), "length changed for {format:?}, len {len}");
945                    let twice = bgr_to_rgb_swap(&once, format).expect("BGR format must swap");
946                    assert_eq!(twice, input, "swap is not an involution for {format:?}, len {len}");
947                    // Only whole pixels move; the tail is untouched.
948                    let tail = len - (len / stride) * stride;
949                    assert_eq!(once[len - tail..], input[len - tail..]);
950                }
951            }
952        }
953
954        #[test]
955        fn swap_handles_arbitrary_non_utf8_and_unicode_bytes() {
956            assert_eq!(
957                bgr_to_rgb_swap(&[0xFF, 0xFE, 0x00], RawImageFormat::BGR8),
958                Some(vec![0x00, 0xFE, 0xFF])
959            );
960            // Bytes of "\u{1F600}" (F0 9F 98 80) reinterpreted as one BGRA8 pixel.
961            assert_eq!(
962                bgr_to_rgb_swap("\u{1F600}".as_bytes(), RawImageFormat::BGRA8),
963                Some(vec![0x98, 0x9F, 0xF0, 0x80])
964            );
965            assert_eq!(
966                bgr_to_rgb_swap(b"   \t\n", RawImageFormat::BGR8),
967                Some(vec![b' ', b' ', b' ', b'\t', b'\n'])
968            );
969            assert_eq!(
970                bgr_to_rgb_swap(&[0, 0, 0, 255, 255, 255], RawImageFormat::BGR8),
971                Some(vec![0, 0, 0, 255, 255, 255])
972            );
973        }
974
975        #[test]
976        fn swap_of_a_very_large_buffer_does_not_hang() {
977            const LEN: usize = 3 * 333_333 + 2; // ~1 MB, deliberately not pixel-aligned
978            let input = (0..LEN).map(|i| (i % 256) as u8).collect::<Vec<_>>();
979            let out = bgr_to_rgb_swap(&input, RawImageFormat::BGR8).expect("BGR8 must swap");
980            assert_eq!(out.len(), LEN);
981            assert_eq!(out[0], input[2]);
982            assert_eq!(out[2], input[0]);
983            assert_eq!(out[LEN - 1], input[LEN - 1]); // partial trailing pixel kept as-is
984            assert_eq!(out[LEN - 2], input[LEN - 2]);
985        }
986
987        // --- translate_image_error_encode (other) --------------------------------
988
989        #[test]
990        fn translate_maps_limit_kinds() {
991            assert_eq!(
992                translate_image_error_encode(ImageError::Limits(LimitError::from_kind(
993                    LimitErrorKind::InsufficientMemory
994                ))),
995                EncodeImageError::InsufficientMemory
996            );
997            assert_eq!(
998                translate_image_error_encode(ImageError::Limits(LimitError::from_kind(
999                    LimitErrorKind::DimensionError
1000                ))),
1001                EncodeImageError::DimensionError
1002            );
1003            assert_eq!(
1004                translate_image_error_encode(ImageError::Limits(LimitError::from_kind(
1005                    LimitErrorKind::Unsupported {
1006                        limits: image::Limits::default(),
1007                        supported: image::LimitSupport::default(),
1008                    }
1009                ))),
1010                EncodeImageError::Unknown
1011            );
1012        }
1013
1014        #[test]
1015        fn translate_maps_every_other_variant_to_unknown() {
1016            let cases = vec![
1017                ImageError::IoError(std::io::Error::other("boom")),
1018                ImageError::Encoding(EncodingError::new(ImageFormatHint::Unknown, "bad pixel")),
1019                ImageError::Decoding(DecodingError::new(ImageFormatHint::Unknown, "bad chunk")),
1020                // An unsupported *color type* (what the JPEG encoder returns for RGBA8) is
1021                // flattened to `Unknown`, never to `InvalidData`.
1022                ImageError::Unsupported(UnsupportedError::from_format_and_kind(
1023                    ImageFormatHint::Unknown,
1024                    UnsupportedErrorKind::Format(ImageFormatHint::Unknown),
1025                )),
1026                // ... and a parameter dimension mismatch is NOT mapped to DimensionError.
1027                ImageError::Parameter(ParameterError::from_kind(
1028                    ParameterErrorKind::DimensionMismatch,
1029                )),
1030            ];
1031            for c in cases {
1032                let debug = format!("{c:?}");
1033                assert_eq!(
1034                    translate_image_error_encode(c),
1035                    EncodeImageError::Unknown,
1036                    "unexpected mapping for {debug}"
1037                );
1038            }
1039        }
1040
1041        // --- encode_png ----------------------------------------------------------
1042
1043        #[cfg(feature = "png")]
1044        #[test]
1045        fn encode_png_round_trips_through_the_decoder() {
1046            use crate::image::decode::decode_raw_image_from_any_bytes;
1047
1048            for (format, bpp) in [
1049                (RawImageFormat::R8, 1usize),
1050                (RawImageFormat::RGB8, 3),
1051                (RawImageFormat::RGBA8, 4),
1052            ] {
1053                let pixels = (0..(2 * 2 * bpp)).map(|i| (i * 7 + 1) as u8).collect::<Vec<_>>();
1054                let encoded = encode_png(&raw_u8(2, 2, format, pixels.clone()))
1055                    .into_result()
1056                    .unwrap_or_else(|e| panic!("encode_png({format:?}) failed: {e}"));
1057                let bytes = encoded.as_slice();
1058                assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n", "missing PNG signature");
1059
1060                let decoded = decode_raw_image_from_any_bytes(bytes)
1061                    .into_result()
1062                    .unwrap_or_else(|e| panic!("decode of our own PNG ({format:?}) failed: {e}"));
1063                assert_eq!(decoded.width, 2);
1064                assert_eq!(decoded.height, 2);
1065                assert_eq!(decoded.data_format, format, "format changed across the round trip");
1066                assert!(!decoded.premultiplied_alpha);
1067                assert_eq!(
1068                    decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
1069                    pixels.as_slice(),
1070                    "pixels changed across the round trip ({format:?})"
1071                );
1072            }
1073        }
1074
1075        /// BGR input must come back out of the decoder as RGB with the channels swapped
1076        /// (PNG has no BGR color type -- `bgr_to_rgb_swap` is what makes this correct).
1077        #[cfg(feature = "png")]
1078        #[test]
1079        fn encode_png_swaps_bgr_channels_before_writing() {
1080            use crate::image::decode::decode_raw_image_from_any_bytes;
1081
1082            let bgra = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160];
1083            let encoded = encode_png(&raw_u8(2, 2, RawImageFormat::BGRA8, bgra.clone()))
1084                .into_result()
1085                .expect("encode_png(BGRA8) failed");
1086            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
1087                .into_result()
1088                .expect("decode of our own BGRA8 PNG failed");
1089            assert_eq!(decoded.data_format, RawImageFormat::RGBA8);
1090            let want = bgr_to_rgb_swap(&bgra, RawImageFormat::BGRA8).expect("BGRA8 must swap");
1091            assert_eq!(
1092                decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
1093                want.as_slice()
1094            );
1095
1096            let bgr = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
1097            let encoded = encode_png(&raw_u8(2, 2, RawImageFormat::BGR8, bgr.clone()))
1098                .into_result()
1099                .expect("encode_png(BGR8) failed");
1100            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
1101                .into_result()
1102                .expect("decode of our own BGR8 PNG failed");
1103            assert_eq!(decoded.data_format, RawImageFormat::RGB8);
1104            let want = bgr_to_rgb_swap(&bgr, RawImageFormat::BGR8).expect("BGR8 must swap");
1105            assert_eq!(
1106                decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
1107                want.as_slice()
1108            );
1109        }
1110
1111        /// Non-8-bit payloads have no `get_u8_vec_ref()`, so they must be rejected as
1112        /// `InvalidData` -- even though `translate_rawimage_colortype` happily maps them.
1113        #[cfg(feature = "png")]
1114        #[test]
1115        fn encode_png_rejects_16_bit_and_float_payloads() {
1116            let u16_img = RawImage {
1117                pixels: RawImageData::U16(vec![0u16; 4].into()),
1118                width: 2,
1119                height: 2,
1120                premultiplied_alpha: false,
1121                data_format: RawImageFormat::R16,
1122                tag: Vec::<u8>::new().into(),
1123            };
1124            assert_eq!(err_of(&encode_png(&u16_img)), EncodeImageError::InvalidData);
1125
1126            let f32_img = RawImage {
1127                pixels: RawImageData::F32(vec![0.0f32, f32::NAN, f32::INFINITY, f32::MIN].into()),
1128                width: 2,
1129                height: 2,
1130                premultiplied_alpha: false,
1131                data_format: RawImageFormat::RGBAF32,
1132                tag: Vec::<u8>::new().into(),
1133            };
1134            assert_eq!(err_of(&encode_png(&f32_img)), EncodeImageError::InvalidData);
1135        }
1136
1137        #[cfg(all(feature = "png", target_pointer_width = "64"))]
1138        #[test]
1139        fn encode_png_rejects_dimensions_that_overflow_u32() {
1140            let too_wide = raw_u8(usize::MAX, 1, RawImageFormat::RGBA8, vec![0; 4]);
1141            assert_eq!(err_of(&encode_png(&too_wide)), EncodeImageError::DimensionError);
1142
1143            let too_tall = raw_u8(1, (u32::MAX as usize) + 1, RawImageFormat::RGBA8, vec![0; 4]);
1144            assert_eq!(err_of(&encode_png(&too_tall)), EncodeImageError::DimensionError);
1145        }
1146
1147        #[cfg(feature = "png")]
1148        #[test]
1149        fn encode_png_rejects_zero_dimensions() {
1150            let zero = raw_u8(0, 0, RawImageFormat::RGBA8, Vec::new());
1151            assert!(encode_png(&zero).is_err(), "a 0x0 PNG is not a valid image");
1152
1153            let zero_width = raw_u8(0, 4, RawImageFormat::RGBA8, Vec::new());
1154            assert!(encode_png(&zero_width).is_err());
1155        }
1156
1157        /// ADVERSARIAL: `image`'s `PngEncoder::write_image` *asserts* that the buffer length
1158        /// matches `width * height * bpp`, and `encode_png` forwards an unvalidated buffer.
1159        /// A short/long `RawImage` therefore aborts the process instead of returning `Err`.
1160        /// The invariant asserted here is the weaker one that always holds: it must never
1161        /// report success. See the report for the underlying bug.
1162        #[cfg(feature = "png")]
1163        #[test]
1164        fn encode_png_never_succeeds_on_a_buffer_of_the_wrong_length() {
1165            for (w, h, format, len) in [
1166                (4usize, 4usize, RawImageFormat::RGBA8, 3usize), // far too short
1167                (2, 2, RawImageFormat::RGBA8, 15),               // one byte short
1168                (2, 2, RawImageFormat::RGBA8, 17),               // one byte long
1169                (2, 2, RawImageFormat::R8, 0),                   // empty, non-zero dimensions
1170            ] {
1171                let outcome = std::panic::catch_unwind(move || {
1172                    encode_png(&raw_u8(w, h, format, vec![0u8; len])).is_ok()
1173                });
1174                assert!(
1175                    !matches!(outcome, Ok(true)),
1176                    "encode_png claimed success for {w}x{h} {format:?} with {len} byte(s)"
1177                );
1178            }
1179        }
1180
1181        #[cfg(not(feature = "png"))]
1182        #[test]
1183        fn encode_png_without_the_feature_always_reports_encoder_not_available() {
1184            for img in [
1185                raw_u8(2, 2, RawImageFormat::RGBA8, vec![0; 16]),
1186                raw_u8(0, 0, RawImageFormat::RGBA8, Vec::new()),
1187                raw_u8(usize::MAX, usize::MAX, RawImageFormat::BGRA8, vec![0; 3]),
1188            ] {
1189                assert_eq!(err_of(&encode_png(&img)), EncodeImageError::EncoderNotAvailable);
1190            }
1191        }
1192
1193        // --- encode_jpeg (numeric: `quality`) ------------------------------------
1194
1195        /// `JpegEncoder::new_with_quality` clamps the quality to 1..=100 internally, so the
1196        /// out-of-range ends must not panic and must be indistinguishable from the clamped value.
1197        #[cfg(feature = "jpeg")]
1198        #[test]
1199        fn encode_jpeg_clamps_quality_at_both_ends() {
1200            let pixels = (0..(4 * 4 * 3)).map(|i| (i * 5) as u8).collect::<Vec<_>>();
1201            let at = |q: u8| {
1202                encode_jpeg(&raw_u8(4, 4, RawImageFormat::RGB8, pixels.clone()), q)
1203                    .into_result()
1204                    .unwrap_or_else(|e| panic!("encode_jpeg(quality = {q}) failed: {e}"))
1205                    .as_slice()
1206                    .to_vec()
1207            };
1208
1209            assert_eq!(at(u8::MIN), at(1), "quality 0 must clamp to 1");
1210            assert_eq!(at(u8::MAX), at(100), "quality 255 must clamp to 100");
1211            assert_ne!(at(1), at(100), "quality must actually affect the output");
1212
1213            for q in [0u8, 1, 50, 100, 101, 254, 255] {
1214                let bytes = at(q);
1215                assert_eq!(&bytes[..2], &[0xFF, 0xD8], "missing JPEG SOI marker (quality {q})");
1216                assert_eq!(
1217                    &bytes[bytes.len() - 2..],
1218                    &[0xFF, 0xD9],
1219                    "missing JPEG EOI marker (quality {q})"
1220                );
1221            }
1222        }
1223
1224        #[cfg(feature = "jpeg")]
1225        #[test]
1226        fn encode_jpeg_round_trips_dimensions_through_the_decoder() {
1227            use crate::image::decode::decode_raw_image_from_any_bytes;
1228
1229            let pixels = (0..(8 * 8 * 3)).map(|i| (i % 256) as u8).collect::<Vec<_>>();
1230            let encoded = encode_jpeg(&raw_u8(8, 8, RawImageFormat::RGB8, pixels), 90)
1231                .into_result()
1232                .expect("encode_jpeg(RGB8) failed");
1233            // JPEG is lossy, so only the geometry/format survives -- not the exact pixels.
1234            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
1235                .into_result()
1236                .expect("decode of our own JPEG failed");
1237            assert_eq!(decoded.width, 8);
1238            assert_eq!(decoded.height, 8);
1239            assert_eq!(decoded.data_format, RawImageFormat::RGB8);
1240            assert_eq!(decoded.pixels.get_u8_vec_ref().expect("8-bit data").len(), 8 * 8 * 3);
1241        }
1242
1243        /// The JPEG encoder only supports L8 and Rgb8. Everything else comes back as an
1244        /// `Unsupported` image error, which the wrapper flattens to `Unknown`.
1245        #[cfg(feature = "jpeg")]
1246        #[test]
1247        fn encode_jpeg_rejects_color_types_it_cannot_write() {
1248            for (format, bpp) in [
1249                (RawImageFormat::RGBA8, 4usize),
1250                (RawImageFormat::BGRA8, 4),
1251                (RawImageFormat::RG8, 2),
1252            ] {
1253                let img = raw_u8(2, 2, format, vec![7u8; 2 * 2 * bpp]);
1254                assert_eq!(
1255                    err_of(&encode_jpeg(&img, 80)),
1256                    EncodeImageError::Unknown,
1257                    "unexpected error for {format:?}"
1258                );
1259            }
1260            // ... while the two supported ones do encode.
1261            assert!(encode_jpeg(&raw_u8(2, 2, RawImageFormat::R8, vec![1; 4]), 80).is_ok());
1262            assert!(encode_jpeg(&raw_u8(2, 2, RawImageFormat::RGB8, vec![1; 12]), 80).is_ok());
1263        }
1264
1265        #[cfg(feature = "jpeg")]
1266        #[test]
1267        fn encode_jpeg_rejects_zero_and_u16_max_plus_one_dimensions() {
1268            // JPEG dimensions are u16 in the frame header: 0 and > 65535 must both fail,
1269            // and must fail as an error rather than a panic.
1270            assert!(encode_jpeg(&raw_u8(0, 0, RawImageFormat::RGB8, Vec::new()), 75).is_err());
1271            assert!(encode_jpeg(&raw_u8(0, 4, RawImageFormat::RGB8, Vec::new()), 75).is_err());
1272
1273            let too_wide = raw_u8(65_536, 1, RawImageFormat::RGB8, vec![0u8; 65_536 * 3]);
1274            // NOTE: reported as `Unknown`, not `DimensionError` -- the wrapper only maps
1275            // `ImageError::Limits`, and the JPEG encoder raises an encoding error instead.
1276            assert_eq!(err_of(&encode_jpeg(&too_wide, 75)), EncodeImageError::Unknown);
1277        }
1278
1279        #[cfg(all(feature = "jpeg", target_pointer_width = "64"))]
1280        #[test]
1281        fn encode_jpeg_rejects_dimensions_that_overflow_u32() {
1282            let too_wide = raw_u8(usize::MAX, 1, RawImageFormat::RGB8, vec![0; 3]);
1283            assert_eq!(err_of(&encode_jpeg(&too_wide, 75)), EncodeImageError::DimensionError);
1284
1285            let too_tall = raw_u8(1, (u32::MAX as usize) + 1, RawImageFormat::RGB8, vec![0; 3]);
1286            assert_eq!(err_of(&encode_jpeg(&too_tall, 0)), EncodeImageError::DimensionError);
1287        }
1288
1289        #[cfg(feature = "jpeg")]
1290        #[test]
1291        fn encode_jpeg_rejects_16_bit_payloads_before_looking_at_quality() {
1292            let u16_img = RawImage {
1293                pixels: RawImageData::U16(vec![u16::MAX; 12].into()),
1294                width: 2,
1295                height: 2,
1296                premultiplied_alpha: false,
1297                data_format: RawImageFormat::RGB16,
1298                tag: Vec::<u8>::new().into(),
1299            };
1300            for q in [0u8, 50, 255] {
1301                assert_eq!(err_of(&encode_jpeg(&u16_img, q)), EncodeImageError::InvalidData);
1302            }
1303        }
1304
1305        /// Same unvalidated-buffer hazard as `encode_png`, at every quality boundary.
1306        #[cfg(feature = "jpeg")]
1307        #[test]
1308        fn encode_jpeg_never_succeeds_on_a_buffer_of_the_wrong_length() {
1309            for q in [0u8, 1, 100, 255] {
1310                for (w, h, len) in [(4usize, 4usize, 3usize), (2, 2, 11), (2, 2, 13)] {
1311                    let outcome = std::panic::catch_unwind(move || {
1312                        encode_jpeg(&raw_u8(w, h, RawImageFormat::RGB8, vec![0u8; len]), q).is_ok()
1313                    });
1314                    assert!(
1315                        !matches!(outcome, Ok(true)),
1316                        "encode_jpeg claimed success for {w}x{h} RGB8, {len} byte(s), quality {q}"
1317                    );
1318                }
1319            }
1320        }
1321
1322        #[cfg(not(feature = "jpeg"))]
1323        #[test]
1324        fn encode_jpeg_without_the_feature_always_reports_encoder_not_available() {
1325            // Every quality boundary, including the ones the real encoder would clamp.
1326            for q in [u8::MIN, 1, 50, 100, 101, 254, u8::MAX] {
1327                let img = raw_u8(2, 2, RawImageFormat::RGB8, vec![0; 12]);
1328                assert_eq!(err_of(&encode_jpeg(&img, q)), EncodeImageError::EncoderNotAvailable);
1329            }
1330            // ... and for images that the real encoder would reject outright.
1331            let broken = raw_u8(usize::MAX, 0, RawImageFormat::RGBAF32, Vec::new());
1332            assert_eq!(err_of(&encode_jpeg(&broken, 0)), EncodeImageError::EncoderNotAvailable);
1333        }
1334    }
1335}