use std::{
ffi::{OsStr, OsString},
fs, io, path, process,
};
use thiserror::Error;
#[derive(Debug)]
pub struct TexRender {
source: Vec<u8>,
texinputs: Vec<path::PathBuf>,
latex_mk_path: path::PathBuf,
use_xelatex: bool,
allow_shell_escape: bool,
assets_dir: Option<tempdir::TempDir>,
}
#[derive(Debug, Error)]
pub enum RenderingError {
#[error("could not create temporary directory: {0}")]
TempdirCreation(io::Error),
#[error("could not write input file: {0}")]
WriteInputFile(io::Error),
#[error("could not read output file: {0}")]
ReadOutputFile(io::Error),
#[error("could not run latexmk: {0}")]
RunError(io::Error),
#[error("LaTeX failure: {stdout:?} {stderr:?}")]
LatexError {
status: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
},
}
impl TexRender {
pub fn from_bytes(source: Vec<u8>) -> TexRender {
TexRender {
source,
texinputs: Vec::new(),
latex_mk_path: "latexmk".into(),
use_xelatex: true,
allow_shell_escape: false,
assets_dir: None,
}
}
pub fn from_file<P: AsRef<path::Path>>(source: P) -> io::Result<TexRender> {
Ok(Self::from_bytes(fs::read(source)?))
}
pub fn add_asset_from_bytes<S: AsRef<OsStr>>(
&mut self,
filename: S,
bytes: &[u8],
) -> io::Result<()> {
let assets_path = match self.assets_dir {
Some(ref assets_dir) => assets_dir.path(),
None => {
let assets_dir = tempdir::TempDir::new("texrender-assets")?;
self.texinputs.push(assets_dir.path().to_owned());
self.assets_dir = Some(assets_dir);
&self.texinputs[self.texinputs.len() - 1]
}
};
let output_fn = assets_path.join(filename.as_ref());
fs::create_dir_all(output_fn.parent().expect("filename has no parent?"))?;
fs::write(output_fn, bytes)
}
pub fn add_asset_from_file<P: AsRef<path::Path>>(&mut self, path: P) -> io::Result<()> {
let source = path.as_ref();
let filename = source.file_name().expect("file has no filename");
let buf = fs::read(source)?;
self.add_asset_from_bytes(filename, &buf)
}
pub fn add_texinput<P: Into<path::PathBuf>>(&mut self, input_path: P) -> &mut Self {
self.texinputs.push(input_path.into());
self
}
pub fn render(&self) -> Result<Vec<u8>, RenderingError> {
let tmp = tempdir::TempDir::new("texrender").map_err(RenderingError::TempdirCreation)?;
let input_file = tmp.path().join("input.tex");
let output_file = tmp.path().join("input.pdf");
let mut texinputs = OsString::new();
for input in &self.texinputs {
texinputs.push(":");
texinputs.push(input.as_os_str());
}
fs::write(&input_file, &self.source).map_err(RenderingError::WriteInputFile)?;
let mut cmd = process::Command::new(&self.latex_mk_path);
cmd.args(&[
"-interaction=batchmode",
"-halt-on-error",
"-file-line-error",
"-pdf",
]);
if self.use_xelatex {
cmd.arg("-xelatex");
}
if !self.allow_shell_escape {
cmd.arg("-no-shell-escape");
}
cmd.arg(&input_file);
cmd.env("TEXINPUTS", texinputs);
cmd.current_dir(tmp.path());
let output = cmd.output().map_err(RenderingError::RunError)?;
if !output.status.success() {
return Err(RenderingError::LatexError {
status: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
});
}
fs::read(output_file).map_err(RenderingError::ReadOutputFile)
}
}
#[cfg(test)]
mod tests {
use super::{RenderingError, TexRender};
#[test]
fn render_example_tex() {
let doc = r"
\documentclass{article}
\begin{document}
hello, world.
\end{document}
";
let tex = TexRender::from_bytes(doc.into());
let _pdf = tex.render().unwrap();
}
#[test]
fn broken_tex_gives_correct_error() {
let doc = r"
\documentSOBROKENclass{article}
";
let tex = TexRender::from_bytes(doc.into());
match tex.render() {
Err(RenderingError::LatexError { .. }) => (),
other => panic!("expected latex error, got {:?}", other),
}
}
}