Skip to main content

didbot_brand/
ico.rs

1//! The one container format a favicon still needs.
2//!
3//! `.ico` is a directory of images, and since Windows Vista an entry may
4//! be a whole PNG rather than a bitmap — which is the only kind this
5//! writes, because everything that reads a favicon today reads that and
6//! nothing here has a reason to emit two encodings of the same tile.
7
8/// Packs `images` — PNG bytes, each `size` x `size` — into one `.ico`.
9///
10/// # Panics
11///
12/// If a size is not in `1..=256`, which is all the format's one-byte
13/// dimension fields can say.
14pub fn ico(images: &[(u32, Vec<u8>)]) -> Vec<u8> {
15    let mut out = Vec::new();
16    out.extend_from_slice(&0u16.to_le_bytes()); // reserved
17    out.extend_from_slice(&1u16.to_le_bytes()); // 1 = icon, 2 = cursor
18    out.extend_from_slice(&(images.len() as u16).to_le_bytes());
19
20    // Every entry is 16 bytes and they all precede the first payload.
21    let mut offset = 6 + 16 * images.len() as u32;
22    for (size, png) in images {
23        assert!(
24            (1..=256).contains(size),
25            "an ico entry is at most 256 pixels, not {size}"
26        );
27        // 256 is written as 0: the field is one byte and 256 does not fit.
28        let dimension = if *size == 256 { 0u8 } else { *size as u8 };
29        out.push(dimension);
30        out.push(dimension);
31        out.push(0); // palette size, meaningless for a PNG entry
32        out.push(0); // reserved
33        out.extend_from_slice(&1u16.to_le_bytes()); // colour planes
34        out.extend_from_slice(&32u16.to_le_bytes()); // bits a pixel
35        out.extend_from_slice(&(png.len() as u32).to_le_bytes());
36        out.extend_from_slice(&offset.to_le_bytes());
37        offset += png.len() as u32;
38    }
39    for (_, png) in images {
40        out.extend_from_slice(png);
41    }
42    out
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn every_entry_points_at_its_own_payload() {
51        let images = vec![(16u32, vec![1u8; 30]), (32, vec![2u8; 40])];
52        let bytes = ico(&images);
53        assert_eq!(&bytes[0..6], &[0, 0, 1, 0, 2, 0]);
54        for (index, (size, png)) in images.iter().enumerate() {
55            let entry = 6 + 16 * index;
56            assert_eq!(u32::from(bytes[entry]), *size);
57            let length =
58                u32::from_le_bytes(bytes[entry + 8..entry + 12].try_into().unwrap()) as usize;
59            let offset =
60                u32::from_le_bytes(bytes[entry + 12..entry + 16].try_into().unwrap()) as usize;
61            assert_eq!(&bytes[offset..offset + length], png.as_slice());
62        }
63    }
64
65    #[test]
66    fn a_256_pixel_entry_writes_its_size_as_zero() {
67        let bytes = ico(&[(256, vec![0u8; 8])]);
68        assert_eq!(bytes[6], 0);
69        assert_eq!(bytes[7], 0);
70    }
71}