rust_xlsxwriter 0.99.1

A Rust library for writing Excel 2007 xlsx files
Documentation
// Image unit tests.
//
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright 2022-2026, John McNamara, jmcnamara@cpan.org

#[cfg(test)]
mod image_tests {

    use crate::XlsxError;

    use crate::Image;

    #[test]
    fn test_images() {
        let image_test_data = vec![
            // Name, width, height, width_dpi, height_dpi, type.
            ("black_150.jpg", 64, 64, 150.0, 150.0, "jpeg"),
            ("black_300.jpg", 64, 64, 300.0, 300.0, "jpeg"),
            ("black_300.png", 64, 64, 299.9994, 299.9994, "png"),
            ("black_300e.png", 64, 64, 299.9994, 299.9994, "png"),
            ("black_40000x45000.gif", 40_000, 45_000, 96.0, 96.0, "gif"),
            ("black_72.jpg", 64, 64, 72.0, 72.0, "jpeg"),
            ("black_72.png", 64, 64, 72.009, 72.009, "png"),
            ("black_72e.png", 64, 64, 72.009, 72.009, "png"),
            ("black_96.jpg", 64, 64, 96.0, 96.0, "jpeg"),
            ("black_96.png", 64, 64, 96.012, 96.012, "png"),
            ("blue.jpg", 23, 23, 96.0, 96.0, "jpeg"),
            ("blue.png", 23, 23, 96.0, 96.0, "png"),
            ("grey.jpg", 99, 69, 96.0, 96.0, "jpeg"),
            ("grey.png", 99, 69, 96.0, 96.0, "png"),
            ("happy.jpg", 423, 563, 96.0, 96.0, "jpeg"),
            ("issue32.png", 115, 115, 96.0, 96.0, "png"),
            ("logo.gif", 200, 80, 96.0, 96.0, "gif"),
            ("logo.jpg", 200, 80, 96.0, 96.0, "jpeg"),
            ("logo.png", 200, 80, 96.0, 96.0, "png"),
            ("mylogo.png", 215, 36, 95.9866, 95.9866, "png"),
            ("red.bmp", 32, 32, 96.0, 96.0, "bmp"),
            ("red.gif", 32, 32, 96.0, 96.0, "gif"),
            ("red.jpg", 32, 32, 96.0, 96.0, "jpeg"),
            ("red.png", 32, 32, 96.0, 96.0, "png"),
            ("red2.png", 32, 32, 96.0, 96.0, "png"),
            ("red_208.png", 208, 49, 96.0, 96.0, "png"),
            ("red_64x20.png", 64, 20, 96.0, 96.0, "png"),
            ("red_readonly.png", 32, 32, 96.0, 96.0, "png"),
            ("red_topdown_20x12.bmp", 20, 12, 96.0, 96.0, "bmp"),
            ("red_topdown_32x32.bmp", 32, 32, 96.0, 96.0, "bmp"),
            ("train.jpg", 640, 480, 96.0, 96.0, "jpeg"),
            ("watermark.png", 1778, 1003, 329.9968, 329.9968, "png"),
            ("yellow.jpg", 72, 72, 96.0, 96.0, "jpeg"),
            ("yellow.png", 72, 72, 96.0, 96.0, "png"),
            ("zero_dpi.jpg", 11, 16, 96.0, 96.0, "jpeg"),
            (
                "black_150.png",
                64,
                64,
                150.01239999999999,
                150.01239999999999,
                "png",
            ),
            (
                "black_150e.png",
                64,
                64,
                150.01239999999999,
                150.01239999999999,
                "png",
            ),
        ];

        for test_data in image_test_data {
            let (filename, width, height, width_dpi, height_dpi, image_type) = test_data;
            let filename = format!("tests/input/images/{filename}");

            let image = Image::new(&filename).unwrap();
            assert_eq!(width as f64, image.width());
            assert_eq!(height as f64, image.height());
            assert_eq!(width_dpi, image.width_dpi());
            assert_eq!(height_dpi, image.height_dpi());
            assert_eq!(image_type, image.image_type.extension());
        }
    }

    #[test]
    fn unknown_file_format() {
        let filename = "tests/input/images/unknown.img".to_string();

        let image = Image::new(filename);
        assert!(matches!(image, Err(XlsxError::UnknownImageType)));
    }

    #[test]
    fn invalid_file_format() {
        let filename = "tests/input/images/no_dimensions.png".to_string();

        let image = Image::new(filename);
        assert!(matches!(image, Err(XlsxError::ImageDimensionError)));
    }

    #[test]
    fn truncated_image_data() {
        // Truncated or tiny buffers should return an error rather than panic on
        // the fixed-offset header reads.
        let cases: Vec<Vec<u8>> = vec![
            vec![],                                               // Empty.
            vec![0x42, 0x4D, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],      // "BM" + junk.
            vec![0u8; 25],                                        // Below the guard.
            vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A], // PNG sig only.
        ];

        for data in cases {
            let image = Image::new_from_buffer(&data);
            assert!(matches!(image, Err(XlsxError::UnknownImageType)));
        }
    }

    #[test]
    fn truncated_image_loop_data() {
        // Tests to check for truncated data in PNG and JPEG loops.

        // PNG truncated 1 byte into a chunk header.
        let mut png_short_marker = Vec::new();
        png_short_marker.extend_from_slice(&[0x89]);
        png_short_marker.extend_from_slice(b"PNG\r\n\x1a\n");
        png_short_marker.extend_from_slice(&13u32.to_be_bytes());
        png_short_marker.extend_from_slice(b"IHDR");
        png_short_marker.extend_from_slice(&[0; 17]); // Payload + CRC.
        png_short_marker.push(0); // 1 byte of the next chunk header.

        // PNG where "IHDR" isn't the first marker and is truncated mid-payload.
        let mut png_short_ihdr = Vec::new();
        png_short_ihdr.extend_from_slice(&[0x89]);
        png_short_ihdr.extend_from_slice(b"PNG\r\n\x1a\n");
        png_short_ihdr.extend_from_slice(&2u32.to_be_bytes());
        png_short_ihdr.extend_from_slice(b"aaaa");
        png_short_ihdr.extend_from_slice(&[0; 10]);
        png_short_ihdr.extend_from_slice(b"IHDR");

        // PNG truncated just after a non-first pHYs marker.
        let mut png_short_phys = Vec::new();
        png_short_phys.extend_from_slice(&[0x89]);
        png_short_phys.extend_from_slice(b"PNG\r\n\x1a\n");
        png_short_phys.extend_from_slice(&2u32.to_be_bytes());
        png_short_phys.extend_from_slice(b"aaaa");
        png_short_phys.extend_from_slice(&[0; 6]); // Payload + CRC.
        png_short_phys.extend_from_slice(&9u32.to_be_bytes());
        png_short_phys.extend_from_slice(b"pHYs"); // Truncated after marker.

        // JPEG truncated mid-SOF0, after the length field.
        let mut jpg_short_sof = Vec::new();
        jpg_short_sof.extend_from_slice(&[0xFF, 0xD8]); // SOI.
        jpg_short_sof.extend_from_slice(&[0xFF, 0xE0, 0x00, 0x14]); // APP0.
        jpg_short_sof.extend_from_slice(&[0; 18]); // APP0 payload.
        jpg_short_sof.extend_from_slice(&[0xFF, 0xC0, 0x00, 0x11]); // SOF0.
        jpg_short_sof.push(8); // Precision byte only, then truncated.

        // JPEG truncated 1 byte into a segment marker.
        let mut jpg_short_marker = Vec::new();
        jpg_short_marker.extend_from_slice(&[0xFF, 0xD8]); // SOI.
        jpg_short_marker.extend_from_slice(&[0xFF, 0xE0, 0x00, 0x15]); // APP0.
        jpg_short_marker.extend_from_slice(&[0; 19]); // APP0 payload.
        jpg_short_marker.push(0xFF); // 1 byte of the next marker.

        // JPEG truncated in a non-first 0xFFE0 segment.
        let mut jpg_short_app0 = Vec::new();
        jpg_short_app0.extend_from_slice(&[0xFF, 0xD8]); // SOI.
        jpg_short_app0.extend_from_slice(&[0xFF, 0xFE, 0x00, 0x14]); // COM.
        jpg_short_app0.extend_from_slice(&[0; 18]); // COM payload.
        jpg_short_app0.extend_from_slice(&[0xFF, 0xE0, 0x00, 0x14]); // APP0.
        jpg_short_app0.extend_from_slice(&[0; 5]); // Truncated payload.

        // PNG with a length large enough to overflow the 32 bit offset
        // calculation.
        let mut png_overflow = Vec::new();
        png_overflow.extend_from_slice(&[0x89]);
        png_overflow.extend_from_slice(b"PNG\r\n\x1a\n");
        png_overflow.extend_from_slice(&u32::MAX.to_be_bytes());
        png_overflow.extend_from_slice(b"aaaa");
        png_overflow.extend_from_slice(&[0; 10]);

        let cases = vec![
            png_short_marker,
            png_short_ihdr,
            png_short_phys,
            jpg_short_sof,
            jpg_short_marker,
            jpg_short_app0,
            png_overflow,
        ];

        for data in cases {
            let image = Image::new_from_buffer(&data);
            assert!(matches!(image, Err(XlsxError::ImageDimensionError)));
        }
    }

    #[test]
    fn bmp_negative_height() {
        // Test negative BMP heights, which indicate a top-down DIB. Also check
        // `i32::MIN` to ensure that overflow is handled/rejected correctly.
        fn tmp_bmp(height: i32) -> Vec<u8> {
            let mut data = vec![0u8; 26];
            let width: i32 = 100;

            data[0] = b'B';
            data[1] = b'M';
            data[18..22].copy_from_slice(&width.to_le_bytes());
            data[22..26].copy_from_slice(&height.to_le_bytes());

            data
        }

        // Positive and negative control values.
        for height in [50, -50] {
            let image = Image::new_from_buffer(&tmp_bmp(height)).unwrap();
            assert_eq!(100.0, image.width());
            assert_eq!(50.0, image.height());
        }

        let image = Image::new_from_buffer(&tmp_bmp(i32::MIN));
        assert!(matches!(image, Err(XlsxError::ImageDimensionError)));
    }
}