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")]
8
pub 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
24
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30
24
            match self {
31
6
                Self::InsufficientMemory => write!(
32
6
                    f,
33
6
                    "Error decoding image: Not enough memory available to perform encoding \
34
6
                     operation"
35
                ),
36
                Self::DimensionError => {
37
6
                    write!(f, "Error decoding image: Wrong dimensions")
38
                }
39
                Self::UnsupportedImageFormat => {
40
6
                    write!(f, "Error decoding image: Invalid data format")
41
                }
42
6
                Self::Unknown => write!(f, "Error decoding image: Unknown error"),
43
            }
44
24
        }
45
    }
46

            
47
283
    fn translate_image_error_decode(i: ImageError) -> DecodeImageError {
48
283
        match i {
49
3
            ImageError::Limits(l) => match l.kind() {
50
1
                LimitErrorKind::InsufficientMemory => DecodeImageError::InsufficientMemory,
51
1
                LimitErrorKind::DimensionError => DecodeImageError::DimensionError,
52
1
                _ => DecodeImageError::Unknown,
53
            },
54
280
            _ => DecodeImageError::Unknown,
55
        }
56
283
    }
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
275
    #[must_use] pub fn decode_raw_image_from_any_bytes(image_bytes: &[u8]) -> ResultRawImageDecodeImageError {
71
        use azul_core::resources::RawImageData;
72

            
73
275
        let image_format = match image::guess_format(image_bytes) {
74
8
            Ok(o) => o,
75
267
            Err(e) => {
76
267
                return ResultRawImageDecodeImageError::Err(translate_image_error_decode(e));
77
            }
78
        };
79

            
80
8
        let decoded = match image::load_from_memory_with_format(image_bytes, image_format) {
81
            Ok(o) => o,
82
8
            Err(e) => {
83
8
                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
275
    }
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")]
438
pub 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
25
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475
            use self::EncodeImageError::{EncoderNotAvailable, InsufficientMemory, DimensionError, InvalidData, Unknown};
476
25
            match self {
477
5
                EncoderNotAvailable => write!(
478
5
                    f,
479
5
                    "Missing encoder (library was not compiled with given codec)"
480
                ),
481
5
                InsufficientMemory => write!(
482
5
                    f,
483
5
                    "Error encoding image: Not enough memory available to perform encoding \
484
5
                     operation"
485
                ),
486
5
                DimensionError => write!(f, "Error encoding image: Wrong dimensions"),
487
5
                InvalidData => write!(f, "Error encoding image: Invalid data format"),
488
5
                Unknown => write!(f, "Error encoding image: Unknown error"),
489
            }
490
25
        }
491
    }
492

            
493
28
    const fn translate_rawimage_colortype(i: RawImageFormat) -> image::ColorType {
494
28
        match i {
495
2
            RawImageFormat::R8 => image::ColorType::L8,
496
2
            RawImageFormat::RG8 => image::ColorType::La8,
497
6
            RawImageFormat::RGB8 | RawImageFormat::BGR8 => image::ColorType::Rgb8,
498
6
            RawImageFormat::RGBA8 | RawImageFormat::BGRA8 => image::ColorType::Rgba8,
499
2
            RawImageFormat::R16 => image::ColorType::L16,
500
2
            RawImageFormat::RG16 => image::ColorType::La16,
501
2
            RawImageFormat::RGB16 => image::ColorType::Rgb16,
502
2
            RawImageFormat::RGBA16 => image::ColorType::Rgba16,
503
2
            RawImageFormat::RGBF32 => image::ColorType::Rgb32F,
504
2
            RawImageFormat::RGBAF32 => image::ColorType::Rgba32F,
505
        }
506
28
    }
507

            
508
293
    fn bgr_to_rgb_swap(pixels: &[u8], format: RawImageFormat) -> Option<Vec<u8>> {
509
293
        match format {
510
            RawImageFormat::BGR8 => {
511
137
                let mut out = pixels.to_vec();
512
334642
                for chunk in out.chunks_exact_mut(3) {
513
334642
                    chunk.swap(0, 2);
514
334642
                }
515
137
                Some(out)
516
            }
517
            RawImageFormat::BGRA8 => {
518
136
                let mut out = pixels.to_vec();
519
964
                for chunk in out.chunks_exact_mut(4) {
520
964
                    chunk.swap(0, 2);
521
964
                }
522
136
                Some(out)
523
            }
524
20
            _ => None,
525
        }
526
293
    }
527

            
528
8
    fn translate_image_error_encode(i: ImageError) -> EncodeImageError {
529
8
        match i {
530
3
            ImageError::Limits(l) => match l.kind() {
531
1
                LimitErrorKind::InsufficientMemory => EncodeImageError::InsufficientMemory,
532
1
                LimitErrorKind::DimensionError => EncodeImageError::DimensionError,
533
1
                _ => EncodeImageError::Unknown,
534
            },
535
5
            _ => EncodeImageError::Unknown,
536
        }
537
8
    }
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
3
    #[must_use] pub const fn encode_png(image: &RawImage) -> ResultU8VecEncodeImageError {
646
3
        ResultU8VecEncodeImageError::Err(EncodeImageError::EncoderNotAvailable)
647
3
    }
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
8
    #[must_use] pub const fn encode_jpeg(image: &RawImage, quality: u8) -> ResultU8VecEncodeImageError {
689
8
        ResultU8VecEncodeImageError::Err(EncodeImageError::EncoderNotAvailable)
690
8
    }
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
                ))),
                EncodeImageError::DimensionError
            );
            assert_eq!(
                translate_image_error_encode(ImageError::Limits(LimitError::from_kind(
                    LimitErrorKind::Unsupported {
                        limits: image::Limits::default(),
                        supported: image::LimitSupport::default(),
                    }
                ))),
                EncodeImageError::Unknown
            );
        }
        #[test]
        fn translate_maps_every_other_variant_to_unknown() {
            let cases = vec![
                ImageError::IoError(std::io::Error::other("boom")),
                ImageError::Encoding(EncodingError::new(ImageFormatHint::Unknown, "bad pixel")),
                ImageError::Decoding(DecodingError::new(ImageFormatHint::Unknown, "bad chunk")),
                // An unsupported *color type* (what the JPEG encoder returns for RGBA8) is
                // flattened to `Unknown`, never to `InvalidData`.
                ImageError::Unsupported(UnsupportedError::from_format_and_kind(
                    ImageFormatHint::Unknown,
                    UnsupportedErrorKind::Format(ImageFormatHint::Unknown),
                )),
                // ... and a parameter dimension mismatch is NOT mapped to DimensionError.
                ImageError::Parameter(ParameterError::from_kind(
                    ParameterErrorKind::DimensionMismatch,
                )),
            ];
            for c in cases {
                let debug = format!("{c:?}");
                assert_eq!(
                    translate_image_error_encode(c),
                    EncodeImageError::Unknown,
                    "unexpected mapping for {debug}"
                );
            }
        }
        // --- encode_png ----------------------------------------------------------
        #[cfg(feature = "png")]
        #[test]
        fn encode_png_round_trips_through_the_decoder() {
            use crate::image::decode::decode_raw_image_from_any_bytes;
            for (format, bpp) in [
                (RawImageFormat::R8, 1usize),
                (RawImageFormat::RGB8, 3),
                (RawImageFormat::RGBA8, 4),
            ] {
                let pixels = (0..(2 * 2 * bpp)).map(|i| (i * 7 + 1) as u8).collect::<Vec<_>>();
                let encoded = encode_png(&raw_u8(2, 2, format, pixels.clone()))
                    .into_result()
                    .unwrap_or_else(|e| panic!("encode_png({format:?}) failed: {e}"));
                let bytes = encoded.as_slice();
                assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n", "missing PNG signature");
                let decoded = decode_raw_image_from_any_bytes(bytes)
                    .into_result()
                    .unwrap_or_else(|e| panic!("decode of our own PNG ({format:?}) failed: {e}"));
                assert_eq!(decoded.width, 2);
                assert_eq!(decoded.height, 2);
                assert_eq!(decoded.data_format, format, "format changed across the round trip");
                assert!(!decoded.premultiplied_alpha);
                assert_eq!(
                    decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
                    pixels.as_slice(),
                    "pixels changed across the round trip ({format:?})"
                );
            }
        }
        /// BGR input must come back out of the decoder as RGB with the channels swapped
        /// (PNG has no BGR color type -- `bgr_to_rgb_swap` is what makes this correct).
        #[cfg(feature = "png")]
        #[test]
        fn encode_png_swaps_bgr_channels_before_writing() {
            use crate::image::decode::decode_raw_image_from_any_bytes;
            let bgra = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160];
            let encoded = encode_png(&raw_u8(2, 2, RawImageFormat::BGRA8, bgra.clone()))
                .into_result()
                .expect("encode_png(BGRA8) failed");
            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
                .into_result()
                .expect("decode of our own BGRA8 PNG failed");
            assert_eq!(decoded.data_format, RawImageFormat::RGBA8);
            let want = bgr_to_rgb_swap(&bgra, RawImageFormat::BGRA8).expect("BGRA8 must swap");
            assert_eq!(
                decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
                want.as_slice()
            );
            let bgr = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
            let encoded = encode_png(&raw_u8(2, 2, RawImageFormat::BGR8, bgr.clone()))
                .into_result()
                .expect("encode_png(BGR8) failed");
            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
                .into_result()
                .expect("decode of our own BGR8 PNG failed");
            assert_eq!(decoded.data_format, RawImageFormat::RGB8);
            let want = bgr_to_rgb_swap(&bgr, RawImageFormat::BGR8).expect("BGR8 must swap");
            assert_eq!(
                decoded.pixels.get_u8_vec_ref().expect("8-bit data").as_slice(),
                want.as_slice()
            );
        }
        /// Non-8-bit payloads have no `get_u8_vec_ref()`, so they must be rejected as
        /// `InvalidData` -- even though `translate_rawimage_colortype` happily maps them.
        #[cfg(feature = "png")]
        #[test]
        fn encode_png_rejects_16_bit_and_float_payloads() {
            let u16_img = RawImage {
                pixels: RawImageData::U16(vec![0u16; 4].into()),
                width: 2,
                height: 2,
                premultiplied_alpha: false,
                data_format: RawImageFormat::R16,
                tag: Vec::<u8>::new().into(),
            };
            assert_eq!(err_of(&encode_png(&u16_img)), EncodeImageError::InvalidData);
            let f32_img = RawImage {
                pixels: RawImageData::F32(vec![0.0f32, f32::NAN, f32::INFINITY, f32::MIN].into()),
                width: 2,
                height: 2,
                premultiplied_alpha: false,
                data_format: RawImageFormat::RGBAF32,
                tag: Vec::<u8>::new().into(),
            };
            assert_eq!(err_of(&encode_png(&f32_img)), EncodeImageError::InvalidData);
        }
        #[cfg(all(feature = "png", target_pointer_width = "64"))]
        #[test]
        fn encode_png_rejects_dimensions_that_overflow_u32() {
            let too_wide = raw_u8(usize::MAX, 1, RawImageFormat::RGBA8, vec![0; 4]);
            assert_eq!(err_of(&encode_png(&too_wide)), EncodeImageError::DimensionError);
            let too_tall = raw_u8(1, (u32::MAX as usize) + 1, RawImageFormat::RGBA8, vec![0; 4]);
            assert_eq!(err_of(&encode_png(&too_tall)), EncodeImageError::DimensionError);
        }
        #[cfg(feature = "png")]
        #[test]
        fn encode_png_rejects_zero_dimensions() {
            let zero = raw_u8(0, 0, RawImageFormat::RGBA8, Vec::new());
            assert!(encode_png(&zero).is_err(), "a 0x0 PNG is not a valid image");
            let zero_width = raw_u8(0, 4, RawImageFormat::RGBA8, Vec::new());
            assert!(encode_png(&zero_width).is_err());
        }
        /// ADVERSARIAL: `image`'s `PngEncoder::write_image` *asserts* that the buffer length
        /// matches `width * height * bpp`, and `encode_png` forwards an unvalidated buffer.
        /// A short/long `RawImage` therefore aborts the process instead of returning `Err`.
        /// The invariant asserted here is the weaker one that always holds: it must never
        /// report success. See the report for the underlying bug.
        #[cfg(feature = "png")]
        #[test]
        fn encode_png_never_succeeds_on_a_buffer_of_the_wrong_length() {
            for (w, h, format, len) in [
                (4usize, 4usize, RawImageFormat::RGBA8, 3usize), // far too short
                (2, 2, RawImageFormat::RGBA8, 15),               // one byte short
                (2, 2, RawImageFormat::RGBA8, 17),               // one byte long
                (2, 2, RawImageFormat::R8, 0),                   // empty, non-zero dimensions
            ] {
                let outcome = std::panic::catch_unwind(move || {
                    encode_png(&raw_u8(w, h, format, vec![0u8; len])).is_ok()
                });
                assert!(
                    !matches!(outcome, Ok(true)),
                    "encode_png claimed success for {w}x{h} {format:?} with {len} byte(s)"
                );
            }
        }
        #[cfg(not(feature = "png"))]
        #[test]
        fn encode_png_without_the_feature_always_reports_encoder_not_available() {
            for img in [
                raw_u8(2, 2, RawImageFormat::RGBA8, vec![0; 16]),
                raw_u8(0, 0, RawImageFormat::RGBA8, Vec::new()),
                raw_u8(usize::MAX, usize::MAX, RawImageFormat::BGRA8, vec![0; 3]),
            ] {
                assert_eq!(err_of(&encode_png(&img)), EncodeImageError::EncoderNotAvailable);
            }
        }
        // --- encode_jpeg (numeric: `quality`) ------------------------------------
        /// `JpegEncoder::new_with_quality` clamps the quality to 1..=100 internally, so the
        /// out-of-range ends must not panic and must be indistinguishable from the clamped value.
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_clamps_quality_at_both_ends() {
            let pixels = (0..(4 * 4 * 3)).map(|i| (i * 5) as u8).collect::<Vec<_>>();
            let at = |q: u8| {
                encode_jpeg(&raw_u8(4, 4, RawImageFormat::RGB8, pixels.clone()), q)
                    .into_result()
                    .unwrap_or_else(|e| panic!("encode_jpeg(quality = {q}) failed: {e}"))
                    .as_slice()
                    .to_vec()
            };
            assert_eq!(at(u8::MIN), at(1), "quality 0 must clamp to 1");
            assert_eq!(at(u8::MAX), at(100), "quality 255 must clamp to 100");
            assert_ne!(at(1), at(100), "quality must actually affect the output");
            for q in [0u8, 1, 50, 100, 101, 254, 255] {
                let bytes = at(q);
                assert_eq!(&bytes[..2], &[0xFF, 0xD8], "missing JPEG SOI marker (quality {q})");
                assert_eq!(
                    &bytes[bytes.len() - 2..],
                    &[0xFF, 0xD9],
                    "missing JPEG EOI marker (quality {q})"
                );
            }
        }
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_round_trips_dimensions_through_the_decoder() {
            use crate::image::decode::decode_raw_image_from_any_bytes;
            let pixels = (0..(8 * 8 * 3)).map(|i| (i % 256) as u8).collect::<Vec<_>>();
            let encoded = encode_jpeg(&raw_u8(8, 8, RawImageFormat::RGB8, pixels), 90)
                .into_result()
                .expect("encode_jpeg(RGB8) failed");
            // JPEG is lossy, so only the geometry/format survives -- not the exact pixels.
            let decoded = decode_raw_image_from_any_bytes(encoded.as_slice())
                .into_result()
                .expect("decode of our own JPEG failed");
            assert_eq!(decoded.width, 8);
            assert_eq!(decoded.height, 8);
            assert_eq!(decoded.data_format, RawImageFormat::RGB8);
            assert_eq!(decoded.pixels.get_u8_vec_ref().expect("8-bit data").len(), 8 * 8 * 3);
        }
        /// The JPEG encoder only supports L8 and Rgb8. Everything else comes back as an
        /// `Unsupported` image error, which the wrapper flattens to `Unknown`.
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_rejects_color_types_it_cannot_write() {
            for (format, bpp) in [
                (RawImageFormat::RGBA8, 4usize),
                (RawImageFormat::BGRA8, 4),
                (RawImageFormat::RG8, 2),
            ] {
                let img = raw_u8(2, 2, format, vec![7u8; 2 * 2 * bpp]);
                assert_eq!(
                    err_of(&encode_jpeg(&img, 80)),
                    EncodeImageError::Unknown,
                    "unexpected error for {format:?}"
                );
            }
            // ... while the two supported ones do encode.
            assert!(encode_jpeg(&raw_u8(2, 2, RawImageFormat::R8, vec![1; 4]), 80).is_ok());
            assert!(encode_jpeg(&raw_u8(2, 2, RawImageFormat::RGB8, vec![1; 12]), 80).is_ok());
        }
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_rejects_zero_and_u16_max_plus_one_dimensions() {
            // JPEG dimensions are u16 in the frame header: 0 and > 65535 must both fail,
            // and must fail as an error rather than a panic.
            assert!(encode_jpeg(&raw_u8(0, 0, RawImageFormat::RGB8, Vec::new()), 75).is_err());
            assert!(encode_jpeg(&raw_u8(0, 4, RawImageFormat::RGB8, Vec::new()), 75).is_err());
            let too_wide = raw_u8(65_536, 1, RawImageFormat::RGB8, vec![0u8; 65_536 * 3]);
            // NOTE: reported as `Unknown`, not `DimensionError` -- the wrapper only maps
            // `ImageError::Limits`, and the JPEG encoder raises an encoding error instead.
            assert_eq!(err_of(&encode_jpeg(&too_wide, 75)), EncodeImageError::Unknown);
        }
        #[cfg(all(feature = "jpeg", target_pointer_width = "64"))]
        #[test]
        fn encode_jpeg_rejects_dimensions_that_overflow_u32() {
            let too_wide = raw_u8(usize::MAX, 1, RawImageFormat::RGB8, vec![0; 3]);
            assert_eq!(err_of(&encode_jpeg(&too_wide, 75)), EncodeImageError::DimensionError);
            let too_tall = raw_u8(1, (u32::MAX as usize) + 1, RawImageFormat::RGB8, vec![0; 3]);
            assert_eq!(err_of(&encode_jpeg(&too_tall, 0)), EncodeImageError::DimensionError);
        }
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_rejects_16_bit_payloads_before_looking_at_quality() {
            let u16_img = RawImage {
                pixels: RawImageData::U16(vec![u16::MAX; 12].into()),
                width: 2,
                height: 2,
                premultiplied_alpha: false,
                data_format: RawImageFormat::RGB16,
                tag: Vec::<u8>::new().into(),
            };
            for q in [0u8, 50, 255] {
                assert_eq!(err_of(&encode_jpeg(&u16_img, q)), EncodeImageError::InvalidData);
            }
        }
        /// Same unvalidated-buffer hazard as `encode_png`, at every quality boundary.
        #[cfg(feature = "jpeg")]
        #[test]
        fn encode_jpeg_never_succeeds_on_a_buffer_of_the_wrong_length() {
            for q in [0u8, 1, 100, 255] {
                for (w, h, len) in [(4usize, 4usize, 3usize), (2, 2, 11), (2, 2, 13)] {
                    let outcome = std::panic::catch_unwind(move || {
                        encode_jpeg(&raw_u8(w, h, RawImageFormat::RGB8, vec![0u8; len]), q).is_ok()
                    });
                    assert!(
                        !matches!(outcome, Ok(true)),
                        "encode_jpeg claimed success for {w}x{h} RGB8, {len} byte(s), quality {q}"
                    );
                }
            }
        }
        #[cfg(not(feature = "jpeg"))]
        #[test]
        fn encode_jpeg_without_the_feature_always_reports_encoder_not_available() {
            // Every quality boundary, including the ones the real encoder would clamp.
            for q in [u8::MIN, 1, 50, 100, 101, 254, u8::MAX] {
                let img = raw_u8(2, 2, RawImageFormat::RGB8, vec![0; 12]);
                assert_eq!(err_of(&encode_jpeg(&img, q)), EncodeImageError::EncoderNotAvailable);
            }
            // ... and for images that the real encoder would reject outright.
            let broken = raw_u8(usize::MAX, 0, RawImageFormat::RGBAF32, Vec::new());
            assert_eq!(err_of(&encode_jpeg(&broken, 0)), EncodeImageError::EncoderNotAvailable);
        }
    }
}