1use 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#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct PeriscopeArgs {
33 pub y_above: f32,
35 pub y_below: f32,
37 pub scale: f32,
39 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#[derive(Debug, Clone)]
56pub struct PeriscopeRenderer {
57 p: PeriscopeArgs,
59}
60
61impl Default for PeriscopeRenderer {
62 fn default() -> Self {
63 Self::new(PeriscopeArgs::default())
64 }
65}
66
67impl PeriscopeRenderer {
68 pub fn new(args: PeriscopeArgs) -> Self {
70 Self { p: args }
71 }
72
73 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 let base64 = base64::engine::general_purpose::STANDARD.encode(svg_payload);
89 Some(enlarge_image(format_args!(
90 ""
91 )))
92 }
93
94 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 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 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
140fn 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}