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 to rasterise and no three-dimensional axes is exported without ever touching the GPU, so
13//! exporting on a machine with no graphics adapter works as it always has. A three-dimensional axes under the
14//! default policy needs an adapter only to verify that its painter's order shows what the viewer shows; without one
15//! it is still exported, back to front, with an [`ironlab_pdf::ExportWarning`] that says so and names a software
16//! adapter as the remedy. Only a figure that must be rasterised needs an adapter, and when none is available that
17//! is reported as [`ExportError::Render`] rather than quietly exported as something else.
18
19use std::path::Path;
20
21use ironlab_ir::Figure;
22use ironlab_pdf::raster::{Need, needs_rasteriser};
23use ironlab_pdf::{
24 ExportWarning, ExportWarningKind, Exported, PdfError, PdfOptions, RasterImage, Rasteriser,
25 Rendered, UnverifiedCause,
26};
27use ironlab_scene::SceneWarning;
28use ironlab_scene::display::DisplayList;
29use ironlab_text::TextEngine;
30
31use crate::offscreen::{OffscreenRenderer, RenderError, with_shared_renderer};
32
33/// A failure to export a figure as a PDF.
34#[derive(Debug, thiserror::Error)]
35pub enum ExportError {
36 /// The PDF could not be written.
37 #[error(transparent)]
38 Pdf(#[from] PdfError),
39 /// The figure has content that the raster policy rasterises, and the renderer could not be created or could not
40 /// draw it.
41 #[error("the figure has content that must be rasterised, but {0}")]
42 Render(#[from] RenderError),
43}
44
45/// The viewer's headless renderer, presented to the PDF exporter as a [`Rasteriser`].
46pub struct GpuRasteriser<'a> {
47 renderer: &'a mut OffscreenRenderer,
48 text: &'a TextEngine,
49 /// The first failure of the renderer. The exporter only learns that rasterising failed, as a message, so the
50 /// failure itself is kept here: a readback failure means the device may have been lost, and only a
51 /// [`RenderError`] reaching [`with_shared_renderer`] makes it replace the device rather than hand the same dead
52 /// one to every later export.
53 failure: Option<RenderError>,
54}
55
56impl<'a> GpuRasteriser<'a> {
57 /// Wraps a renderer, which resolves any text in the rasterised content through `text`.
58 pub fn new(renderer: &'a mut OffscreenRenderer, text: &'a TextEngine) -> Self {
59 Self {
60 renderer,
61 text,
62 failure: None,
63 }
64 }
65}
66
67impl Rasteriser for GpuRasteriser<'_> {
68 fn rasterise(&mut self, list: &DisplayList, dpi: f64) -> Result<RasterImage, String> {
69 let rendered = match self.renderer.render_display_list(list, self.text, dpi) {
70 Ok(rendered) => rendered,
71 Err(error) => {
72 let message = error.to_string();
73 self.failure.get_or_insert(error);
74 return Err(message);
75 }
76 };
77 Ok(RasterImage {
78 width: rendered.width,
79 height: rendered.height,
80 rgba: rendered.rgba,
81 })
82 }
83}
84
85/// Compiles and exports a figure, rasterising its dense content on the GPU.
86///
87/// The warnings the scene compiler raised while drawing the figure are returned with the bytes, as
88/// [`ironlab_pdf::export_pdf`] returns them, so that a caller learns which artists were left off the page.
89///
90/// # Errors
91///
92/// Returns [`ExportError::Render`] when the figure has content the policy rasterises and the renderer cannot be
93/// created or cannot draw it, and [`ExportError::Pdf`] when the PDF itself cannot be written.
94pub fn export_pdf(
95 figure: &Figure,
96 text: &TextEngine,
97 options: &PdfOptions,
98) -> Result<Exported, ExportError> {
99 let scene = ironlab_scene::compile(figure, text);
100 let rendered = render_display_list(&scene.display_list, text, options)?;
101 Ok(Exported {
102 bytes: rendered.bytes,
103 warnings: scene.warnings,
104 export: rendered.warnings,
105 })
106}
107
108/// Exports an already compiled display list, rasterising and verifying through the GPU what the options ask.
109///
110/// A list that needs the renderer only to verify its three-dimensional axes is exported without it when no
111/// adapter is available, with each such axes drawn back to front and a warning naming the missing adapter.
112///
113/// # Errors
114///
115/// As for [`export_pdf`].
116pub fn render_display_list(
117 list: &DisplayList,
118 text: &TextEngine,
119 options: &PdfOptions,
120) -> Result<Rendered, ExportError> {
121 let need = needs_rasteriser(list, &options.raster);
122 if need == Need::No {
123 return Ok(ironlab_pdf::render_display_list(list, text, options, None)?);
124 }
125 let attempt = with_shared_renderer(|renderer| {
126 let mut raster = GpuRasteriser::new(renderer, text);
127 let result = ironlab_pdf::render_display_list(list, text, options, Some(&mut raster));
128 // A failure of the renderer is returned as itself, so that a lost device is recognised and replaced. Every
129 // other failure is the exporter's and travels through the inner result, where it cannot be mistaken for one.
130 match (result, raster.failure) {
131 (Err(PdfError::Raster(_)), Some(failure)) => Err(failure),
132 (result, _) => Ok(result),
133 }
134 });
135 match attempt {
136 Ok(result) => result.map_err(ExportError::Pdf),
137 Err(RenderError::NoAdapter(message)) if need == Need::ToVerify => {
138 let mut rendered = ironlab_pdf::render_display_list(list, text, options, None)?;
139 for warning in &mut rendered.warnings {
140 if let ExportWarningKind::Unverified {
141 cause: UnverifiedCause::NoRasteriser,
142 } = warning.kind
143 {
144 *warning = ExportWarning::unverified(
145 warning.node,
146 UnverifiedCause::NoAdapter,
147 &format!(
148 "no graphics adapter is available to verify it ({message}); a software adapter \
149 such as lavapipe from Mesa serves on a machine without a graphics device"
150 ),
151 );
152 }
153 }
154 Ok(rendered)
155 }
156 Err(error) => Err(ExportError::Render(error)),
157 }
158}
159
160/// Compiles and exports a figure, writing the PDF to `path`, and returns the warnings the scene compiler raised
161/// while drawing the figure, as [`export_pdf`] does.
162///
163/// # Errors
164///
165/// Returns the errors of [`export_pdf`], and [`PdfError::Io`] when the file cannot be written.
166pub fn write_pdf(
167 figure: &Figure,
168 text: &TextEngine,
169 options: &PdfOptions,
170 path: impl AsRef<Path>,
171) -> Result<Vec<SceneWarning>, ExportError> {
172 let exported = export_pdf(figure, text, options)?;
173 std::fs::write(path.as_ref(), exported.bytes)
174 .map_err(|error| ExportError::Pdf(PdfError::Io(error)))?;
175 Ok(exported.warnings)
176}