use png;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::env;
use std::io;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io::Write;
pub struct EmittedImage {
ident: String,
src: PathBuf,
width: u32,
height: u32,
}
pub enum ErrorKind {
IOErrorReading(PathBuf, io::Error),
IOErrorWriting(PathBuf, io::Error),
DecodingError(PathBuf, png::DecodingError),
InvalidIdent,
}
pub struct Error {
err: ErrorKind,
image_ident: String,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("buildtime-png: ")?;
match &self.err {
ErrorKind::IOErrorReading(path, e) => write!(f, "error reading {}: {}", path.to_string_lossy(), &e),
ErrorKind::IOErrorWriting(path, e) => write!(f, "error writing {}: {}", path.to_string_lossy(), &e),
ErrorKind::DecodingError(path, e) => write!(f, "error decoding {}: {}", path.to_string_lossy(), &e),
ErrorKind::InvalidIdent => write!(f, "identifier {:?} is not a valid rust identifier.", &self.image_ident),
}
}
}
impl EmittedImage {
fn is_valid_ident(ident: &str) -> bool {
if ident.len() == 0 {
return false;
}
let allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
for c in ident.chars() {
if allowed_chars.find(c).is_none() {
return false;
}
}
let digits = "0123456789";
if digits.find(ident.chars().next().unwrap()).is_some() {
return false;
}
true
}
fn emit<P1: AsRef<Path>, P2: AsRef<Path>>(root: P1, outdir: P1, path_rel_to_root: P2, ident: &str) -> Result<Self, Error> {
let ident = String::from(ident);
if !EmittedImage::is_valid_ident(&ident) {
return Err(Error{err: ErrorKind::InvalidIdent, image_ident: ident});
}
let inpath = root.as_ref().join(path_rel_to_root);
let f = match File::open(&inpath) {
Ok(f) => f,
Err(e) => return Err(Error{err: ErrorKind::IOErrorReading(inpath, e), image_ident: ident}),
};
let (info, mut reader) = match png::Decoder::new(f).read_info() {
Ok(k) => k,
Err(e) => return Err(Error{err: ErrorKind::DecodingError(inpath, e), image_ident: ident}),
};
let outpath = outdir.as_ref().join(format!("buildtime-png-{}.data", ident));
let mut outf = match std::fs::OpenOptions::new().create(true).truncate(true).write(true).open(&outpath) {
Ok(f) => f,
Err(e) => return Err(Error{err: ErrorKind::IOErrorWriting(outpath, e), image_ident: ident}),
};
let mut frame = vec![0u8; info.buffer_size()];
match reader.next_frame(frame.as_mut_slice()) {
Ok(()) => {},
Err(e) => return Err(Error{err: ErrorKind::DecodingError(inpath, e), image_ident: ident}),
};
match outf.write_all(frame.as_slice()) {
Ok(()) => {},
Err(e) => return Err(Error{err: ErrorKind::IOErrorWriting(outpath, e), image_ident: ident}),
};
Ok(Self{ident, src: outpath, width: info.width, height: info.height})
}
pub fn to_source(&self) -> String {
format!(r#"Image{{data: include_bytes!({src:?}), width: {width:?}, height: {height:?}}}"#,
src = self.src.canonicalize().unwrap(),
width = self.width,
height = self.height)
}
}
#[test]
fn is_valid_ident_test() {
assert_eq!(EmittedImage::is_valid_ident("aaaa"), true);
assert_eq!(EmittedImage::is_valid_ident("1234"), false);
assert_eq!(EmittedImage::is_valid_ident("1aaa"), false);
assert_eq!(EmittedImage::is_valid_ident("aaa1"), true);
assert_eq!(EmittedImage::is_valid_ident("a1"), true);
assert_eq!(EmittedImage::is_valid_ident("1a"), false);
assert_eq!(EmittedImage::is_valid_ident("1"), false);
assert_eq!(EmittedImage::is_valid_ident(""), false);
assert_eq!(EmittedImage::is_valid_ident("aaaa/aaa"), false);
assert_eq!(EmittedImage::is_valid_ident("aaaa::aaa"), false);
assert_eq!(EmittedImage::is_valid_ident("aaaa\naaa"), false);
assert_eq!(EmittedImage::is_valid_ident("aaa bbb"), false);
assert_eq!(EmittedImage::is_valid_ident(" bbb"), false);
assert_eq!(EmittedImage::is_valid_ident("bbb "), false);
assert_eq!(EmittedImage::is_valid_ident("r#aaa"), false);
}
pub struct Builder {
images: Vec<EmittedImage>,
root: PathBuf,
outdir: PathBuf,
}
impl Default for Builder {
fn default() -> Self {
Self{images: Vec::new(), root: PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()), outdir: PathBuf::from(env::var_os("OUT_DIR").unwrap())}
}
}
impl Builder {
pub fn new() -> Self {
Builder::default()
}
pub fn with_out_dir<P: AsRef<Path>>(&mut self, outdir: P) -> &mut Self {
self.outdir = PathBuf::from(outdir.as_ref());
self
}
pub fn include_png<P: AsRef<Path>>(&mut self, path_rel_to_root: P, ident: &str) -> &mut Self {
let em = match EmittedImage::emit(&self.root, &self.outdir, path_rel_to_root, ident) {
Ok(em) => em,
Err(e) => {
eprint!("{}\n\n", &e);
panic!(e);
},
};
self.images.push(em);
self
}
pub fn emit_source_file(&self) -> io::Result<()> {
let outpath = self.outdir.join("images.rs");
self.emit_source_file_at(outpath)
}
pub fn emit_source_file_at<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let mut f = std::fs::OpenOptions::new().create(true).truncate(true).write(true).open(self.outdir.join(path))?;
f.write_all(b"\
pub struct Image {
pub data: &'static [u8],
pub width: u32,
pub height: u32
}
")?;
for i in &self.images {
f.write_fmt(format_args!("pub static r#{}: Image = {};\n", &i.ident, &i.to_source()))?;
}
Ok(())
}
}