use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::panic::{AssertUnwindSafe, catch_unwind};
use ratex_layout::{LayoutOptions, layout, to_display_list};
use ratex_parser::parser::parse;
use ratex_render::{RenderOptions, render_to_png};
use ratex_types::color::Color;
use ratex_types::math_style::MathStyle;
use crate::image_cache::{ImageCache, LoadedImage, decode};
const PAD: f32 = 2.0;
const MAX_RASTER: f32 = 900.0;
pub struct MathBlock {
pub block: Range<usize>,
pub content: Range<usize>,
pub anchor_line: usize,
}
#[derive(Clone)]
pub struct MathJob {
pub latex: String,
pub display: bool,
pub font_px: f32,
pub scale: f32,
pub fg: (f32, f32, f32),
}
pub fn key_for(job: &MathJob) -> String {
let mut h = DefaultHasher::new();
job.latex.hash(&mut h);
job.display.hash(&mut h);
job.font_px.to_bits().hash(&mut h);
job.scale.to_bits().hash(&mut h);
let (r, g, b) = job.fg;
(r.to_bits(), g.to_bits(), b.to_bits()).hash(&mut h);
format!("math:{:016x}", h.finish())
}
fn render_to_image(job: &MathJob) -> Result<LoadedImage, Option<String>> {
let out = catch_unwind(AssertUnwindSafe(
|| -> Result<LoadedImage, Option<String>> {
let nodes = parse(&job.latex).map_err(|e| Some(e.to_string()))?;
let (r, g, b) = job.fg;
let opts = LayoutOptions {
style: if job.display {
MathStyle::Display
} else {
MathStyle::Text
},
color: Color { r, g, b, a: 1.0 },
..Default::default()
};
let dl = to_display_list(&layout(&nodes, &opts));
let total_h = (dl.height + dl.depth) as f32;
if dl.width <= 0.0 || total_h <= 0.0 {
return Err(None);
}
let em = job.font_px;
let base_w = dl.width as f32 * em + 2.0 * PAD;
let base_h = total_h * em + 2.0 * PAD;
let dpr = job.scale.min(MAX_RASTER / base_w.max(base_h)).max(0.01);
let ropts = RenderOptions {
font_size: em,
padding: PAD,
background_color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
},
font_dir: String::new(),
device_pixel_ratio: dpr,
};
let png = render_to_png(&dl, &ropts).map_err(|_| None)?;
let mut img = decode(&png).ok_or(None)?;
img.display_w = img.width as f32 / dpr;
img.display_h = img.height as f32 / dpr;
img.baseline = Some(dl.height as f32 * em + PAD);
Ok(img)
},
));
match out {
Ok(r) => r,
Err(_) => Err(None), }
}
pub fn spawn_math_renders(
jobs: &[(String, MathJob)],
cache: &ImageCache,
notify: impl Fn() + Send + Clone + 'static,
) {
for (key, job) in jobs {
if cache.contains(key) {
continue;
}
cache.mark_loading(key);
let cache = cache.clone();
let notify = notify.clone();
let key = key.clone();
let job = job.clone();
std::thread::spawn(move || {
cache.store_render(&key, render_to_image(&job));
notify();
});
}
}
pub fn render_math_blocking(jobs: &[(String, MathJob)], cache: &ImageCache) {
for (key, job) in jobs {
if cache.contains(key) {
continue;
}
cache.store_render(key, render_to_image(job));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image_cache::ImageState;
fn job(latex: &str, display: bool) -> MathJob {
MathJob {
latex: latex.to_string(),
display,
font_px: 16.0,
scale: 2.0,
fg: (0.9, 0.9, 0.9),
}
}
#[test]
fn renders_math_into_cache() {
let j = job("x^2 + \\frac{1}{2}", true);
let key = key_for(&j);
let cache = ImageCache::new();
render_math_blocking(&[(key.clone(), j)], &cache);
assert!(
matches!(cache.get(&key), Some(ImageState::Loaded(_))),
"valid LaTeX should render + decode"
);
}
#[test]
fn bad_latex_fails_without_panicking() {
let j = job("\\frac{ oops unbalanced", false);
let key = key_for(&j);
let cache = ImageCache::new();
render_math_blocking(&[(key.clone(), j)], &cache);
assert!(cache.get(&key).is_some());
}
#[test]
fn parse_error_surfaces_reason() {
let j = job("\\unknowncmd{x}", true);
let key = key_for(&j);
let cache = ImageCache::new();
render_math_blocking(&[(key.clone(), j)], &cache);
match cache.get(&key) {
Some(ImageState::Failed(Some(msg))) => assert!(!msg.is_empty()),
_ => panic!("expected Failed(Some(reason))"),
}
}
#[test]
fn key_stable_and_distinguishes_display() {
assert_eq!(key_for(&job("x", true)), key_for(&job("x", true)));
assert_ne!(key_for(&job("x", true)), key_for(&job("x", false)));
assert_ne!(key_for(&job("x", true)), key_for(&job("y", true)));
}
}