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}