1pub mod tex_escape;
8pub mod tpl;
9
10use std::{
11 ffi::{OsStr, OsString},
12 fs, io, path, process,
13};
14use thiserror::Error;
15
16#[derive(Debug)]
37pub struct TexRender {
38 source: Vec<u8>,
40 texinputs: Vec<path::PathBuf>,
42 latex_mk_path: path::PathBuf,
44 use_xelatex: bool,
46 allow_shell_escape: bool,
48 assets_dir: Option<tempdir::TempDir>,
50}
51
52#[derive(Debug, Error)]
54pub enum RenderingError {
55 #[error("could not create temporary directory: {0}")]
57 TempdirCreation(io::Error),
58 #[error("could not write input file: {0}")]
60 WriteInputFile(io::Error),
61 #[error("could not read output file: {0}")]
63 ReadOutputFile(io::Error),
64 #[error("could not run latexmk: {0}")]
66 RunError(io::Error),
67 #[error("LaTeX failure: {stdout:?} {stderr:?}")]
69 LatexError {
70 status: Option<i32>,
72 stdout: Vec<u8>,
74 stderr: Vec<u8>,
76 },
77}
78
79impl TexRender {
80 pub fn from_bytes(source: Vec<u8>) -> TexRender {
82 TexRender {
83 source,
84 texinputs: Vec::new(),
85 latex_mk_path: "latexmk".into(),
86 use_xelatex: true,
87 allow_shell_escape: false,
88 assets_dir: None,
89 }
90 }
91
92 pub fn from_file<P: AsRef<path::Path>>(source: P) -> io::Result<TexRender> {
94 Ok(Self::from_bytes(fs::read(source)?))
95 }
96
97 pub fn add_asset_from_bytes<S: AsRef<OsStr>>(
99 &mut self,
100 filename: S,
101 bytes: &[u8],
102 ) -> io::Result<()> {
103 let assets_path = match self.assets_dir {
105 Some(ref assets_dir) => assets_dir.path(),
106 None => {
107 let assets_dir = tempdir::TempDir::new("texrender-assets")?;
108 self.texinputs.push(assets_dir.path().to_owned());
109 self.assets_dir = Some(assets_dir);
110 &self.texinputs[self.texinputs.len() - 1]
111 }
112 };
113
114 let output_fn = assets_path.join(filename.as_ref());
115 fs::create_dir_all(output_fn.parent().expect("filename has no parent?"))?;
116
117 fs::write(output_fn, bytes)
118 }
119
120 pub fn add_asset_from_file<P: AsRef<path::Path>>(&mut self, path: P) -> io::Result<()> {
126 let source = path.as_ref();
127 let filename = source.file_name().expect("file has no filename");
128
129 let buf = fs::read(source)?;
130 self.add_asset_from_bytes(filename, &buf)
131 }
132
133 pub fn add_texinput<P: Into<path::PathBuf>>(&mut self, input_path: P) -> &mut Self {
135 self.texinputs.push(input_path.into());
136 self
137 }
138
139 pub fn latex_mk_path<P: Into<path::PathBuf>>(&mut self, latex_mk_path: P) -> &mut Self {
143 self.latex_mk_path = latex_mk_path.into();
144 self
145 }
146
147 pub fn render(&self) -> Result<Vec<u8>, RenderingError> {
149 let tmp = tempdir::TempDir::new("texrender").map_err(RenderingError::TempdirCreation)?;
150 let input_file = tmp.path().join("input.tex");
151 let output_file = tmp.path().join("input.pdf");
152
153 let mut texinputs = OsString::new();
154 for input in &self.texinputs {
155 texinputs.push(":");
156 texinputs.push(input.as_os_str());
157 }
158
159 fs::write(&input_file, &self.source).map_err(RenderingError::WriteInputFile)?;
160
161 let mut cmd = process::Command::new(&self.latex_mk_path);
162 cmd.args(&[
163 "-interaction=nonstopmode",
164 "-halt-on-error",
165 "-file-line-error",
166 "-pdf",
167 ]);
168
169 if self.use_xelatex {
170 cmd.arg("-xelatex");
171 }
172
173 if !self.allow_shell_escape {
174 cmd.arg("-no-shell-escape");
175 }
176
177 cmd.arg(&input_file);
178
179 cmd.env("TEXINPUTS", texinputs);
180 cmd.current_dir(tmp.path());
181
182 let output = cmd.output().map_err(RenderingError::RunError)?;
183
184 if !output.status.success() {
185 return Err(RenderingError::LatexError {
187 status: output.status.code(),
188 stdout: output.stdout,
189 stderr: output.stderr,
190 });
191 }
192
193 fs::read(output_file).map_err(RenderingError::ReadOutputFile)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::{RenderingError, TexRender};
200
201 #[test]
202 fn render_example_tex() {
203 let doc = r"
204 \documentclass{article}
205 \begin{document}
206 hello, world.
207 \end{document}
208 ";
209
210 let tex = TexRender::from_bytes(doc.into());
211 let _pdf = tex.render().unwrap();
212 }
213
214 #[test]
215 fn broken_tex_gives_correct_error() {
216 let doc = r"
217 \documentSOBROKENclass{article}
218 ";
219
220 let tex = TexRender::from_bytes(doc.into());
221
222 match tex.render() {
223 Err(RenderingError::LatexError { .. }) => (),
224 other => panic!("expected latex error, got {:?}", other),
225 }
226 }
227}