native/ratex_nif/src/lib.rs

use rustler::{Binary, Encoder, Env, Error, NewBinary, NifStruct, Term};

use ratex_types::display_item::DisplayList;
use ratex_types::{Color, MathStyle};

#[derive(NifStruct)]
#[module = "Ratex.Options"]
pub struct Options {
    pub font_size: f64,
    pub pixel_ratio: Option<f64>,
    pub color: String,
    pub inline: bool,
    pub unicode_font_path: Option<String>,
}

#[rustler::nif(schedule = "DirtyCpu")]
fn render_png<'a>(env: Env<'a>, latex: String, opts: Options) -> Result<Term<'a>, Error> {
    if let Some(path) = &opts.unicode_font_path {
        std::env::set_var("RATEX_UNICODE_FONT", path);
    }

    let result = std::panic::catch_unwind(|| do_render_png(&latex, &opts));

    match result {
        Ok(Ok(bytes)) => {
            let mut bin = NewBinary::new(env, bytes.len());
            bin.as_mut_slice().copy_from_slice(&bytes);
            let binary: Binary = bin.into();
            Ok((rustler::types::atom::ok(), binary).encode(env))
        }
        Ok(Err(msg)) => Ok((rustler::types::atom::error(), msg).encode(env)),
        Err(_panic) => Ok((rustler::types::atom::error(), "ratex_panic").encode(env)),
    }
}

#[rustler::nif(schedule = "DirtyCpu")]
fn render_svg<'a>(env: Env<'a>, latex: String, opts: Options) -> Result<Term<'a>, Error> {
    if let Some(path) = &opts.unicode_font_path {
        std::env::set_var("RATEX_UNICODE_FONT", path);
    }

    let result = std::panic::catch_unwind(|| do_render_svg(&latex, &opts));

    match result {
        Ok(Ok(svg)) => Ok((rustler::types::atom::ok(), svg).encode(env)),
        Ok(Err(msg)) => Ok((rustler::types::atom::error(), msg).encode(env)),
        Err(_panic) => Ok((rustler::types::atom::error(), "ratex_panic").encode(env)),
    }
}

fn build_display_list(latex: &str, inline: bool, color_hex: &str) -> Result<DisplayList, String> {
    let nodes = ratex_parser::parse(latex).map_err(|e| format!("parse error: {e:?}"))?;
    let mut layout_opts = ratex_layout::LayoutOptions::default();

    layout_opts.style = if inline {
        MathStyle::Text
    } else {
        MathStyle::Display
    };

    if let Some(c) = Color::parse(color_hex) {
        layout_opts.color = c;
    }

    let layout_box = ratex_layout::layout(&nodes, &layout_opts);

    Ok(ratex_layout::to_display_list(&layout_box))
}

fn do_render_png(latex: &str, opts: &Options) -> Result<Vec<u8>, String> {
    let dl = build_display_list(latex, opts.inline, &opts.color)?;
    let mut render_opts = ratex_render::RenderOptions::default();

    render_opts.font_size = opts.font_size as f32;

    if let Some(pixel_ratio) = opts.pixel_ratio {
        render_opts.device_pixel_ratio = pixel_ratio as f32;
    }

    ratex_render::render_to_png(&dl, &render_opts).map_err(|e| format!("render error: {e:?}"))
}

fn do_render_svg(latex: &str, opts: &Options) -> Result<String, String> {
    let dl = build_display_list(latex, opts.inline, &opts.color)?;
    let mut svg_opts = ratex_svg::SvgOptions::default();

    svg_opts.font_size = opts.font_size;
    svg_opts.embed_glyphs = true;

    Ok(ratex_svg::render_to_svg(&dl, &svg_opts))
}

rustler::init!("Elixir.Ratex.Native");