Skip to main content

blockworx_kernel/
export.rs

1//! What leaves the document.
2//!
3//! An export projects, renders or prints what is on the canvas, so it is
4//! document work and it happens here. What comes of it is bytes and a name,
5//! left in the [`Handoff`](crate::handoff::Handoff) list; which file they are
6//! written to, and whether a dialog names it, belongs to whoever is showing
7//! the editor.
8
9use blockworx_doc::{
10    document::{DocIndex, Document},
11    repo::Repo,
12    rev::Rev,
13};
14use blockworx_editor::{
15    edit::describe::Label, gesture::Gesture, path::BlockPath, presentation::Presentation,
16    shape::ShapeId, title_block::TitleBlock, widget::drawing::Drawing,
17};
18use blockworx_export::{ExportContent, level, pdf};
19use blockworx_paint::{Scheme, TextLayout};
20use blockworx_store::{
21    doc::{DocumentNonce, Writability},
22    stamp::Source,
23};
24use blockworx_tools::commands::ExportFormat;
25
26use crate::session::{Session, viewed};
27
28/// What an export says about the drawing it writes, beyond the document
29/// itself: how the host names it, the block a printed sheet is stamped with,
30/// and the palette that sheet is printed in.
31///
32/// Stated by the host the way the camera and the clock are — the container's
33/// name, the date a rev was written, the palette the user prints in are facts
34/// about the session's surroundings rather than about the document. The
35/// typeface is not here: it is the text engine's own, and a second statement
36/// of it would be a second source of truth for the glyphs an export places.
37#[derive(Default)]
38pub struct Sheet {
39    pub name: String,
40    pub block: TitleBlock,
41    pub scheme: Scheme,
42}
43
44impl Session {
45    /// What an export writes, apart from the file it is written to — so the
46    /// bytes a user gets are the bytes a test can read.
47    pub(crate) fn export_content(
48        &mut self,
49        layout: &dyn TextLayout,
50        format: ExportFormat,
51        selection: Option<Vec<ShapeId>>,
52    ) -> ExportContent {
53        // A selection export writes a private standalone document (the
54        // selection pasted into a fresh empty one); the current-view export
55        // writes the live one in place.
56        let selection_repo = selection.and_then(|shapes| self.selection_repo(&shapes));
57        match format {
58            // A selection has no scope hierarchy to navigate, so
59            // [`blockworx_tools::commands::ExportScope::Selection`] does not
60            // offer PDF and there is no selection arm to write.
61            ExportFormat::Pdf => self.export_pdf(layout),
62            ExportFormat::Svg | ExportFormat::Png => {
63                let svg = if let Some(repo) = selection_repo {
64                    let mut index = DocIndex::default();
65                    let mut presentation = Presentation::default();
66                    level::render_svg(
67                        &self.theme,
68                        layout,
69                        index.view(repo.document()),
70                        &BlockPath::empty(),
71                        &mut presentation,
72                    )
73                } else {
74                    let doc = viewed(&self.doc, self.time_machine.as_ref()).document();
75                    level::render_svg(
76                        &self.theme,
77                        layout,
78                        self.doc_index.view(doc),
79                        &self.path,
80                        &mut self.presentation,
81                    )
82                };
83                if format == ExportFormat::Png {
84                    ExportContent::Png(svg)
85                } else {
86                    ExportContent::Svg(svg)
87                }
88            }
89        }
90    }
91
92    /// What an export this session writes says about where it came from, for
93    /// the fold at `at`.
94    fn export_source(&self, at: Rev) -> Source {
95        Source {
96            document: self.sheet.name.clone(),
97            author: self.identity.name.clone(),
98            tags: self.doc.tags().of(at).to_vec(),
99        }
100    }
101
102    /// The whole viewed document as a PDF. A failed export writes an empty
103    /// file rather than taking the session down with it, and says so where
104    /// the developer already is.
105    fn export_pdf(&self, layout: &dyn TextLayout) -> ExportContent {
106        let repo = self.viewed_repo();
107        let rev = repo.rev();
108        let provenance = self
109            .doc
110            .stamp_at(rev)
111            .from(self.export_source(rev))
112            .provenance;
113        let doc = repo.document().clone();
114        let mut index = DocIndex::default();
115        let scene = pdf::Scene {
116            document: index.view(&doc),
117            theme: &self.theme,
118            scheme: self.sheet.scheme,
119            layout,
120            block: self.sheet.block.clone(),
121            provenance,
122        };
123        ExportContent::Pdf(pdf::export(scene).unwrap_or_else(|e| {
124            tracing::error!("Failed to export PDF: {e}");
125            Vec::new()
126        }))
127    }
128
129    /// A standalone one-commit repo holding just the selection: copy the
130    /// shapes, then paste them into a fresh empty document, whose root the
131    /// block path then views. Reuses the paste pipeline (id remapping, route
132    /// re-reconstruction). A repo rather than a bare document because the
133    /// projection's names come from the log. `None` when nothing copyable is
134    /// selected.
135    fn selection_repo(&mut self, shapes: &[ShapeId]) -> Option<Repo> {
136        let clip = self.drawing().copy_selection(shapes)?;
137        let doc = Document::default();
138        let mut index = DocIndex::default();
139        let mut presentation = Presentation::default();
140        // The scratch document this builds is the export's own, never the
141        // session's, so a read-only session still exports its selection.
142        let mut gesture = Gesture::open(Label::verb("Export"), Writability::Writable);
143        let path = BlockPath::empty();
144        // A document of its own, so nothing on the clipboard can be a cut
145        // coming home to it: the export always mints.
146        let into = DocumentNonce::mint();
147        Drawing::new(index.view(&doc), &path, &mut presentation, &mut gesture)
148            .paste_snapshot(&clip, into, None);
149        let commit = gesture.seal("Exported a selection".to_owned())?;
150        Repo::folding(&[commit])
151            .inspect_err(|refusal| {
152                tracing::error!("the export document refused a paste: {refusal}");
153            })
154            .ok()
155    }
156}