Skip to content

Commit

Permalink
Add support for ICO
Browse files Browse the repository at this point in the history
  • Loading branch information
P1n3appl3 committed May 30, 2022
1 parent 01d4f1e commit 007d18f
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 31 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ imagesize = "0.9"
* PSD / PSB
* TIFF
* WEBP
* ICO*

If you have a format you think should be added, feel free to create an issue.

*ICO files can contain multiple images, `imagesize` will only retrieve the first one.

## Examples

### From a file
Expand Down
98 changes: 70 additions & 28 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,30 @@ pub enum ImageType {
Psd,
Tiff,
Webp,
Ico,
}

/// Holds the size information of an image.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ImageSize {
/// Width of an image in pixels.
pub width: usize,
/// Height of an image in pixels.
pub height: usize,
}

impl Ord for ImageSize {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.width * self.height).cmp(&(other.width * other.height))
}
}

impl PartialOrd for ImageSize {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

/// Get the image type from a header
///
/// # Arguments
Expand All @@ -71,33 +84,33 @@ pub struct ImageSize {
///
/// This will check the header to determine what image type the data is.
pub fn image_type(header: &[u8]) -> ImageResult<ImageType> {
if header.len() >= 2 {
if &header[0..2] == b"\x42\x4D" {
return Ok(ImageType::Bmp);
} else if header.len() >= 3 && &header[0..3] == b"\xFF\xD8\xFF" {
return Ok(ImageType::Jpeg);
} else if header.len() >= 4 && &header[0..4] == b"\x89PNG" {
return Ok(ImageType::Png);
} else if header.len() >= 4 && &header[0..4] == b"GIF8" {
return Ok(ImageType::Gif);
} else if header.len() >= 4 && (&header[0..4] == b"II\x2A\x00" || &header[0..4] == b"MM\x00\x2A") {
return Ok(ImageType::Tiff);
} else if header.len() >= 4 && &header[0..4] == b"8BPS" {
return Ok(ImageType::Psd);
} else if header.len() >= 8 && &header[4..8] == b"ftyp" {
return Ok(ImageType::Heif);
} else if header.len() >= 12 && &header[0..4] == b"RIFF" && &header[8..12] == b"WEBP" {
return Ok(ImageType::Webp);
} else if (header.len() >= 2 && &header[0..2] == b"\xFF\x0A")
|| (header.len() >= 12 && &header[0..12] == b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A")
{
return Ok(ImageType::Jxl);
} else {
return Err(ImageError::NotSupported);
}
if header.len() < 2 {
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into())
} else if header.starts_with(b"\x42\x4D") {
Ok(ImageType::Bmp)
} else if header.starts_with(b"\xFF\xD8\xFF") {
Ok(ImageType::Jpeg)
} else if header.starts_with(b"\x89PNG") {
Ok(ImageType::Png)
} else if header.starts_with(b"GIF8") {
Ok(ImageType::Gif)
} else if header.starts_with(b"II\x2A\x00") || header.starts_with(b"MM\x00\x2A") {
Ok(ImageType::Tiff)
} else if header.starts_with(b"8BPS") {
Ok(ImageType::Psd)
} else if header.starts_with(&[0, 0, 1, 0]) {
Ok(ImageType::Ico)
} else if header.len() >= 8 && &header[4..8] == b"ftyp" {
Ok(ImageType::Heif)
} else if header.len() >= 12 && &header[0..4] == b"RIFF" && &header[8..12] == b"WEBP" {
Ok(ImageType::Webp)
} else if header.starts_with(b"\xFF\x0A")
|| header.starts_with(b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A")
{
Ok(ImageType::Jxl)
} else {
Err(ImageError::NotSupported)
}

Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into())
}

/// Get the image size from a local file
Expand Down Expand Up @@ -187,7 +200,7 @@ pub fn blob_size(data: &[u8]) -> ImageResult<ImageSize> {
/// * `reader` - A reader for the data
/// * `header` - The header of the file
fn dispatch_header<R: BufRead + Seek>(reader: &mut R, header: &[u8]) -> ImageResult<ImageSize> {
match image_type(&header)? {
match image_type(header)? {
ImageType::Bmp => bmp_size(reader),
ImageType::Gif => gif_size(header),
ImageType::Heif => heif_size(reader),
Expand All @@ -197,6 +210,7 @@ fn dispatch_header<R: BufRead + Seek>(reader: &mut R, header: &[u8]) -> ImageRes
ImageType::Psd => psd_size(reader),
ImageType::Tiff => tiff_size(reader),
ImageType::Webp => webp_size(reader),
ImageType::Ico => ico_size(reader),
}
}

Expand Down Expand Up @@ -663,3 +677,31 @@ fn webp_vp8_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
height: read_u16(reader, &Endian::Little)? as usize,
})
}

fn ico_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
reader.seek(SeekFrom::Start(4))?;
let img_count = read_u16(reader, &Endian::Little)?;
let mut sizes = Vec::with_capacity(img_count as usize);

for _ in 0..img_count {
if let Ok(size) = ico_image_size(reader) {
sizes.push(size)
} else {
// if we don't have all the bytes of the headers, just
// return the largest one found so far
break;
}
// each ICONDIRENTRY (image header) is 16 bytes, skip the last 14
reader.seek(SeekFrom::Current(14))?;
}
sizes.into_iter().max().ok_or(ImageError::CorruptedImage)
}

/// Reads two bytes to determine an individual image's size within an ICO
fn ico_image_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
// ICO dimensions are 1-256 pixels, with a byte value of 0 representing 256
Ok(ImageSize {
width: read_u8(reader)?.wrapping_sub(1) as usize + 1,
height: read_u8(reader)?.wrapping_sub(1) as usize + 1,
})
}
Binary file added test/ico/max_width.ico
Binary file not shown.
Binary file added test/ico/multiple.ico
Binary file not shown.
Binary file added test/ico/test.ico
Binary file not shown.
25 changes: 22 additions & 3 deletions tests/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn blob_test() {
#[test]
fn blob_too_small_test() {
let data = vec![0x89, 0x00, 0x01, 0x02];
assert_eq!(blob_size(&data).is_err(), true);
assert!(blob_size(&data).is_err());
}

#[test]
Expand All @@ -28,11 +28,30 @@ fn blob_test_fail() {
0x00, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x01, 0x41,
0x08, 0x06, 0x00, 0x00, 0x00, 0x9A, 0x38, 0xC4];

assert_eq!(blob_size(&data).is_err(), true);
assert!(blob_size(&data).is_err());
}

#[test]
fn gif_blob_too_small_test() {
let data = vec![0x47, 0x49, 0x46, 0x38];
assert_eq!(blob_size(&data).is_err(), true);
assert!(blob_size(&data).is_err());
}

#[test]
fn blob_test_partial_ico() {
let data = vec![
// Header (says 6 images are included)
0x00, 0x00, 0x01, 0x00, 0x06, 0x00,
// Image 1 (16x32)
0x10, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Image 2 (10x100)
0x0A, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Image 3 (255x? run out of bytes)
0xFF
];
let dim = blob_size(&data).unwrap();
assert_eq!(dim.width, 10);
assert_eq!(dim.height, 100);
}
24 changes: 24 additions & 0 deletions tests/ico.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[cfg(test)]
use imagesize::size;

#[test]
fn ico_test() {
let dim = size("test/ico/test.ico").unwrap();
assert_eq!(dim.width, 16);
assert_eq!(dim.height, 16);
}

#[test]
fn max_size_test() {
let dim = size("test/ico/max_width.ico").unwrap();
assert_eq!(dim.width, 256);
assert_eq!(dim.height, 255);
}

#[test]
fn multiple_test() {
// Contains 48x48, 32x32, and 16x16 versions of the same image
let dim = size("test/ico/multiple.ico").unwrap();
assert_eq!(dim.width, 48);
assert_eq!(dim.height, 48);
}

0 comments on commit 007d18f

Please sign in to comment.