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:
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)),
+ }
+ }
+}