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}