Skip to main content

ironlab_viewer/
export.rs

1//! PDF export through the viewer's own renderer.
2//!
3//! [`ironlab_pdf`] writes vector geometry and text, and asks a [`Rasteriser`] for the parts of a figure that are too
4//! dense to be worth writing as vector paths. This module supplies that rasteriser: [`GpuRasteriser`] wraps the
5//! viewer's headless renderer, so the pixels embedded in a PDF come from the same device, the same tessellation and
6//! the same shaders that draw the interactive canvas. There is no second rasteriser, and therefore nothing that can
7//! drift from what the user inspected on screen.
8//!
9//! [`export_pdf`] and [`write_pdf`] are the export path the whole project uses: the viewer's "Export PDF…" command,
10//! the `ironlab` crate's [`Figure::export_pdf`](../../ironlab/struct.Figure.html) and the documentation gallery.
11//!
12//! A figure with nothing dense in it is exported without ever touching the GPU, so exporting on a machine with no
13//! graphics adapter works as it always has. Only a figure that must be rasterised needs an adapter, and when none is
14//! available that is reported as [`ExportError::Render`] rather than quietly exported as something else.
15
16use std::path::Path;
17
18use ironlab_ir::Figure;
19use ironlab_pdf::{PdfError, PdfOptions, RasterImage, Rasteriser};
20use ironlab_scene::display::DisplayList;
21use ironlab_text::TextEngine;
22
23use crate::offscreen::{OffscreenRenderer, RenderError, with_shared_renderer};
24
25/// A failure to export a figure as a PDF.
26#[derive(Debug, thiserror::Error)]
27pub enum ExportError {
28    /// The PDF could not be written.
29    #[error(transparent)]
30    Pdf(#[from] PdfError),
31    /// The figure has content that the raster policy rasterises, and the renderer could not be created or could not
32    /// draw it.
33    #[error("the figure has content that must be rasterised, but {0}")]
34    Render(#[from] RenderError),
35}
36
37/// The viewer's headless renderer, presented to the PDF exporter as a [`Rasteriser`].
38pub struct GpuRasteriser<'a> {
39    renderer: &'a mut OffscreenRenderer,
40    text: &'a TextEngine,
41    /// The first failure of the renderer. The exporter only learns that rasterising failed, as a message, so the
42    /// failure itself is kept here: a readback failure means the device may have been lost, and only a
43    /// [`RenderError`] reaching [`with_shared_renderer`] makes it replace the device rather than hand the same dead
44    /// one to every later export.
45    failure: Option<RenderError>,
46}
47
48impl<'a> GpuRasteriser<'a> {
49    /// Wraps a renderer, which resolves any text in the rasterised content through `text`.
50    pub fn new(renderer: &'a mut OffscreenRenderer, text: &'a TextEngine) -> Self {
51        Self {
52            renderer,
53            text,
54            failure: None,
55        }
56    }
57}
58
59impl Rasteriser for GpuRasteriser<'_> {
60    fn rasterise(&mut self, list: &DisplayList, dpi: f64) -> Result<RasterImage, String> {
61        let rendered = match self.renderer.render_display_list(list, self.text, dpi) {
62            Ok(rendered) => rendered,
63            Err(error) => {
64                let message = error.to_string();
65                self.failure.get_or_insert(error);
66                return Err(message);
67            }
68        };
69        Ok(RasterImage {
70            width: rendered.width,
71            height: rendered.height,
72            rgba: rendered.rgba,
73        })
74    }
75}
76
77/// Compiles and exports a figure, rasterising its dense content on the GPU.
78///
79/// # Errors
80///
81/// Returns [`ExportError::Render`] when the figure has content the policy rasterises and the renderer cannot be
82/// created or cannot draw it, and [`ExportError::Pdf`] when the PDF itself cannot be written.
83pub fn export_pdf(
84    figure: &Figure,
85    text: &TextEngine,
86    options: &PdfOptions,
87) -> Result<Vec<u8>, ExportError> {
88    let scene = ironlab_scene::compile(figure, text);
89    render_display_list(&scene.display_list, text, options)
90}
91
92/// Exports an already compiled display list, rasterising its dense content on the GPU.
93///
94/// # Errors
95///
96/// As for [`export_pdf`].
97pub fn render_display_list(
98    list: &DisplayList,
99    text: &TextEngine,
100    options: &PdfOptions,
101) -> Result<Vec<u8>, ExportError> {
102    if !ironlab_pdf::raster::rasterises_any(list, &options.raster) {
103        return Ok(ironlab_pdf::render_display_list(list, text, options, None)?);
104    }
105    with_shared_renderer(|renderer| {
106        let mut raster = GpuRasteriser::new(renderer, text);
107        let result = ironlab_pdf::render_display_list(list, text, options, Some(&mut raster));
108        // A failure of the renderer is returned as itself, so that a lost device is recognised and replaced. Every
109        // other failure is the exporter's and travels through the inner result, where it cannot be mistaken for one.
110        match (result, raster.failure) {
111            (Err(PdfError::Raster(_)), Some(failure)) => Err(failure),
112            (result, _) => Ok(result),
113        }
114    })?
115    .map_err(ExportError::Pdf)
116}
117
118/// Compiles and exports a figure, writing the PDF to `path`.
119///
120/// # Errors
121///
122/// Returns the errors of [`export_pdf`], and [`PdfError::Io`] when the file cannot be written.
123pub fn write_pdf(
124    figure: &Figure,
125    text: &TextEngine,
126    options: &PdfOptions,
127    path: impl AsRef<Path>,
128) -> Result<(), ExportError> {
129    let bytes = export_pdf(figure, text, options)?;
130    std::fs::write(path.as_ref(), bytes).map_err(|error| ExportError::Pdf(PdfError::Io(error)))
131}