buildtime_png/
lib.rs

1//! Embed image as pixel data `&[u8]` into binary at build time.
2
3use png;
4use std::path::{Path, PathBuf};
5use std::fs::File;
6use std::env;
7use std::io;
8use std::fmt;
9use std::fmt::{Display, Formatter};
10use std::io::Write;
11
12pub struct EmittedImage {
13  ident: String,
14  src: PathBuf,
15  width: u32,
16  height: u32,
17}
18
19pub enum ErrorKind {
20  IOErrorReading(PathBuf, io::Error),
21  IOErrorWriting(PathBuf, io::Error),
22  DecodingError(PathBuf, png::DecodingError),
23  InvalidIdent,
24}
25pub struct Error {
26  err: ErrorKind,
27  image_ident: String,
28}
29
30impl Display for Error {
31  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
32    f.write_str("buildtime-png: ")?;
33    match &self.err {
34      ErrorKind::IOErrorReading(path, e) => write!(f, "error reading {}: {}", path.to_string_lossy(), &e),
35      ErrorKind::IOErrorWriting(path, e) => write!(f, "error writing {}: {}", path.to_string_lossy(), &e),
36      ErrorKind::DecodingError(path, e) => write!(f, "error decoding {}: {}", path.to_string_lossy(), &e),
37      ErrorKind::InvalidIdent => write!(f, "identifier {:?} is not a valid rust identifier.", &self.image_ident),
38    }
39  }
40}
41
42impl EmittedImage {
43  fn is_valid_ident(ident: &str) -> bool {
44    if ident.len() == 0 {
45      return false;
46    }
47    let allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
48    for c in ident.chars() {
49      if allowed_chars.find(c).is_none() {
50        return false;
51      }
52    }
53    let digits = "0123456789";
54    if digits.find(ident.chars().next().unwrap()).is_some() {
55      return false;
56    }
57    true
58  }
59
60  fn emit<P1: AsRef<Path>, P2: AsRef<Path>>(root: P1, outdir: P1, path_rel_to_root: P2, ident: &str) -> Result<Self, Error> {
61    let ident = String::from(ident);
62    if !EmittedImage::is_valid_ident(&ident) {
63      return Err(Error{err: ErrorKind::InvalidIdent, image_ident: ident});
64    }
65    let inpath = root.as_ref().join(path_rel_to_root);
66    let f = match File::open(&inpath) {
67      Ok(f) => f,
68      Err(e) => return Err(Error{err: ErrorKind::IOErrorReading(inpath, e), image_ident: ident}),
69    };
70    let (info, mut reader) = match png::Decoder::new(f).read_info() {
71      Ok(k) => k,
72      Err(e) => return Err(Error{err: ErrorKind::DecodingError(inpath, e), image_ident: ident}),
73    };
74    let outpath = outdir.as_ref().join(format!("buildtime-png-{}.data", ident));
75    let mut outf = match std::fs::OpenOptions::new().create(true).truncate(true).write(true).open(&outpath) {
76      Ok(f) => f,
77      Err(e) => return Err(Error{err: ErrorKind::IOErrorWriting(outpath, e), image_ident: ident}),
78    };
79    let mut frame = vec![0u8; info.buffer_size()];
80    match reader.next_frame(frame.as_mut_slice()) {
81      Ok(()) => {},
82      Err(e) => return Err(Error{err: ErrorKind::DecodingError(inpath, e), image_ident: ident}),
83    };
84    match outf.write_all(frame.as_slice()) {
85      Ok(()) => {},
86      Err(e) => return Err(Error{err: ErrorKind::IOErrorWriting(outpath, e), image_ident: ident}),
87    };
88    Ok(Self{ident, src: outpath, width: info.width, height: info.height})
89  }
90
91  /// Return a Image structure, in rust source code.
92  ///
93  /// Sample output: `Image{data: include_bytes!(".../buildtime-png-image.data"), width: 1024, height: 768}`
94  pub fn to_source(&self) -> String {
95    format!(r#"Image{{data: include_bytes!({src:?}), width: {width:?}, height: {height:?}}}"#,
96      src = self.src.canonicalize().unwrap(),
97      width = self.width,
98      height = self.height)
99  }
100}
101
102#[test]
103fn is_valid_ident_test() {
104  assert_eq!(EmittedImage::is_valid_ident("aaaa"), true);
105  assert_eq!(EmittedImage::is_valid_ident("1234"), false);
106  assert_eq!(EmittedImage::is_valid_ident("1aaa"), false);
107  assert_eq!(EmittedImage::is_valid_ident("aaa1"), true);
108  assert_eq!(EmittedImage::is_valid_ident("a1"), true);
109  assert_eq!(EmittedImage::is_valid_ident("1a"), false);
110  assert_eq!(EmittedImage::is_valid_ident("1"), false);
111  assert_eq!(EmittedImage::is_valid_ident(""), false);
112  assert_eq!(EmittedImage::is_valid_ident("aaaa/aaa"), false);
113  assert_eq!(EmittedImage::is_valid_ident("aaaa::aaa"), false);
114  assert_eq!(EmittedImage::is_valid_ident("aaaa\naaa"), false);
115  assert_eq!(EmittedImage::is_valid_ident("aaa bbb"), false);
116  assert_eq!(EmittedImage::is_valid_ident(" bbb"), false);
117  assert_eq!(EmittedImage::is_valid_ident("bbb "), false);
118  assert_eq!(EmittedImage::is_valid_ident("r#aaa"), false);
119}
120
121pub struct Builder {
122	images: Vec<EmittedImage>,
123  root: PathBuf,
124  outdir: PathBuf,
125}
126
127impl Default for Builder {
128  fn default() -> Self {
129    Self{images: Vec::new(), root: PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()), outdir: PathBuf::from(env::var_os("OUT_DIR").unwrap())}
130  }
131}
132
133impl Builder {
134  /// Simply calls Builder::default().
135	pub fn new() -> Self {
136    Builder::default()
137	}
138
139  /// Customize where the pixel .data files are placed in.
140  ///
141  /// Because absolute paths is used in the generated rust code, you do not
142  /// have to worry about where these files are, usually.
143  ///
144  /// Default is OUT_DIR
145  pub fn with_out_dir<P: AsRef<Path>>(&mut self, outdir: P) -> &mut Self {
146    self.outdir = PathBuf::from(outdir.as_ref());
147    self
148  }
149
150  /// Add a image.
151  ///
152  /// This method will read the image pointed to by `path_rel_to_root`, decode it, and
153  /// emits relevant .data files into the out_dir perviously set (default to OUT_DIR).
154  ///
155  /// # Arguments
156  ///
157  /// * `path_rel_to_root` - Path of the image, relative to the directory where your `Cargo.toml`
158  /// is placed.
159  ///
160  /// `ident` - An identifier for the image to use in your code. Must be valid rust identifiers, such
161  /// as `image`, `left_arrow` and not `123`, `a/b`, `a b`, `a-b` or `a::b`.
162	pub fn include_png<P: AsRef<Path>>(&mut self, path_rel_to_root: P, ident: &str) -> &mut Self {
163    let em = match EmittedImage::emit(&self.root, &self.outdir, path_rel_to_root, ident) {
164      Ok(em) => em,
165      Err(e) => {
166        eprint!("{}\n\n", &e);
167        panic!(e);
168      },
169    };
170    self.images.push(em);
171		self
172	}
173
174  /// Emit the rust source file to $OUTDIR/image.rs
175  pub fn emit_source_file(&self) -> io::Result<()> {
176    let outpath = self.outdir.join("images.rs");
177    self.emit_source_file_at(outpath)
178  }
179
180  /// Emit the rust source file to the provided path, relative to the output directory.
181  ///
182  /// Source file contains absolute paths, so you don't have to worry about the placement
183  /// of it.
184  pub fn emit_source_file_at<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
185    let mut f = std::fs::OpenOptions::new().create(true).truncate(true).write(true).open(self.outdir.join(path))?;
186    f.write_all(b"\
187pub struct Image {
188  pub data: &'static [u8],
189  pub width: u32,
190  pub height: u32
191}
192
193")?;
194    for i in &self.images {
195      f.write_fmt(format_args!("pub static r#{}: Image = {};\n", &i.ident, &i.to_source()))?;
196    }
197    Ok(())
198  }
199}
200
201// use crate::Image;
202// pub static r#i: Image = Image{data: &[], width: 0, height: 0};