tinymist_render/
lib.rs

1//! # tinymist-render
2//!
3//! **Note: this crate is under development. it currently doesn't ensure stable
4//! APIs, and heavily depending on some unstable crates.**
5//!
6//! This crate provides rendering features for tinymist server.
7
8use core::fmt;
9
10use base64::Engine;
11use reflexo_vec2svg::{ExportFeature, SvgExporter, SvgText};
12use tinymist_query::{FramePosition, LocalContext};
13use tinymist_std::typst::TypstDocument;
14
15struct PeriscopeExportFeature {}
16
17impl ExportFeature for PeriscopeExportFeature {
18    const ENABLE_INLINED_SVG: bool = false;
19    const ENABLE_TRACING: bool = false;
20    const SHOULD_ATTACH_DEBUG_INFO: bool = false;
21    const SHOULD_RENDER_TEXT_ELEMENT: bool = false;
22    const USE_STABLE_GLYPH_ID: bool = true;
23    const SHOULD_RASTERIZE_TEXT: bool = false;
24    const WITH_BUILTIN_CSS: bool = true;
25    const WITH_RESPONSIVE_JS: bool = false;
26    const AWARE_HTML_ENTITY: bool = false;
27}
28
29/// The arguments for periscope renderer.
30#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct PeriscopeArgs {
33    /// The distance above the center line.
34    pub y_above: f32,
35    /// The distance below the center line.
36    pub y_below: f32,
37    /// The scale of the image.
38    pub scale: f32,
39    /// Whether to invert the color. (will become smarter in the future)
40    pub invert_color: String,
41}
42
43impl Default for PeriscopeArgs {
44    fn default() -> Self {
45        Self {
46            y_above: 55.,
47            y_below: 55.,
48            scale: 1.5,
49            invert_color: "never".to_owned(),
50        }
51    }
52}
53
54/// The renderer in periscope mode.
55#[derive(Debug, Clone)]
56pub struct PeriscopeRenderer {
57    /// The arguments for periscope renderer.
58    p: PeriscopeArgs,
59}
60
61impl Default for PeriscopeRenderer {
62    fn default() -> Self {
63        Self::new(PeriscopeArgs::default())
64    }
65}
66
67impl PeriscopeRenderer {
68    /// Create a new periscope renderer.
69    pub fn new(args: PeriscopeArgs) -> Self {
70        Self { p: args }
71    }
72
73    /// Render the periscope image for the given document into markdown format.
74    pub fn render_marked(
75        &self,
76        ctx: &mut LocalContext,
77        doc: &TypstDocument,
78        pos: FramePosition,
79    ) -> Option<String> {
80        let (svg_payload, w, h) = self.render(ctx, doc, pos)?;
81
82        let sw = w * self.p.scale;
83        let sh = h * self.p.scale;
84
85        log::debug!("periscope image: {sw}x{sh}, {svg_payload}");
86
87        // encode as markdown dataurl image
88        let base64 = base64::engine::general_purpose::STANDARD.encode(svg_payload);
89        Some(enlarge_image(format_args!(
90            "![Periscope Mode](data:image/svg+xml;base64,{base64}|width={sw}|height={sh})"
91        )))
92    }
93
94    /// Render the periscope image for the given document.
95    pub fn render(
96        &self,
97        _ctx: &mut LocalContext,
98        doc: &TypstDocument,
99        pos: FramePosition,
100    ) -> Option<(String, f32, f32)> {
101        match doc {
102            TypstDocument::Paged(paged_doc) => {
103                // todo: svg viewer compatibility
104                type UsingExporter = SvgExporter<PeriscopeExportFeature>;
105                let mut doc = UsingExporter::svg_doc(paged_doc);
106                doc.module.prepare_glyphs();
107                let page0 = doc.pages.get(pos.page.get() - 1)?.clone();
108                let mut svg_text =
109                    UsingExporter::render(&doc.module, std::slice::from_ref(&page0), None);
110
111                // todo: let typst.ts expose it
112                let svg_header = svg_text.get_mut(0)?;
113
114                let y_center = pos.point.y.to_pt() as f32;
115                let y_lo = y_center - self.p.y_above;
116                let y_hi = y_center + self.p.y_below;
117
118                let width = page0.size.x.0;
119                let height = y_hi - y_lo;
120
121                *svg_header = SvgText::Plain(header_inner(
122                    page0.size.x.0,
123                    y_lo,
124                    y_hi,
125                    self.p.scale,
126                    self.p.invert_color == "always",
127                ));
128
129                Some((SvgText::join(svg_text), width, height))
130            }
131            _ => None,
132        }
133    }
134}
135
136fn enlarge_image(md: fmt::Arguments) -> String {
137    format!("```\n```\n{md}\n```\n```")
138}
139
140/// Render the header of SVG.
141/// <svg> .. </svg>
142/// ^^^^^
143fn header_inner(w: f32, y_lo: f32, y_hi: f32, scale: f32, invert_color: bool) -> String {
144    let h = y_hi - y_lo;
145    let sw = w * scale;
146    let sh = h * scale;
147
148    let invert_style = if invert_color {
149        r#"-webkit-filter: invert(0.933333) hue-rotate(180deg); filter: invert(0.933333) hue-rotate(180deg);"#
150    } else {
151        ""
152    };
153
154    format!(
155        r#"<svg style="{invert_style}" class="typst-doc" width="{sw:.3}px" height="{sh:.3}px" data-width="{w:.3}" data-height="{h:.3}" viewBox="0 {y_lo:.3} {w:.3} {h:.3}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">"#,
156    )
157}