icns-rs

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

commit 53ae20ea5829e5e9632be608cd2d0a1b256a1ceb
parent 11de1c718f3e94ed391d0abd20b690868399eea7
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Sun, 19 Apr 2020 23:24:26 +0800

Feat: impl decoder, piping and updated deps.

Diffstat:
MCargo.toml | 4+---
Mexamples/fractal_icns.rs | 20+++++++++-----------
Asrc/decode.rs | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/encode.rs | 59+++++++++++++++++++++++++++++++++++++++--------------------
Msrc/lib.rs | 7++++---
Msrc/main.rs | 92++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/os_type.rs | 17+++++++++++++++++
7 files changed, 272 insertions(+), 71 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -13,12 +13,10 @@ name = "icnsify" path = "src/main.rs" [dependencies] -image = "0.23.0" +image = "0.23.3" byteorder = "1.3.4" rayon = "1.3.0" clap = "2.33.0" -deflate = "0.8.3" -png = "0.16.1" [dev-dependencies] clap = "2.33.0" diff --git a/examples/fractal_icns.rs b/examples/fractal_icns.rs @@ -1,8 +1,8 @@ +use icns::Encoder; +use image::buffer::ConvertBuffer; +use num_complex; use std::fs::File; use std::io::BufWriter; -use num_complex; -use icns::Encoder; -use image::{self, ConvertBuffer}; fn main() { let imgx = 1024; @@ -14,7 +14,7 @@ fn main() { // Create a new ImgBuf with width: imgx and height: imgy. let mut imgbuf = image::ImageBuffer::new(imgx, imgy); - // Generate fractal. + // Generate fractal. for x in 0..imgx { for y in 0..imgy { let cx = y as f32 * scalex - 1.5; @@ -36,12 +36,11 @@ fn main() { } // Open output file. - let mut output = BufWriter::new(File::create("fractal.icns") - .expect("creating output file")); - - // Encode the image as icns. - // Note that we use ConvertBuffer trait to convert from RGB to RGBA. + let mut output = BufWriter::new(File::create("fractal.icns").expect("creating output file")); + + // Encode the image as icns. + // Note that we use ConvertBuffer trait to convert from RGB to RGBA. Encoder::new(&mut output) .encode(&imgbuf.convert()) .expect("encoding icns"); -} -\ No newline at end of file +} diff --git a/src/decode.rs b/src/decode.rs @@ -0,0 +1,144 @@ +use crate::os_type::OSType; +use byteorder::{BigEndian, ReadBytesExt}; +use image::{self, DynamicImage}; +use std::error::Error; +use std::io::{self, Read}; + +/// Decoder decodes the largest image from an icns image. +pub struct Decoder<R: Read> { + r: R, +} + +impl<R: Read> Decoder<R> { + /// Create a new Decoder that reads from `r`. + pub fn new(r: R) -> Self { + Decoder { r } + } + /// Decode icns from reader into image buffer. + /// Picks the largest image in the icns container. + /// + // This algorithm simply reads off the byte stream `r`, expecting + // data to come in a certain order. + // + // If the data is not in the expected order, we return an error. + // ICNS is a container format that generally contains differently sized + // png images, and some other bits and pieces like a table of contents. + // + // Each image is preceeded by an 8 byte header containing 2 data points: + // 1. 4 byte ascii ID indicating the OS_Type. + // 2. 4 byte unsigned integer (u32) that contains the size of the png + // content including the header. + // + // Once we've parsed out all the icon data, we return the largest available + // image. + pub fn decode(&mut self) -> Result<DynamicImage, Box<dyn Error>> { + // Note(jfm): 4 bytes is the width of the headers and the size integers, + // hence this buffer is 4 bytes long. + let mut buffer: [u8; 4] = [0; 4]; + self.r.read_exact(&mut buffer)?; + + // Check the header. + let header = std::str::from_utf8(&buffer) + .map_err(|e| format!("parsing header as utf8 string: {:?}", e))?; + if header != "icns" { + return Err(format!("invalid header for icns file: got {}", header).into()); + } + + let _size = self.r.read_u32::<BigEndian>()?; + let mut icons: Vec<IconReader> = Vec::new(); + + loop { + if let Err(err) = self.r.read_exact(&mut buffer) { + if err.kind() == io::ErrorKind::UnexpectedEof { + break; + } else { + return Err(err.into()); + } + }; + match std::str::from_utf8(&buffer) + .map_err(|e| format!("parsing chunk as utf8 string: {:?}", e)) + { + Ok("TOC ") => { + // Note(jfm): Advance the reader to skip over the TOC. + // TODO(jfm): Could we use TOC to jump to the PNG we care about? + let toc_size = self.r.read_u32::<BigEndian>()?; + self.r + .read_exact(&mut Vec::with_capacity(toc_size as usize))?; + continue; + } + Ok("icnV") => continue, + Ok(next) => { + if let Ok(os_type) = next.parse::<OSType>() { + let data_size = self.r.read_u32::<BigEndian>()?; + if data_size == 0 { + continue; + } + let mut data: Vec<u8> = vec![0; (data_size as usize) - 8]; + self.r + .read_exact(&mut data) + .map_err(|e| format!("reading into data buffer: {}", e))?; + assert!( + data.len() > 0, + "data buffer should not be empty after reading" + ); + icons.push(IconReader { os_type, data }); + } + } + Err(_) => continue, + }; + } + + if icons.is_empty() { + return Err("no icons found".into()); + } + + let largest = icons + .into_iter() + .fold(None, |mut largest: Option<IconReader>, next| { + if let Some(l) = largest.as_ref() { + if next.os_type.size() > l.os_type.size() { + largest.replace(next); + } + } else { + largest = Some(next); + } + largest + }); + if let Some(icon) = largest { + Ok(image::load_from_memory(&icon.data.as_slice()) + .map_err(|e| format!("loading image from memory: {}", e))?) + } else { + Err("no icons found".into()) + } + } +} + +// IconReader assosciates os_type with icon data. +struct IconReader { + os_type: OSType, + data: Vec<u8>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Encoder; + use std::error::Error; + + #[test] + fn codec_symmetry() -> Result<(), Box<dyn Error>> { + let input_img = image::RgbaImage::new(64, 64); + let mut icns_buffer: Vec<u8> = vec![]; + Encoder::new(&mut icns_buffer) + .encode(&input_img) + .map_err(|e| format!("encoding icns: {}", e))?; + let got_img = Decoder::new(icns_buffer.as_slice()) + .decode() + .map_err(|e| format!("decoding icns: {}", e))? + .into_rgba(); + let (l, r) = (input_img.dimensions(), got_img.dimensions()); + assert_eq!(l, r, "dimension mismatch: {:?} != {:?}", l, r); + assert_eq!(input_img.into_vec(), got_img.into_vec(), "buffer mismatch"); + Ok(()) + } +} diff --git a/src/encode.rs b/src/encode.rs @@ -1,10 +1,9 @@ use byteorder::{BigEndian, WriteBytesExt}; use image::imageops::{resize, Lanczos3}; -use image::{self, RgbaImage}; -use png; +use image::{self, DynamicImage, GenericImageView, RgbaImage}; use rayon::{self, prelude::*}; use std::cmp::max; -use std::io::{self, Write}; +use std::io::Write; use crate::os_type::OSType; @@ -18,14 +17,19 @@ impl<W: Write> Encoder<W> { pub fn new(w: W) -> Self { Encoder { w } } - // Encode the icns from a source png encoded buffer. - pub fn encode(&mut self, img: &RgbaImage) -> io::Result<()> { - IconSet::from(img).write_to(self.w.by_ref()) + /// Encode encode an icns into the writer, using the source image. + pub fn encode<Img>(&mut self, img: Img) -> Result<(), Box<dyn std::error::Error>> + where + Img: Into<IconSet>, + { + // Note(jfm): CPU intensive work is being done in `From` trait. + // This is probably not good practice since it hides the actual work. + img.into().write_to(self.w.by_ref()) } } /// IconSet encodes a vector of icons. -struct IconSet { +pub struct IconSet { icons: Vec<Icon>, } @@ -34,7 +38,7 @@ const ICONSET_MAGIC: &'static str = "icns"; impl IconSet { /// Write the encoded iconset to writer `w`. - fn write_to(self, mut wr: impl Write) -> io::Result<()> { + pub fn write_to(self, mut wr: impl Write) -> Result<(), Box<dyn std::error::Error>> { // Pre-buffer the encoded icons so we can calculate the final size. let mut buffer: Vec<u8> = vec![]; for icon in self.icons { @@ -58,7 +62,7 @@ struct Icon { impl Icon { /// Write the encoded icon to writer `w`. - fn write_to(self, mut wr: impl Write) -> io::Result<()> { + fn write_to(self, mut wr: impl Write) -> Result<(), Box<dyn std::error::Error>> { // Pre-buffer the png image so we can calculate size total. let (width, height) = (self.image.width(), self.image.height()); let mut buffer: Vec<u8> = vec![]; @@ -66,8 +70,7 @@ impl Icon { self.image.into_raw().as_ref(), width, height, - png::ColorType::RGBA, - png::BitDepth::Eight, + image::ColorType::Rgba8, )?; // Write the 4-byte OSType identifier. wr.write_all(&self.kind.header().as_bytes())?; @@ -93,15 +96,31 @@ impl<W: Write> PNGEncoder<W> { data: &[u8], width: u32, height: u32, - ct: png::ColorType, - bits: png::BitDepth, - ) -> io::Result<()> { - let mut encoder = png::Encoder::new(self.w, width, height); - encoder.set_color(ct); - encoder.set_depth(bits); - encoder.set_compression(png::Compression::Default); - let mut writer = encoder.write_header()?; - writer.write_image_data(data).map_err(|e| e.into()) + ct: image::ColorType, + ) -> Result<(), Box<dyn std::error::Error>> { + image::png::PNGEncoder::new(self.w).encode(data, width, height, ct)?; + Ok(()) + } +} + +/// Create an IconSet from the provided image. +/// If width != height, the image will be resized using the largest side +/// without preserving the aspect ratio. +impl From<&DynamicImage> for IconSet { + fn from(img: &DynamicImage) -> Self { + let kind = OSType::nearest(max(img.width(), img.height())); + let icons: Vec<Icon> = kind + .smaller_variants() + .into_par_iter() + .map(|v| { + let size = v.size(); + Icon { + kind: v, + image: resize(img, size, size, Lanczos3), + } + }) + .collect(); + IconSet { icons } } } diff --git a/src/lib.rs b/src/lib.rs @@ -1,6 +1,8 @@ -//! This crate provides encoding for ICNS icons (Apple Icon Image Format). +//! This crate provides encoding for ICNS icons (Apple Icon Image Format). +mod decode; mod encode; mod os_type; -pub use crate::encode::Encoder; -\ No newline at end of file +pub use crate::decode::Decoder; +pub use crate::encode::Encoder; diff --git a/src/main.rs b/src/main.rs @@ -1,47 +1,72 @@ +mod decode; mod encode; mod os_type; -use std::io::{self, BufReader, BufWriter}; -use std::fs::File; use clap::{App, Arg}; -use image; +use decode::Decoder; use encode::Encoder; +use image::{self, png::PNGEncoder, ColorType, ImageFormat}; +use std::fs::File; +use std::io::{self, BufReader, BufWriter, Cursor}; fn main() { let cli = App::new("icnsify") - .version("0.1.0") + .version("0.2.0") .author("Jack Mordaunt <jackmordaunt@gmail.com>") .about("easily create icns icons from png images") - .arg(Arg::with_name("in") - .short("i") - .long("input") - .takes_value(true) - .requires("out") - .help("path to input file")) - .arg(Arg::with_name("out") - .short("o") - .long("output") - .takes_value(true) - .requires("in") - .help("path to output file")) + .arg( + Arg::with_name("in") + .short("i") + .long("input") + .takes_value(true) + .requires("out") + .help("path to input file"), + ) + .arg( + Arg::with_name("out") + .short("o") + .long("output") + .takes_value(true) + .requires("in") + .help("path to output file"), + ) + .arg( + Arg::with_name("decode") + .long("decode") + .help("decode an icns into a png (reverse the direction)"), + ) .get_matches(); if let (Some(src), Some(out)) = (cli.value_of("in"), cli.value_of("out")) { - let src = image::open(&src) - .expect("decoding input image"); - let out = BufWriter::new(File::create(&out) - .expect("creating output file")); - Encoder::new(out).encode(&src.to_rgba()) - .expect("encoding icns"); + if cli.is_present("decode") { + let src = BufReader::new(File::open(&src).expect("opening src file")); + let img = Decoder::new(src).decode().expect("decoding png from icns"); + img.save(&out).expect("writing png"); + } else { + let src = image::open(&src).expect("decoding input image"); + let out = BufWriter::new(File::create(&out).expect("creating output file")); + Encoder::new(out) + .encode(&src.to_rgba()) + .expect("encoding icns"); + } } else { - let mut buf: Vec<u8> = vec![]; - let stdin = io::stdin(); - let mut stdin = BufReader::new(stdin.lock()); - io::copy(&mut stdin, &mut buf) - .expect("reading from stdin"); - let src = image::load_from_memory(&buf) - .expect("decoding input image"); - Encoder::new(BufWriter::new(io::stdout().lock())) - .encode(&src.to_rgba()) - .expect("encoding icns"); + // BUG: Piping doesn't work with pwsh version of cat (builtin, I think). + // What is the powershell way of piping? + // Check on Unix. + if cli.is_present("decode") { + let img = Decoder::new(BufReader::new(io::stdin().lock())) + .decode() + .expect("decoding icns") + .into_rgba(); + PNGEncoder::new(io::stdout().lock()) + .encode(&img, img.width(), img.height(), ColorType::Rgb8) + .expect("encoding png"); + } else { + let mut buffer: Vec<u8> = vec![]; + io::copy(&mut io::stdin().lock(), &mut buffer).expect("reading from stdin"); + let img = image::load(&mut Cursor::new(buffer), ImageFormat::Png).expect("loading png"); + Encoder::new(BufWriter::new(io::stdout().lock())) + .encode(&img.to_rgba()) + .expect("encoding icns"); + } } -} -\ No newline at end of file +} diff --git a/src/os_type.rs b/src/os_type.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + /// OSType is an enum of various icon types that can exist inside an icns /// container. This enum only contains the high resolution variants that we care /// about. Find the full list here: https://en.wikipedia.org/wiki/Apple_Icon_Image_format @@ -65,3 +67,18 @@ impl OSType { variants } } + +impl FromStr for OSType { + type Err = String; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "ic10" => Ok(OSType::IC10), + "ic14" => Ok(OSType::IC14), + "ic13" => Ok(OSType::IC13), + "ic07" => Ok(OSType::IC07), + "ic12" => Ok(OSType::IC12), + "ic11" => Ok(OSType::IC11), + _ => Err(format!("{} is not an icns OSType", s)), + } + } +}