1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Embed image as pixel data `&[u8]` into binary at build time.

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})
  }

  /// Return a Image structure, in rust source code.
  ///
  /// Sample output: `Image{data: include_bytes!(".../buildtime-png-image.data"), width: 1024, height: 768}`
  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 {
  /// Simply calls Builder::default().
	pub fn new() -> Self {
    Builder::default()
	}

  /// Customize where the pixel .data files are placed in.
  ///
  /// Because absolute paths is used in the generated rust code, you do not
  /// have to worry about where these files are, usually.
  ///
  /// Default is OUT_DIR
  pub fn with_out_dir<P: AsRef<Path>>(&mut self, outdir: P) -> &mut Self {
    self.outdir = PathBuf::from(outdir.as_ref());
    self
  }

  /// Add a image.
  ///
  /// This method will read the image pointed to by `path_rel_to_root`, decode it, and
  /// emits relevant .data files into the out_dir perviously set (default to OUT_DIR).
  ///
  /// # Arguments
  ///
  /// * `path_rel_to_root` - Path of the image, relative to the directory where your `Cargo.toml`
  /// is placed.
  ///
  /// `ident` - An identifier for the image to use in your code. Must be valid rust identifiers, such
  /// as `image`, `left_arrow` and not `123`, `a/b`, `a b`, `a-b` or `a::b`.
	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
	}

  /// Emit the rust source file to $OUTDIR/image.rs
  pub fn emit_source_file(&self) -> io::Result<()> {
    let outpath = self.outdir.join("images.rs");
    self.emit_source_file_at(outpath)
  }

  /// Emit the rust source file to the provided path, relative to the output directory.
  ///
  /// Source file contains absolute paths, so you don't have to worry about the placement
  /// of it.
  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(())
  }
}

// use crate::Image;
// pub static r#i: Image = Image{data: &[], width: 0, height: 0};