From 7cc7437d2dc4b6bc407f47408d6d323845d12688 Mon Sep 17 00:00:00 2001 From: "Matthew D. Steele" Date: Tue, 27 Feb 2018 22:43:30 -0500 Subject: [PATCH] Initial support for reading/writing ICO files --- .gitignore | 13 +-- Cargo.toml | 16 +++ examples/icotool.rs | 31 ++++++ rustfmt.toml | 11 ++ src/lib.rs | 250 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 Cargo.toml create mode 100644 examples/icotool.rs create mode 100644 rustfmt.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 50281a4..7a1466d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ -# Generated by Cargo -# will have compiled files and executables +*.rs.bk +*~ +/Cargo.lock +/scratch/ /target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7b664de --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ico" +version = "0.1.0" +authors = ["Matthew D. Steele "] +description = "A library for encoding/decoding ICO image files" +repository = "https://github.com/mdsteele/rust-ico" +keywords = ["ico", "icon", "image"] +license = "MIT" +readme = "README.md" + +[dependencies] +byteorder = "1" +png = "0.11" + +[dev-dependencies] +clap = "2.30" diff --git a/examples/icotool.rs b/examples/icotool.rs new file mode 100644 index 0000000..4611dd7 --- /dev/null +++ b/examples/icotool.rs @@ -0,0 +1,31 @@ +extern crate ico; +extern crate clap; + +use clap::{App, Arg, SubCommand}; +use std::fs; + +// ========================================================================= // + +fn main() { + let matches = App::new("icotool") + .version("0.1") + .author("Matthew D. Steele ") + .about("Manipulates ICO files") + .subcommand(SubCommand::with_name("list") + .about("Lists icons in an ICO file") + .arg(Arg::with_name("ico").required(true))) + .get_matches(); + if let Some(submatches) = matches.subcommand_matches("list") { + let path = submatches.value_of("ico").unwrap(); + let file = fs::File::open(path).unwrap(); + let icondir = ico::IconDir::read(file).unwrap(); + println!("There are {} {:?} entries", + icondir.entries().len(), + icondir.resource_type()); + for entry in icondir.entries() { + println!("{}x{}", entry.width(), entry.height()); + } + } +} + +// ========================================================================= // diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6382e8a --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,11 @@ +chain_one_line_max = 79 +fn_args_density = "Compressed" +fn_args_layout = "Visual" +fn_call_style = "Visual" +fn_single_line = true +generics_indent = "Visual" +max_width = 79 +reorder_imports = true +reorder_imported_names = true +report_fixme = "Always" +use_try_shorthand = true diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e1b355c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,250 @@ +//! A library for encoding/decoding ICO images files. + +#![warn(missing_docs)] + +extern crate byteorder; +extern crate png; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::u16; + +// ========================================================================= // + +macro_rules! invalid_data { + ($e:expr) => { + return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidData, + $e)) + }; + ($fmt:expr, $($arg:tt)+) => { + return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidData, + format!($fmt, $($arg)+))) + }; +} + +macro_rules! invalid_input { + ($e:expr) => { + return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidInput, + $e)) + }; + ($fmt:expr, $($arg:tt)+) => { + return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidInput, + format!($fmt, $($arg)+))) + }; +} + +// ========================================================================= // + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// The type of resource stored in an ICO file. +pub enum ResourceType { + /// Plain images + Icon, + /// Images with cursor hotspots + Cursor, +} + +impl ResourceType { + pub(crate) fn from_number(number: u16) -> Option { + match number { + 1 => Some(ResourceType::Icon), + 2 => Some(ResourceType::Cursor), + _ => None, + } + } + + pub(crate) fn number(&self) -> u16 { + match *self { + ResourceType::Icon => 1, + ResourceType::Cursor => 2, + } + } +} + +// ========================================================================= // + +/// A collection of images; the contents of a single ICO file. +pub struct IconDir { + restype: ResourceType, + entries: Vec, +} + +impl IconDir { + /// Creates a new, empty collection of icons/cursors. + pub fn new(resource_type: ResourceType) -> IconDir { + IconDir { + restype: resource_type, + entries: Vec::new(), + } + } + + /// Returns the type of resource stored in this collection, either icons or + /// cursors. + pub fn resource_type(&self) -> ResourceType { self.restype } + + /// Returns the entries in this collection. + pub fn entries(&self) -> &[IconDirEntry] { &self.entries } + + /// Reads an ICO file into memory. + pub fn read(mut reader: R) -> io::Result { + let reserved = reader.read_u16::()?; + if reserved != 0 { + invalid_data!("Invalid reserved field value in ICONDIR \ + (was {}, but must be 0)", + reserved); + } + let restype = reader.read_u16::()?; + let restype = match ResourceType::from_number(restype) { + Some(restype) => restype, + None => invalid_data!("Invalid resource type ({})", restype), + }; + let num_entries = reader.read_u16::()? as usize; + let mut entries = Vec::::with_capacity(num_entries); + let mut spans = Vec::<(u32, u32)>::with_capacity(num_entries); + for _ in 0..num_entries { + let width = reader.read_u8()?; + let height = reader.read_u8()?; + let num_colors = reader.read_u8()?; + let reserved = reader.read_u8()?; + if reserved != 0 { + invalid_data!("Invalid reserved field value in ICONDIRENTRY \ + (was {}, but must be 0)", + reserved); + } + let color_planes = reader.read_u16::()?; + let bits_per_pixel = reader.read_u16::()?; + let data_size = reader.read_u32::()?; + let data_offset = reader.read_u32::()?; + spans.push((data_offset, data_size)); + let entry = IconDirEntry { + width: if width == 0 { 256 } else { width as u32 }, + height: if height == 0 { 256 } else { height as u32 }, + num_colors, + color_planes, + bits_per_pixel, + data: Vec::new(), + }; + entries.push(entry); + } + for (index, &(data_offset, data_size)) in spans.iter().enumerate() { + reader.seek(SeekFrom::Start(data_offset as u64))?; + let mut data = vec![0u8; data_size as usize]; + reader.read_exact(&mut data)?; + entries[index].data = data; + } + Ok(IconDir { restype, entries }) + } + + /// Writes an ICO file out to disk. + pub fn write(&self, mut writer: W) -> io::Result<()> { + if self.entries.len() > (u16::MAX as usize) { + invalid_input!("Too many entries in IconDir \ + (was {}, but max is {})", + self.entries.len(), + u16::MAX); + } + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.restype.number())?; + writer.write_u16::(self.entries.len() as u16)?; + let mut data_offset = 6 + 16 * (self.entries.len() as u32); + for entry in self.entries.iter() { + let width = if entry.width > 255 { + 0 + } else { + entry.width as u8 + }; + writer.write_u8(width)?; + let height = if entry.height > 255 { + 0 + } else { + entry.height as u8 + }; + writer.write_u8(height)?; + writer.write_u8(entry.num_colors)?; + writer.write_u8(0)?; // reserved + writer.write_u16::(entry.color_planes)?; + writer.write_u16::(entry.bits_per_pixel)?; + let data_size = entry.data.len() as u32; + writer.write_u32::(data_size)?; + writer.write_u32::(data_offset)?; + data_offset += data_size; + } + for entry in self.entries.iter() { + writer.write_all(&entry.data)?; + } + Ok(()) + } +} + +// ========================================================================= // + +/// One entry in an ICO file; a single image or cursor. +pub struct IconDirEntry { + width: u32, + height: u32, + num_colors: u8, + color_planes: u16, + bits_per_pixel: u16, + data: Vec, +} + +impl IconDirEntry { + /// Returns the width of the image, in pixels. + pub fn width(&self) -> u32 { self.width } + + /// Returns the height of the image, in pixels. + pub fn height(&self) -> u32 { self.height } +} + +// ========================================================================= // + +#[cfg(test)] +mod tests { + use super::{IconDir, ResourceType}; + use std::io::Cursor; + + #[test] + fn resource_type_round_trip() { + let restypes = &[ResourceType::Icon, ResourceType::Cursor]; + for &restype in restypes.iter() { + assert_eq!(ResourceType::from_number(restype.number()), + Some(restype)); + } + } + + #[test] + fn read_empty_icon_set() { + let input = b"\x00\x00\x01\x00\x00\x00"; + let icondir = IconDir::read(Cursor::new(input)).unwrap(); + assert_eq!(icondir.resource_type(), ResourceType::Icon); + assert_eq!(icondir.entries().len(), 0); + } + + #[test] + fn read_empty_cursor_set() { + let input = b"\x00\x00\x02\x00\x00\x00"; + let icondir = IconDir::read(Cursor::new(input)).unwrap(); + assert_eq!(icondir.resource_type(), ResourceType::Cursor); + assert_eq!(icondir.entries().len(), 0); + } + + #[test] + fn write_empty_icon_set() { + let icondir = IconDir::new(ResourceType::Icon); + let mut output = Vec::::new(); + icondir.write(&mut output).unwrap(); + let expected: &[u8] = b"\x00\x00\x01\x00\x00\x00"; + assert_eq!(output.as_slice(), expected); + } + + #[test] + fn write_empty_cursor_set() { + let icondir = IconDir::new(ResourceType::Cursor); + let mut output = Vec::::new(); + icondir.write(&mut output).unwrap(); + let expected: &[u8] = b"\x00\x00\x02\x00\x00\x00"; + assert_eq!(output.as_slice(), expected); + } +} + +// ========================================================================= //