katana-document-viewer 0.1.4

KatanA document viewer artifact, render evaluation, and export foundation.
Documentation
use super::{
    CODE_BLOCK_MARGIN, CODE_EMPTY_BLOCK_MIN_HEIGHT, CODE_VERTICAL_PADDING, DIAGRAM_FALLBACK_HEIGHT,
    DIAGRAM_MAX_WIDTH, DIAGRAM_VERTICAL_MARGIN, MATH_FALLBACK_HEIGHT, MATH_VERTICAL_MARGIN,
};
use crate::export_surface_helpers::SURFACE_CONTENT_WIDTH;
use crate::export_surface_line::SurfaceLine;
use crate::export_surface_span::SurfaceTextSpan;
use crate::export_surface_svg::{SurfaceSvgImage, SurfaceSvgRasterizer};
use crate::render_runtime::{KrrMathMode, KrrRenderOutput, KrrRenderRuntimeAdapter};
use image::RgbaImage;
use katana_render_runtime::RenderThemeSnapshot;

pub(crate) struct SurfaceCodeBlock {
    pub(crate) lines: Vec<SurfaceLine>,
    pub(crate) quote_depth: u32,
    pub(crate) indent_depth: u32,
}

impl SurfaceCodeBlock {
    pub(crate) fn new(lines: Vec<SurfaceLine>, quote_depth: u32, indent_depth: u32) -> Self {
        Self {
            lines,
            quote_depth,
            indent_depth,
        }
    }

    pub(crate) fn height(&self) -> u32 {
        self.box_height() + CODE_BLOCK_MARGIN * 2
    }

    pub(crate) fn box_height(&self) -> u32 {
        let content_height = self.lines.iter().map(SurfaceLine::line_height).sum::<u32>()
            + CODE_VERTICAL_PADDING * 2;
        content_height.max(CODE_EMPTY_BLOCK_MIN_HEIGHT)
    }

    #[cfg(test)]
    pub(crate) fn text_for_tests(&self) -> String {
        self.lines
            .iter()
            .map(|line| line.text.as_str())
            .collect::<Vec<_>>()
            .join("\n")
    }

    #[cfg(test)]
    pub(crate) fn debug_style_tags(&self) -> Vec<String> {
        self.lines
            .iter()
            .flat_map(SurfaceLine::debug_style_tags)
            .collect()
    }
}

pub(crate) struct SurfaceMathBlock {
    pub(crate) image: Option<SurfaceSvgImage>,
    fallback_text: String,
}

impl SurfaceMathBlock {
    pub(crate) fn new(expression: &str, theme: Option<RenderThemeSnapshot>) -> Self {
        let output = KrrRenderRuntimeAdapter::render_math_tex_with_theme(
            expression,
            KrrMathMode::Display,
            theme,
        );
        let image = output.svg_payload().and_then(|svg| {
            SurfaceSvgRasterizer::rasterize_with_root_font_size(
                svg,
                super::super::MATH_MAX_WIDTH,
                Some(super::super::BODY_FONT_SIZE),
            )
        });
        Self {
            image,
            fallback_text: math_fallback_text(expression, &output),
        }
    }

    pub(crate) fn height(&self) -> u32 {
        self.image
            .as_ref()
            .map(|rendered| rendered.image.height() + MATH_VERTICAL_MARGIN * 2)
            .unwrap_or(MATH_FALLBACK_HEIGHT)
    }

    #[cfg(test)]
    pub(crate) fn text(&self) -> String {
        if self.image.is_some() {
            return "math-svg:rendered".to_string();
        }
        self.fallback_text.clone()
    }

    pub(crate) fn fallback_text(&self) -> &str {
        &self.fallback_text
    }

    #[cfg(test)]
    pub(crate) fn for_tests(image: Option<RgbaImage>, fallback_text: String) -> Self {
        Self {
            image: image.map(|image| SurfaceSvgImage { image }),
            fallback_text,
        }
    }
}

fn math_fallback_text(expression: &str, output: &KrrRenderOutput) -> String {
    if output.svg_payload().is_some() {
        return expression.trim().to_string();
    }
    output.raw_payload().to_string()
}

pub(crate) struct SurfaceDiagramBlock {
    pub(crate) image: Option<SurfaceSvgImage>,
    fallback_text: String,
}

impl SurfaceDiagramBlock {
    pub(crate) fn rendered(svg: &str) -> Self {
        Self {
            image: SurfaceSvgRasterizer::rasterize(svg, DIAGRAM_MAX_WIDTH),
            fallback_text: "Rendered diagram".to_string(),
        }
    }

    pub(crate) fn height(&self) -> u32 {
        let content_height = self
            .image
            .as_ref()
            .map(|rendered| rendered.image.height())
            .unwrap_or(DIAGRAM_FALLBACK_HEIGHT);
        content_height + DIAGRAM_VERTICAL_MARGIN * 2
    }

    pub(crate) fn fallback_text(&self) -> &str {
        &self.fallback_text
    }
}

pub(crate) struct SurfaceImageBlock {
    pub(crate) image: RgbaImage,
    pub(crate) _alt: String,
}

impl SurfaceImageBlock {
    pub(crate) fn from_path(
        path: &std::path::Path,
        requested_width: Option<u32>,
        alt: String,
    ) -> Option<Self> {
        let image = image::open(path).ok()?.to_rgba8();
        let image = scaled_image(image, requested_width);
        Some(Self { image, _alt: alt })
    }

    pub(crate) fn height(&self) -> u32 {
        self.image.height() + super::super::IMAGE_VERTICAL_MARGIN * 2
    }

    #[cfg(test)]
    pub(crate) fn alt_for_tests(&self) -> String {
        self._alt.clone()
    }
}

fn scaled_image(image: RgbaImage, requested_width: Option<u32>) -> RgbaImage {
    let max_width = requested_width
        .unwrap_or(image.width())
        .min(SURFACE_CONTENT_WIDTH);
    if image.width() <= max_width {
        return image;
    }
    let height = (image.height() as f32 * max_width as f32 / image.width() as f32)
        .round()
        .max(1.0) as u32;
    image::imageops::resize(
        &image,
        max_width,
        height,
        image::imageops::FilterType::Lanczos3,
    )
}

pub(crate) struct SurfaceSpanMetrics;

impl SurfaceSpanMetrics {
    pub(crate) fn estimated_width(span: &SurfaceTextSpan, font_size: f32) -> u32 {
        span.estimated_width(font_size)
    }
}

#[cfg(test)]
#[path = "export_surface_blocks_media_tests.rs"]
mod tests;