texrender/
lib.rs

1//! LaTeX-rendering
2//!
3//! A thin wrapper around external tools like `latexmk`. See `TexRender` for details.
4//!
5//! Also supports generation of LaTeX documents, see the `tpl` module.
6
7pub mod tex_escape;
8pub mod tpl;
9
10use std::{
11    ffi::{OsStr, OsString},
12    fs, io, path, process,
13};
14use thiserror::Error;
15
16/// LaTeX-rendering command.
17///
18/// Creating a new rendering command usually starts by supplying a LaTeX-document, either via
19/// `from_bytes` or `from_file`. Other options can be set through the various builder methods.
20///
21/// Once satisfied, the `render` method will perform the call to the external TeX-engine and return
22/// a rendered PDF as raw bytes.
23///
24/// # TEXINPUTS
25///
26/// The search path for classes, includes or other files used in TeX can be extended using the
27/// `TEXINPUTS` environment variable; `TexRender` sets this variable when rendering. See
28/// `add_texinput` for details.
29///
30/// # Assets
31///
32/// Instead of adding a folder to `TEXINPUTS`, any sort of external file can be added as an asset.
33/// Assets are stored in a temporary folder that lives as long as the `TexRender` instance, the
34/// folder will automatically be added to `TEXINPUTS` when rendering. See the `add_asset_*`
35/// functions for details.
36#[derive(Debug)]
37pub struct TexRender {
38    /// Content to render.
39    source: Vec<u8>,
40    /// A number of folders to add to `TEXINPUTS`.
41    texinputs: Vec<path::PathBuf>,
42    /// Path to latexmk.
43    latex_mk_path: path::PathBuf,
44    /// Whether or not to use XeLaTeX.
45    use_xelatex: bool,
46    /// Whether or not to allow shell escaping.
47    allow_shell_escape: bool,
48    /// Temporary directory holding assets to be included.
49    assets_dir: Option<tempdir::TempDir>,
50}
51
52/// Error occuring during rendering.
53#[derive(Debug, Error)]
54pub enum RenderingError {
55    /// Temporary directry could not be created.
56    #[error("could not create temporary directory: {0}")]
57    TempdirCreation(io::Error),
58    /// Writing the input file failed.
59    #[error("could not write input file: {0}")]
60    WriteInputFile(io::Error),
61    /// Reading the resulting output file failed.
62    #[error("could not read output file: {0}")]
63    ReadOutputFile(io::Error),
64    /// Could not run LaTeX rendering command.
65    #[error("could not run latexmk: {0}")]
66    RunError(io::Error),
67    /// latexmk failed.
68    #[error("LaTeX failure: {stdout:?} {stderr:?}")]
69    LatexError {
70        /// Process exit code.
71        status: Option<i32>,
72        /// Content of stdout.
73        stdout: Vec<u8>,
74        /// Content of stderr.
75        stderr: Vec<u8>,
76    },
77}
78
79impl TexRender {
80    /// Create a new tex render configuration using raw input bytes as the source file.
81    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    /// Create a new tex render configuration from an input latex file.
93    pub fn from_file<P: AsRef<path::Path>>(source: P) -> io::Result<TexRender> {
94        Ok(Self::from_bytes(fs::read(source)?))
95    }
96
97    /// Adds an asset to the texrender.
98    pub fn add_asset_from_bytes<S: AsRef<OsStr>>(
99        &mut self,
100        filename: S,
101        bytes: &[u8],
102    ) -> io::Result<()> {
103        // Initialize assets dir, if not present.
104        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    /// Adds an assets to the texrender from a file.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the passed-in path has no proper filename.
125    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    /// Adds a path to list of texinputs.
134    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    /// Sets the path of `latexmk`.
140    ///
141    /// If not set, will look for `latexmk` on the current `PATH`.
142    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    /// Renders the given source as PDF.
148    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            // latexmk failed,
186            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}