Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ICO #17

Merged
merged 1 commit into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 give the dimensions of the largest 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") {
Roughsketch marked this conversation as resolved.
Show resolved Hide resolved
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);
}