use crate::image::{IconImage, ImageStats};
use crate::restype::ResourceType;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::io::{self, Read, Seek, SeekFrom, Write};
const PNG_SIGNATURE: &[u8] = &[0x89, b'P', b'N', b'G'];
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct IconDir {
restype: ResourceType,
entries: Vec<IconDirEntry>,
}
impl IconDir {
pub fn new(resource_type: ResourceType) -> IconDir {
IconDir { restype: resource_type, entries: Vec::new() }
}
pub fn resource_type(&self) -> ResourceType {
self.restype
}
pub fn entries(&self) -> &[IconDirEntry] {
&self.entries
}
pub fn add_entry(&mut self, entry: IconDirEntry) {
if self.resource_type() != entry.resource_type() {
panic!(
"Can't add {:?} IconDirEntry to {:?} IconDir",
entry.resource_type(),
self.resource_type()
);
}
self.entries.push(entry);
}
pub fn read<R: Read + Seek>(mut reader: R) -> io::Result<IconDir> {
let reserved = reader.read_u16::<LittleEndian>()?;
if reserved != 0 {
invalid_data!(
"Invalid reserved field value in ICONDIR \
(was {}, but must be 0)",
reserved
);
}
let restype = reader.read_u16::<LittleEndian>()?;
let restype = match ResourceType::from_number(restype) {
Some(restype) => restype,
None => invalid_data!("Invalid resource type ({})", restype),
};
let num_entries = reader.read_u16::<LittleEndian>()? as usize;
let mut entries = Vec::<IconDirEntry>::with_capacity(num_entries);
let mut spans = Vec::<(u32, u32)>::with_capacity(num_entries);
for _ in 0..num_entries {
let width_byte = reader.read_u8()?;
let height_byte = 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::<LittleEndian>()?;
let bits_per_pixel = reader.read_u16::<LittleEndian>()?;
let data_size = reader.read_u32::<LittleEndian>()?;
let data_offset = reader.read_u32::<LittleEndian>()?;
let width = if width_byte == 0 { 256 } else { width_byte as u32 };
let height =
if height_byte == 0 { 256 } else { height_byte as u32 };
spans.push((data_offset, data_size));
let entry = IconDirEntry {
restype,
width,
height,
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;
}
for entry in entries.iter_mut() {
if let Ok((width, height)) = entry.decode_size() {
entry.width = width;
entry.height = height;
}
}
Ok(IconDir { restype, entries })
}
pub fn write<W: 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::<LittleEndian>(0)?; writer.write_u16::<LittleEndian>(self.restype.number())?;
writer.write_u16::<LittleEndian>(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)?; writer.write_u16::<LittleEndian>(entry.color_planes)?;
writer.write_u16::<LittleEndian>(entry.bits_per_pixel)?;
let data_size = entry.data.len() as u32;
writer.write_u32::<LittleEndian>(data_size)?;
writer.write_u32::<LittleEndian>(data_offset)?;
data_offset += data_size;
}
for entry in self.entries.iter() {
writer.write_all(&entry.data)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct IconDirEntry {
restype: ResourceType,
width: u32,
height: u32,
num_colors: u8,
color_planes: u16,
bits_per_pixel: u16,
data: Vec<u8>,
}
impl IconDirEntry {
pub fn resource_type(&self) -> ResourceType {
self.restype
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn bits_per_pixel(&self) -> u16 {
if self.restype == ResourceType::Cursor {
0
} else {
self.bits_per_pixel
}
}
pub fn cursor_hotspot(&self) -> Option<(u16, u16)> {
if self.restype == ResourceType::Cursor {
Some((self.color_planes, self.bits_per_pixel))
} else {
None
}
}
pub fn is_png(&self) -> bool {
self.data.starts_with(PNG_SIGNATURE)
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub(crate) fn decode_size(&mut self) -> io::Result<(u32, u32)> {
if self.is_png() {
let png_reader = IconImage::read_png_info(self.data.as_slice())?;
Ok((png_reader.info().width, png_reader.info().height))
} else {
IconImage::read_bmp_size(&mut self.data.as_slice())
}
}
pub fn decode(&self) -> io::Result<IconImage> {
let mut image = if self.is_png() {
IconImage::read_png(self.data.as_slice())?
} else {
IconImage::read_bmp(self.data.as_slice())?
};
if image.width() != self.width || image.height() != self.height {
invalid_data!(
"Encoded image has wrong dimensions \
(was {}x{}, but should be {}x{})",
image.width(),
image.height(),
self.width,
self.height
);
}
image.set_cursor_hotspot(self.cursor_hotspot());
Ok(image)
}
pub fn encode(image: &IconImage) -> io::Result<IconDirEntry> {
let stats = image.compute_stats();
let use_png = stats.has_nonbinary_alpha
|| image.width() * image.height() > 64 * 64;
if use_png {
IconDirEntry::encode_as_png_internal(image, &stats)
} else {
IconDirEntry::encode_as_bmp_internal(image, &stats)
}
}
pub fn encode_as_bmp(image: &IconImage) -> io::Result<IconDirEntry> {
IconDirEntry::encode_as_bmp_internal(image, &image.compute_stats())
}
fn encode_as_bmp_internal(
image: &IconImage,
stats: &ImageStats,
) -> io::Result<IconDirEntry> {
let (num_colors, bits_per_pixel, data) =
image.write_bmp_internal(stats)?;
let (color_planes, bits_per_pixel) =
image.cursor_hotspot().unwrap_or((1, bits_per_pixel));
let restype = if image.cursor_hotspot().is_some() {
ResourceType::Cursor
} else {
ResourceType::Icon
};
let entry = IconDirEntry {
restype,
width: image.width(),
height: image.height(),
num_colors,
color_planes,
bits_per_pixel,
data,
};
Ok(entry)
}
pub fn encode_as_png(image: &IconImage) -> io::Result<IconDirEntry> {
IconDirEntry::encode_as_png_internal(image, &image.compute_stats())
}
fn encode_as_png_internal(
image: &IconImage,
stats: &ImageStats,
) -> io::Result<IconDirEntry> {
let mut data = Vec::new();
let bits_per_pixel = image.write_png_internal(stats, &mut data)?;
let (color_planes, bits_per_pixel) =
image.cursor_hotspot().unwrap_or((0, bits_per_pixel));
let restype = if image.cursor_hotspot().is_some() {
ResourceType::Cursor
} else {
ResourceType::Icon
};
let entry = IconDirEntry {
restype,
width: image.width(),
height: image.height(),
num_colors: 0,
color_planes,
bits_per_pixel,
data,
};
Ok(entry)
}
}
#[cfg(test)]
mod tests {
use super::{IconDir, IconDirEntry, IconImage, ResourceType};
use std::io::Cursor;
#[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::<u8>::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::<u8>::new();
icondir.write(&mut output).unwrap();
let expected: &[u8] = b"\x00\x00\x02\x00\x00\x00";
assert_eq!(output.as_slice(), expected);
}
#[test]
fn read_bmp_1bpp_icon() {
let input: &[u8] = b"\
\x00\x00\x01\x00\x01\x00\
\
\x02\x02\x02\x00\x01\x00\x01\x00\
\x40\x00\x00\x00\x16\x00\x00\x00\
\
\x28\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\
\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\
\
\x55\x00\x55\x00\xff\xff\xff\x00\
\
\xc0\x00\x00\x00\
\x40\x00\x00\x00\
\
\x40\x00\x00\x00\
\x00\x00\x00\x00";
let icondir = IconDir::read(Cursor::new(input)).unwrap();
assert_eq!(icondir.resource_type(), ResourceType::Icon);
assert_eq!(icondir.entries().len(), 1);
let entry = &icondir.entries()[0];
assert_eq!(entry.width(), 2);
assert_eq!(entry.height(), 2);
assert!(!entry.is_png());
let image = entry.decode().unwrap();
assert_eq!(image.width(), 2);
assert_eq!(image.height(), 2);
let rgba: &[u8] = b"\
\x55\x00\x55\xff\xff\xff\xff\xff\
\xff\xff\xff\xff\xff\xff\xff\x00";
assert_eq!(image.rgba_data(), rgba);
}
#[test]
fn read_bmp_4bpp_icon() {
let input: &[u8] = b"\
\x00\x00\x01\x00\x01\x00\
\
\x05\x03\x10\x00\x01\x00\x04\x00\
\x80\x00\x00\x00\x16\x00\x00\x00\
\
\x28\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\
\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\
\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x7f\x00\x00\x00\xff\x00\
\x00\x7f\x00\x00\x00\xff\x00\x00\
\x00\x7f\x7f\x00\x00\xff\xff\x00\
\x7f\x00\x00\x00\xff\x00\x00\x00\
\x7f\x00\x7f\x00\xff\x00\xff\x00\
\x7f\x7f\x00\x00\xff\xff\x00\x00\
\x7f\x7f\x7f\x00\xff\xff\xff\x00\
\
\x0f\x35\x00\x00\
\xf3\x59\x10\x00\
\x05\x91\x00\x00\
\
\x88\x00\x00\x00\
\x00\x00\x00\x00\
\x88\x00\x00\x00";
let icondir = IconDir::read(Cursor::new(input)).unwrap();
assert_eq!(icondir.resource_type(), ResourceType::Icon);
assert_eq!(icondir.entries().len(), 1);
let entry = &icondir.entries()[0];
assert_eq!(entry.width(), 5);
assert_eq!(entry.height(), 3);
assert!(!entry.is_png());
let image = entry.decode().unwrap();
assert_eq!(image.width(), 5);
assert_eq!(image.height(), 3);
let rgba: &[u8] = b"\
\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\xff\xff\
\x00\x00\x00\xff\x00\x00\x00\x00\
\xff\xff\xff\xff\xff\x00\x00\xff\x00\xff\x00\xff\
\x00\x00\xff\xff\x00\x00\x00\xff\
\x00\x00\x00\x00\xff\xff\xff\xff\xff\x00\x00\xff\
\x00\xff\x00\xff\x00\x00\x00\x00";
assert_eq!(image.rgba_data(), rgba);
}
#[test]
fn read_png_grayscale_icon() {
let input: &[u8] = b"\
\x00\x00\x01\x00\x01\x00\
\
\x02\x02\x00\x00\x00\x00\x00\x00\
\x47\x00\x00\x00\x16\x00\x00\x00\
\
\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\
\x00\x00\x00\x02\x00\x00\x00\x02\x08\x00\x00\x00\x00\x57\xdd\x52\
\xf8\x00\x00\x00\x0e\x49\x44\x41\x54\x78\x9c\x63\xb4\x77\x60\xdc\
\xef\x00\x00\x04\x08\x01\x81\x86\x2e\xc9\x8d\x00\x00\x00\x00\x49\
\x45\x4e\x44\xae\x42\x60\x82";
let icondir = IconDir::read(Cursor::new(input)).unwrap();
assert_eq!(icondir.resource_type(), ResourceType::Icon);
assert_eq!(icondir.entries().len(), 1);
let entry = &icondir.entries()[0];
assert_eq!(entry.width(), 2);
assert_eq!(entry.height(), 2);
assert!(entry.is_png());
let image = entry.decode().unwrap();
assert_eq!(image.width(), 2);
assert_eq!(image.height(), 2);
let rgba: &[u8] = b"\
\x3f\x3f\x3f\xff\x7f\x7f\x7f\xff\
\xbf\xbf\xbf\xff\xff\xff\xff\xff";
assert_eq!(image.rgba_data(), rgba);
}
#[test]
fn image_data_round_trip() {
let width = 11;
let height = 13;
let mut rgba = Vec::new();
for index in 0..(width * height) {
rgba.push(if index % 2 == 0 { 0 } else { 255 });
rgba.push(if index % 3 == 0 { 0 } else { 255 });
rgba.push(if index % 5 == 0 { 0 } else { 255 });
rgba.push(if index % 7 == 0 { 128 } else { 255 });
}
let image = IconImage::from_rgba_data(width, height, rgba.clone());
let mut icondir = IconDir::new(ResourceType::Icon);
icondir.add_entry(IconDirEntry::encode(&image).unwrap());
let mut file = Vec::<u8>::new();
icondir.write(&mut file).unwrap();
let icondir = IconDir::read(Cursor::new(&file)).unwrap();
assert_eq!(icondir.entries().len(), 1);
let image = icondir.entries()[0].decode().unwrap();
assert_eq!(image.width(), width);
assert_eq!(image.height(), height);
assert_eq!(image.rgba_data(), rgba.as_slice());
}
}