katana-render-runtime 0.3.2

Versioned render runtime for KatanA diagrams and math (Mermaid, Draw.io, ZenUML, PlantUML, MathJax).
Documentation
use super::super::theme::PlantUmlRenderStyle;
use jni::{
    JavaVM, jni_sig, jni_str,
    objects::{JByteArray, JObject, JString, JValue},
};

pub(crate) struct PlantUmlJvmBridgeOps;

impl PlantUmlJvmBridgeOps {
    pub(crate) fn render(
        java_vm: &JavaVM,
        source: &str,
        style: &PlantUmlRenderStyle,
    ) -> Result<String, String> {
        let (svg, description) = java_vm
            .attach_current_thread(|env| -> jni::errors::Result<(String, String)> {
                let source = env.new_string(source)?;
                let reader = Self::source_string_reader(env, &source, style)?;
                let output_stream = Self::byte_array_output_stream(env)?;
                let description = Self::render_to_stream(env, &reader, &output_stream, style)?;
                let svg = Self::svg_from_stream(env, &output_stream)?;
                Ok((svg, description))
            })
            .map_err(|error| error.to_string())?;
        if description.to_ascii_lowercase().contains("error") {
            return Err(format!("PlantUML render failed: {description}"));
        }
        Ok(svg)
    }

    fn source_string_reader<'local>(
        env: &mut jni::Env<'local>,
        source: &JString<'local>,
        style: &PlantUmlRenderStyle,
    ) -> jni::errors::Result<JObject<'local>> {
        let defines = Self::empty_defines(env)?;
        let config = Self::config_list(env, style)?;
        env.new_object(
            jni_str!("net/sourceforge/plantuml/SourceStringReader"),
            jni_sig!(
                "(Lnet/sourceforge/plantuml/preproc/Defines;Ljava/lang/String;Ljava/util/List;)V"
            ),
            &[
                JValue::Object(&defines),
                JValue::Object(source),
                JValue::Object(&config),
            ],
        )
    }

    fn empty_defines<'local>(env: &mut jni::Env<'local>) -> jni::errors::Result<JObject<'local>> {
        env.call_static_method(
            jni_str!("net/sourceforge/plantuml/preproc/Defines"),
            jni_str!("createEmpty"),
            jni_sig!("()Lnet/sourceforge/plantuml/preproc/Defines;"),
            &[],
        )?
        .l()
    }

    fn config_list<'local>(
        env: &mut jni::Env<'local>,
        style: &PlantUmlRenderStyle,
    ) -> jni::errors::Result<JObject<'local>> {
        let list = env.new_object(jni_str!("java/util/ArrayList"), jni_sig!("()V"), &[])?;
        for line in style.config_lines() {
            let java_line = env.new_string(line)?;
            env.call_method(
                &list,
                jni_str!("add"),
                jni_sig!("(Ljava/lang/Object;)Z"),
                &[JValue::Object(&java_line)],
            )?;
        }
        Ok(list)
    }

    fn byte_array_output_stream<'local>(
        env: &mut jni::Env<'local>,
    ) -> jni::errors::Result<JObject<'local>> {
        env.new_object(
            jni_str!("java/io/ByteArrayOutputStream"),
            jni_sig!("()V"),
            &[],
        )
    }

    fn render_to_stream<'local>(
        env: &mut jni::Env<'local>,
        reader: &JObject<'local>,
        output_stream: &JObject<'local>,
        style: &PlantUmlRenderStyle,
    ) -> jni::errors::Result<String> {
        let format_option = Self::svg_format_option(env, style)?;
        let description = Self::call_output_image(env, reader, output_stream, &format_option)?;
        if description.is_null() {
            return Ok("error: PlantUML did not return a diagram description".to_string());
        }
        Self::description(env, description)
    }

    fn svg_format_option<'local>(
        env: &mut jni::Env<'local>,
        style: &PlantUmlRenderStyle,
    ) -> jni::errors::Result<JObject<'local>> {
        let svg_format = env
            .get_static_field(
                jni_str!("net/sourceforge/plantuml/FileFormat"),
                jni_str!("SVG"),
                jni_sig!("Lnet/sourceforge/plantuml/FileFormat;"),
            )?
            .l()?;
        let format_option = env.new_object(
            jni_str!("net/sourceforge/plantuml/FileFormatOption"),
            jni_sig!("(Lnet/sourceforge/plantuml/FileFormat;)V"),
            &[JValue::Object(&svg_format)],
        )?;
        if style.dark_mode() {
            return Self::dark_format_option(env, &format_option);
        }
        Ok(format_option)
    }

    fn dark_format_option<'local>(
        env: &mut jni::Env<'local>,
        format_option: &JObject<'local>,
    ) -> jni::errors::Result<JObject<'local>> {
        let dark_mapper = env
            .get_static_field(
                jni_str!("net/sourceforge/plantuml/klimt/color/ColorMapper"),
                jni_str!("DARK_MODE"),
                jni_sig!("Lnet/sourceforge/plantuml/klimt/color/ColorMapper;"),
            )?
            .l()?;
        env.call_method(
            format_option,
            jni_str!("withColorMapper"),
            jni_sig!(
                "(Lnet/sourceforge/plantuml/klimt/color/ColorMapper;)Lnet/sourceforge/plantuml/FileFormatOption;"
            ),
            &[JValue::Object(&dark_mapper)],
        )?
        .l()
    }

    fn call_output_image<'local>(
        env: &mut jni::Env<'local>,
        reader: &JObject<'local>,
        output_stream: &JObject<'local>,
        format_option: &JObject<'local>,
    ) -> jni::errors::Result<JObject<'local>> {
        env.call_method(
            reader,
            jni_str!("outputImage"),
            jni_sig!(
                "(Ljava/io/OutputStream;Lnet/sourceforge/plantuml/FileFormatOption;)Lnet/sourceforge/plantuml/core/DiagramDescription;"
            ),
            &[JValue::Object(output_stream), JValue::Object(format_option)],
        )?
        .l()
    }

    fn description<'local>(
        env: &mut jni::Env<'local>,
        description: JObject<'local>,
    ) -> jni::errors::Result<String> {
        let text = env
            .call_method(
                description,
                jni_str!("getDescription"),
                jni_sig!("()Ljava/lang/String;"),
                &[],
            )?
            .l()?;
        let java_text = env.cast_local::<JString>(text)?;
        java_text.try_to_string(env)
    }

    fn svg_from_stream(
        env: &mut jni::Env<'_>,
        output_stream: &JObject<'_>,
    ) -> jni::errors::Result<String> {
        let bytes_object = env
            .call_method(
                output_stream,
                jni_str!("toByteArray"),
                jni_sig!("()[B"),
                &[],
            )?
            .l()?;
        let bytes = env.cast_local::<JByteArray>(bytes_object)?;
        let svg_bytes = env.convert_byte_array(&bytes)?;
        Ok(String::from_utf8_lossy(&svg_bytes).to_string())
    }
}