Skip to content

Instantly share code, notes, and snippets.

@heyimalex
Created December 19, 2019 02:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heyimalex/92a4f056a5de5106e5c026bf6714a546 to your computer and use it in GitHub Desktop.
Save heyimalex/92a4f056a5de5106e5c026bf6714a546 to your computer and use it in GitHub Desktop.
Combine a bunch of pngs into an ico file.
use std::{env, fs, process};
use std::error::Error;
use std::convert::{TryFrom,TryInto};
use std::io::{Write,BufWriter};
static HELP_TEXT: &str = "ico-join [<PNG>...] <DEST>
<PNG> Path to png files you want to combine.
<DEST> Path to the output file.
";
fn main() -> Result<(), Box<dyn Error>> {
let mut args: Vec<String> = env::args().collect();
if args.len() < 3 || args.iter().find(|arg| arg.starts_with('-')).is_some() {
println!("{}", HELP_TEXT);
process::exit(1);
}
let dest = args.pop().unwrap();
let srcs = &args[1..];
let mut pngs: Vec<(Png, Vec<u8>)> = Vec::with_capacity(srcs.len());
for path in srcs {
let data = fs::read(path).expect("failed to read input png");
let header = parse_png(&data).expect("failed to parse input png");
if header.width > 256 || header.width > 256 {
println!("input image width/height must be below 256px, was {}x{}", header.width, header.height);
process::exit(1);
}
pngs.push((header, data));
}
let dest = fs::File::create(dest)?;
let mut w = BufWriter::new(dest);
w.write_all(&0u16.to_le_bytes())?;
w.write_all(&1u16.to_le_bytes())?;
w.write_all(&u16::try_from(srcs.len())?.to_le_bytes())?;
let mut icondir = vec![0u8; 16];
let mut offset: u32 = (6 + pngs.len()*16).try_into()?;
for png in &pngs {
let header = &png.0;
let data_size = u32::try_from(png.1.len())?;
let width: u8 = if header.width == 256 {
0
} else {
header.width.try_into()?
};
let height: u8 = if header.height == 256 {
0
} else {
header.height.try_into()?
};
icondir[0] = width; // width
icondir[1] = height; // height
icondir[2] = 0; // colors in palette (optional for pngs??)
icondir[3] = 0; // reserved
icondir[4] = 0; // color planes
icondir[5] = 0; // color planes pt 2
icondir[6] = 0; // bits per pixel (optional for pngs??)
icondir[7] = 0; // bits per pixel pt 2
icondir[8..12].copy_from_slice(&data_size.to_le_bytes());
icondir[12..16].copy_from_slice(&offset.to_le_bytes());
offset += data_size;
w.write_all(&icondir)?;
}
for png in &pngs {
w.write_all(&png.1)?;
}
w.flush()?;
println!("Done!");
Ok(())
}
#[derive(Debug, Copy, Clone)]
pub struct Png {
pub width: u32,
pub height: u32,
pub color_type: ColorType,
pub bit_depth: u8,
}
#[derive(Debug, Copy, Clone)]
pub enum ColorType {
Gray,
RGB,
PLTE,
GrayAlpha,
RGBA,
}
impl TryFrom<u8> for ColorType {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(ColorType::Gray),
2 => Ok(ColorType::RGB),
3 => Ok(ColorType::PLTE),
4 => Ok(ColorType::GrayAlpha),
6 => Ok(ColorType::RGBA),
_ => Err(format!("Color type {} is not valid", value)),
}
}
}
// PNG signature, plus the initial IHDR chunk header.
const PNG_SIGNATURE: [u8; 16] = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, b'I', b'H', b'D', b'R'];
fn parse_png(data: &[u8]) -> Result<Png, Box<dyn Error>> {
if data.len() < 29 {
return Err("not long enough to be a valid png".into());
}
// Check for the signature, plus the beginning of the IHDR chunk, which
// should be the first chunk in any valid png.
if data[0..16] != PNG_SIGNATURE {
return Err("png had invalid signature".into());
}
let width = u32::from_be_bytes(data[16..20].try_into()?);
let height = u32::from_be_bytes(data[20..24].try_into()?);
let bit_depth = data[24];
let color_type = ColorType::try_from(data[25])?;
let png = Png{
width,
height,
bit_depth,
color_type
};
Ok(png)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment