Skip to main content

ravenrs_report_viewer/
print.rs

1//! Printing a document, on the desktop and in the browser.
2
3use ravenrs_report::Document;
4
5/// Prints the document: through a hidden frame and the browser's print dialog
6/// on the web, or by opening the page in the default browser, with its print
7/// dialog up, on the desktop.
8pub fn print(document: &Document) {
9    let html = document.print_html();
10    #[cfg(target_arch = "wasm32")]
11    print_in_browser(&html);
12    #[cfg(not(target_arch = "wasm32"))]
13    print_on_desktop(&html);
14}
15
16/// The page, asking for the print dialog once its embedded fonts are ready.
17#[cfg_attr(target_arch = "wasm32", allow(dead_code, reason = "the web build prints from outside the page"))]
18fn with_print_dialog(html: &str) -> String {
19    let script = "<script>document.fonts.ready.then(function(){window.print()})</script></body>";
20    html.replacen("</body>", script, 1)
21}
22
23#[cfg(not(target_arch = "wasm32"))]
24fn print_on_desktop(html: &str) {
25    let path = std::env::temp_dir().join("ravenrs-report-print.html");
26    if let Err(e) = std::fs::write(&path, with_print_dialog(html)) {
27        tracing::warn!(error = %e, "could not write the print page");
28        return;
29    }
30    if let Err(e) = ravenrs_platform_native::open_with_system(&path) {
31        tracing::warn!(error = %e, "could not open the print page");
32    }
33}
34
35#[cfg(target_arch = "wasm32")]
36fn print_in_browser(html: &str) {
37    use wasm_bindgen::JsCast;
38    use wasm_bindgen::closure::Closure;
39
40    const FRAME_ID: &str = "ravenrs-print-frame";
41    let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
42    if let Some(old) = doc.get_element_by_id(FRAME_ID) {
43        old.remove();
44    }
45    let Some(frame) =
46        doc.create_element("iframe").ok().and_then(|e| e.dyn_into::<web_sys::HtmlIFrameElement>().ok())
47    else {
48        return;
49    };
50    frame.set_id(FRAME_ID);
51    frame.set_attribute("aria-hidden", "true").ok();
52    // Not display:none: browsers refuse to print hidden frames.
53    let style = frame.style();
54    for (k, v) in [
55        ("position", "fixed"),
56        ("right", "0"),
57        ("bottom", "0"),
58        ("width", "0"),
59        ("height", "0"),
60        ("border", "0"),
61    ] {
62        style.set_property(k, v).ok();
63    }
64    let target = frame.clone();
65    let on_load = Closure::once_into_js(move || {
66        let Some(window) = target.content_window() else { return };
67        // The embedded fonts must be ready, or the page prints in a fallback font.
68        let ready = window.document().and_then(|d| d.fonts().ready().ok());
69        wasm_bindgen_futures::spawn_local(async move {
70            if let Some(ready) = ready {
71                wasm_bindgen_futures::JsFuture::from(ready).await.ok();
72            }
73            window.focus().ok();
74            window.print().ok();
75        });
76    });
77    frame.set_onload(Some(on_load.unchecked_ref()));
78    frame.set_srcdoc(html);
79    if let Some(body) = doc.body() {
80        body.append_child(&frame).ok();
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::with_print_dialog;
87
88    #[test]
89    fn the_page_asks_for_the_dialog_inside_its_body() {
90        let page = with_print_dialog("<html><body><svg></svg></body></html>");
91        assert!(page.contains("window.print()"));
92        assert!(page.ends_with("</script></body></html>"), "{page}");
93    }
94}