icns-rs

Easily create .icns files (Mac Icons) with this Rust library or the included CLI app.
Log | Files | Refs | LICENSE

decode.rs (5298B)


      1 use crate::os_type::OSType;
      2 use byteorder::{BigEndian, ReadBytesExt};
      3 use image::{self, DynamicImage};
      4 use std::error::Error;
      5 use std::io::{self, Read};
      6 
      7 /// Decoder decodes the largest image from an icns image.
      8 pub struct Decoder<R: Read> {
      9     r: R,
     10 }
     11 
     12 impl<R: Read> Decoder<R> {
     13     /// Create a new Decoder that reads from `r`.
     14     pub fn new(r: R) -> Self {
     15         Decoder { r }
     16     }
     17     /// Decode icns from reader into image buffer.
     18     /// Picks the largest image in the icns container.
     19     ///
     20     // This algorithm simply reads off the byte stream `r`, expecting
     21     // data to come in a certain order.
     22     //
     23     // If the data is not in the expected order, we return an error.
     24     // ICNS is a container format that generally contains differently sized
     25     // png images, and some other bits and pieces like a table of contents.
     26     //
     27     // Each image is preceeded by an 8 byte header containing 2 data points:
     28     // 1. 4 byte ascii ID indicating the OS_Type.
     29     // 2. 4 byte unsigned integer (u32) that contains the size of the png
     30     //  content including the header.
     31     //
     32     // Once we've parsed out all the icon data, we return the largest available
     33     // image.
     34     pub fn decode(&mut self) -> Result<DynamicImage, Box<dyn Error>> {
     35         // Note(jfm): 4 bytes is the width of the headers and the size integers,
     36         // hence this buffer is 4 bytes long.
     37         let mut buffer: [u8; 4] = [0; 4];
     38         self.r.read_exact(&mut buffer)?;
     39 
     40         // Check the header.
     41         let header = std::str::from_utf8(&buffer)
     42             .map_err(|e| format!("parsing header as utf8 string: {:?}", e))?;
     43         if header != "icns" {
     44             return Err(format!("invalid header for icns file: got {}", header).into());
     45         }
     46 
     47         let _size = self.r.read_u32::<BigEndian>()?;
     48         let mut icons: Vec<IconReader> = Vec::new();
     49 
     50         loop {
     51             if let Err(err) = self.r.read_exact(&mut buffer) {
     52                 if err.kind() == io::ErrorKind::UnexpectedEof {
     53                     break;
     54                 } else {
     55                     return Err(err.into());
     56                 }
     57             };
     58             match std::str::from_utf8(&buffer)
     59                 .map_err(|e| format!("parsing chunk as utf8 string: {:?}", e))
     60             {
     61                 Ok("TOC ") => {
     62                     // Note(jfm): Advance the reader to skip over the TOC.
     63                     // TODO(jfm): Could we use TOC to jump to the PNG we care about?
     64                     let toc_size = self.r.read_u32::<BigEndian>()?;
     65                     self.r
     66                         .read_exact(&mut Vec::with_capacity(toc_size as usize))?;
     67                     continue;
     68                 }
     69                 Ok("icnV") => continue,
     70                 Ok(next) => {
     71                     if let Ok(os_type) = next.parse::<OSType>() {
     72                         let data_size = self.r.read_u32::<BigEndian>()?;
     73                         if data_size == 0 {
     74                             continue;
     75                         }
     76                         let mut data: Vec<u8> = vec![0; (data_size as usize) - 8];
     77                         self.r
     78                             .read_exact(&mut data)
     79                             .map_err(|e| format!("reading into data buffer: {}", e))?;
     80                         assert!(
     81                             data.len() > 0,
     82                             "data buffer should not be empty after reading"
     83                         );
     84                         icons.push(IconReader { os_type, data });
     85                     }
     86                 }
     87                 Err(_) => continue,
     88             };
     89         }
     90 
     91         if icons.is_empty() {
     92             return Err("no icons found".into());
     93         }
     94 
     95         let largest = icons
     96             .into_iter()
     97             .fold(None, |mut largest: Option<IconReader>, next| {
     98                 if let Some(l) = largest.as_ref() {
     99                     if next.os_type.size() > l.os_type.size() {
    100                         largest.replace(next);
    101                     }
    102                 } else {
    103                     largest = Some(next);
    104                 }
    105                 largest
    106             });
    107         if let Some(icon) = largest {
    108             Ok(image::load_from_memory(&icon.data.as_slice())
    109                 .map_err(|e| format!("loading image from memory: {}", e))?)
    110         } else {
    111             Err("no icons found".into())
    112         }
    113     }
    114 }
    115 
    116 // IconReader assosciates os_type with icon data.
    117 struct IconReader {
    118     os_type: OSType,
    119     data: Vec<u8>,
    120 }
    121 
    122 #[cfg(test)]
    123 mod tests {
    124     use super::*;
    125     use crate::Encoder;
    126     use std::error::Error;
    127 
    128     #[test]
    129     fn codec_symmetry() -> Result<(), Box<dyn Error>> {
    130         let input_img = image::RgbaImage::new(64, 64);
    131         let mut icns_buffer: Vec<u8> = vec![];
    132         Encoder::new(&mut icns_buffer)
    133             .encode(&input_img)
    134             .map_err(|e| format!("encoding icns: {}", e))?;
    135         let got_img = Decoder::new(icns_buffer.as_slice())
    136             .decode()
    137             .map_err(|e| format!("decoding icns: {}", e))?
    138             .into_rgba();
    139         let (l, r) = (input_img.dimensions(), got_img.dimensions());
    140         assert_eq!(l, r, "dimension mismatch: {:?} != {:?}", l, r);
    141         assert_eq!(input_img.into_vec(), got_img.into_vec(), "buffer mismatch");
    142         Ok(())
    143     }
    144 }