ytesrev/latex/
render.rs

1//! The LaTeX renderer. This is quite low level, and you probably don't want to use this. Instead,
2//! use [`LatexObj`], which contains a better, more high-level way to handle LaTeX in your
3//! presentation.
4//!
5//! ---
6//!
7//! ## The rendereing process:
8//!
9//! 1. Collect all LaTeX expressions into a file, saved in /tmp/ytesrev/tmp.tex
10//! 2. Run `pdflatex` on the file
11//! 3. Run `pdfcrop` on to make all expressions the right size
12//! 4. Run `pdftoppm` on the resulting `.pdf`-files to generate `.png`-files of all the expressions
13//! 5. (Done for each `LatexObj`) Load the `.png`-file into a `PngImage`
14//!
15//! [`LatexObj`]: ../latex_obj/struct.LatexObj.html
16
17extern crate sdl2;
18extern crate tempfile;
19
20use std::fs::{create_dir, remove_dir_all, File};
21use std::io::{Error, ErrorKind, Result as IResult, Write};
22use std::mem::drop;
23use std::path::Path;
24use std::process::{exit, Command};
25use std::sync::Mutex;
26use std::time::Instant;
27
28use image::PngImage;
29use tempfile::tempdir;
30
31const LATEX_PRELUDE: &str = include_str!("latex_prelude.tex");
32const LATEX_POSTLUDE: &str = "\\end{document}";
33
34/// An error that might occur when rendering LaTeX expressions
35#[derive(Debug, PartialEq)]
36pub enum LatexError {
37    /// The specified LaTeX expression wasn't registered. This error should be impossible to get,
38    /// as to get it you need an invalid index. See [`LatexIdx`]
39    ///
40    /// [`LatexIdx`]: ../latex/latex_obj/struct.LatexObj.html
41    NotExisting,
42    /// The LaTeX document hasn't been rendered yet. Run the [`render_all_equations`]
43    NotLoaded,
44}
45
46/// An index given to each [`LatexObj`], as they are all rendered in the same document
47/// The only way to obtain an index is to register an equation using `register_equation`,
48/// and as such, an invalid index should be impossible to obtain.
49///
50/// [`LatexObj`]: ../latex_obj/struct.LatexObj.html
51pub struct LatexIdx(usize);
52
53lazy_static! {
54    static ref EQUATIONS: Mutex<Vec<(&'static str, bool, Option<PngImage>)>> =
55        Mutex::new(Vec::new());
56    static ref PRELUDE: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
57}
58
59/// Register an equation to be rendered. To render, use the [`render_all_equations`] method.
60///
61/// ```
62/// use ytesrev::latex::render::*;
63/// # fn make_invalid_idx() -> LatexIdx {
64/// #   use std::mem::transmute;
65/// #   unsafe { transmute::<usize, LatexIdx>(0) }
66/// # }
67/// let invalid_idx = make_invalid_idx(); // This is impossible to do, this is only for demonstration
68/// assert_eq!(read_image(invalid_idx).err(), Some(LatexError::NotExisting));
69///
70/// let valid_idx = register_equation("a^2 + b^2 = c+2", false);
71/// assert_eq!(read_image(valid_idx).err(), Some(LatexError::NotLoaded));
72/// ```
73pub fn register_equation(equation: &'static str, is_text: bool) -> LatexIdx {
74    if let Ok(ref mut eqs) = EQUATIONS.lock() {
75        let idx = eqs.len();
76        eqs.push((equation, is_text, None));
77        LatexIdx(idx)
78    } else {
79        panic!("Can't eqs");
80    }
81}
82
83/// Add prelude to the LaTeX render.
84///
85/// ```
86/// use ytesrev::latex::render::add_prelude;
87///
88/// add_prelude("\\usepackage{skull}");
89/// ```
90///
91/// By default, amsmath is loaded, but nothing else.
92///
93pub fn add_prelude(prelude: &'static str) {
94    if let Ok(ref mut preludes) = PRELUDE.lock() {
95        preludes.push(prelude);
96    }
97    // TODO: Handle Mutex lock fail
98}
99
100/// Reads an image from an LatexIdx.
101pub fn read_image(idx: LatexIdx) -> Result<PngImage, LatexError> {
102    let res = if let Ok(ref mut eqs) = EQUATIONS.lock() {
103        if let Some(ref mut x) = eqs.get_mut(idx.0) {
104            if x.2.is_some() {
105                Ok(x.2.take().unwrap())
106            } else {
107                Err(LatexError::NotLoaded)
108            }
109        } else {
110            Err(LatexError::NotExisting)
111        }
112    } else {
113        Err(LatexError::NotLoaded)
114    };
115    drop(idx);
116    res
117}
118
119/// Run the rendering process. This takes a few seconds.
120///
121/// As with everything in this module, you probably don't want to do this yourself as this is
122/// automatically handled by the [`WindowManager`].
123///
124/// [`WindowManager`]: ../../window/struct.WindowManager.html
125pub fn render_all_equations() -> IResult<()> {
126    if let Ok(eqs) = EQUATIONS.lock() {
127        if eqs.len() == 0 {
128            return Ok(());
129        }
130    }
131    let fallback = Path::new("/tmp/ytesrev").to_path_buf();
132    let path = tempdir().map(|x| x.into_path()).unwrap_or(fallback);
133
134    eprintln!("Rendering in {}", path.display());
135
136    if path.exists() {
137        remove_dir_all(path.clone())?;
138    }
139    create_dir(path.clone())?;
140
141    let mut tex_path = path.clone();
142    tex_path.push("tmp.tex");
143
144    let mut pdf_path = path.clone();
145    pdf_path.push("tmp.pdf");
146
147    let mut raw_path = path.clone();
148    raw_path.push("tmp-res");
149
150    let start = Instant::now();
151
152    create_tex(&tex_path)?;
153
154    render_tex(&tex_path, &pdf_path, &raw_path)?;
155
156    read_pngs(&path)?;
157
158    let diff = Instant::now() - start;
159    eprintln!("Rendering took {:.2?}", diff);
160
161    Ok(())
162}
163
164fn create_tex(tex_path: &Path) -> IResult<()> {
165    let mut tex_file = File::create(tex_path)?;
166    let mut added_prelude = String::new();
167    if let Ok(prelude) = PRELUDE.lock() {
168        prelude.iter().for_each(|prelude| {
169            added_prelude.push_str(prelude);
170            added_prelude.push('\n');
171        });
172    }
173
174    writeln!(
175        tex_file,
176        "{}",
177        LATEX_PRELUDE.replace("$PRELUDE", &added_prelude)
178    )?;
179
180    if let Ok(eqs) = EQUATIONS.lock() {
181        for equation in eqs.iter() {
182            for col in &["red", "blue"] {
183                writeln!(tex_file, "\\begin{{equation*}}")?;
184                writeln!(tex_file, "\\colorbox{{{}}}{{\\makebox[\\linewidth]{{", col)?;
185                if equation.1 {
186                    writeln!(tex_file, "{}", equation.0)?;
187                } else {
188                    writeln!(tex_file, "$ {} $", equation.0)?;
189                }
190                writeln!(tex_file, "}} }}")?;
191                writeln!(tex_file, "\\end{{equation*}}")?;
192            }
193        }
194    }
195
196    writeln!(tex_file, "{}", LATEX_POSTLUDE)?;
197
198    Ok(())
199}
200
201fn render_tex(tex_path: &Path, pdf_path: &Path, raw_path: &Path) -> IResult<()> {
202    let out = Command::new("pdflatex")
203        .current_dir(tex_path.parent().unwrap())
204        .arg(tex_path.file_name().unwrap())
205        .output()
206        .expect("Can't make command");
207
208    if !out.status.success() {
209        eprintln!("Latex compile error:");
210        eprintln!("{}", String::from_utf8_lossy(&out.stderr));
211        exit(1);
212    }
213
214    let out = Command::new("pdftoppm")
215        .arg(pdf_path.as_os_str())
216        .arg(raw_path.as_os_str())
217        .arg("-r")
218        .arg("250")
219        .arg("-png")
220        .output()
221        .expect("Can't make command");
222
223    if !out.status.success() {
224        eprintln!("pdftoppm error");
225        eprintln!("{}", String::from_utf8_lossy(&out.stderr));
226        exit(1);
227    }
228
229    Ok(())
230}
231
232fn read_pngs(path: &Path) -> IResult<()> {
233    if let Ok(ref mut eqs) = EQUATIONS.lock() {
234        let digits_max = format!("{}", eqs.len()).len();
235
236        for (i, (_, _, ref mut im)) in eqs.iter_mut().enumerate() {
237            let num_red = zero_pad(format!("{}", 2 * i + 1), digits_max);
238            let num_blue = zero_pad(format!("{}", 2 * i + 2), digits_max);
239
240            let mut img_path_red = path.to_path_buf();
241            img_path_red.push(format!("tmp-res-{}.png", num_red));
242
243            let mut img_path_blue = path.to_path_buf();
244            img_path_blue.push(format!("tmp-res-{}.png", num_blue));
245
246            let mut im_red_res = PngImage::load_from_path(File::open(img_path_red)?)
247                .map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
248
249            let im_blue = PngImage::load_from_path(File::open(img_path_blue)?)
250                .map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
251
252            let mut maxx = 0;
253            let mut maxy = 0;
254            let mut minx = im_red_res.width;
255            let mut miny = im_red_res.height;
256
257            for i in 0..im_red_res.width * im_red_res.height {
258                let x = i % im_red_res.width;
259                let y = i / im_red_res.width;
260
261                let rr = im_red_res.data[4 * i];
262                let rg = im_red_res.data[4 * i];
263                let rb = im_red_res.data[4 * i + 2];
264
265                let br = im_blue.data[4 * i];
266                let bb = im_blue.data[4 * i + 2];
267
268                let rdiff = rr as i16 - br as i16;
269                let bdiff = bb as i16 - rb as i16;
270
271                let alpha = 255 - (rdiff + bdiff) / 2;
272                let alpha = alpha.min(255).max(0) as u8;
273
274                im_red_res.data[4 * i] = br;
275                im_red_res.data[4 * i + 2] = rb;
276                im_red_res.data[4 * i + 3] = alpha;
277
278                if (br < 250 || rg < 250 || rb < 250) && alpha > 250 {
279                    maxx = maxx.max(x + 1);
280                    maxy = maxy.max(y + 1);
281
282                    minx = minx.min(x);
283                    miny = miny.min(y);
284                }
285            }
286            // Margins
287            maxx = (maxx + 3).min(im_red_res.width - 1);
288            maxy = (maxy + 3).min(im_red_res.height - 1);
289            minx = minx.saturating_sub(3);
290            miny = miny.saturating_sub(3);
291
292            let width = maxx - minx;
293            let height = maxy - miny;
294            let mut resdata = vec![0; 4 * width * height];
295
296            for x in 0..width {
297                for y in 0..height {
298                    let i_r = y * width + x;
299                    let i_l = (y + miny) * im_red_res.width + x + minx;
300
301                    resdata[4 * i_r] = im_red_res.data[4 * i_l];
302                    resdata[4 * i_r + 1] = im_red_res.data[4 * i_l + 1];
303                    resdata[4 * i_r + 2] = im_red_res.data[4 * i_l + 2];
304                    resdata[4 * i_r + 3] = im_red_res.data[4 * i_l + 3];
305                }
306            }
307
308            *im = Some(PngImage {
309                data: resdata,
310                width,
311                height,
312            });
313        }
314    }
315    Ok(())
316}
317
318fn zero_pad(n: String, len: usize) -> String {
319    let needed = len.saturating_sub(n.len());
320    let mut res = (0..needed).map(|_| '0').collect::<String>();
321    res.push_str(&n);
322    res
323}