1use std::io::Write;
4
5use log::{debug, trace};
6use printpdf::{
7 Color, DateTime, Line, LineDashPattern, LinePoint, Mm, Op, PaintMode, ParsedFont, PdfDocument,
8 PdfFontHandle, PdfPage, PdfSaveOptions, Point, Pt, Rect, Rgb, TextItem, WindingOrder,
9};
10
11use crate::page::*;
12
13pub mod qrcode_ops;
14
15pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
17
18const FONT_RATIO: f32 = 3.0 / 5.0;
20
21const CODE_FONT_BYTES: &[u8] = include_bytes!("assets/fonts/IBMPlexMono-Regular.ttf");
22const TITLE_FONT_BYTES: &[u8] = include_bytes!("assets/fonts/IBMPlexMono-Medium.ttf");
23
24pub struct Document {
26 pub doc: PdfDocument,
28
29 ops: Vec<Op>,
31
32 pub title_font: PdfFontHandle,
34
35 pub code_font: PdfFontHandle,
37
38 pub page_size: PageSize,
40
41 pub title: String,
43}
44
45impl Document {
46 pub fn new(title: String, page_size: PageSize) -> Result<Document, Box<dyn std::error::Error>> {
49 debug!("Initializing PDF");
50
51 let dimensions = page_size.dimensions();
52
53 let mut doc = PdfDocument::new(&title);
54
55 let producer = format!("PaperAge v{}", VERSION.unwrap_or("0.0.0"));
56 let now = DateTime::now();
57 doc.metadata.info.producer = producer;
58 doc.metadata.info.creation_date = now;
59 doc.metadata.info.modification_date = now;
60
61 let mut warnings = Vec::new();
62
63 let code_parsed = ParsedFont::from_bytes(CODE_FONT_BYTES, 0, &mut warnings)
64 .ok_or("Failed to parse code font")?;
65 let code_font_id = doc.add_font(&code_parsed);
66 let code_font = PdfFontHandle::External(code_font_id);
67
68 let title_parsed = ParsedFont::from_bytes(TITLE_FONT_BYTES, 0, &mut warnings)
69 .ok_or("Failed to parse title font")?;
70 let title_font_id = doc.add_font(&title_parsed);
71 let title_font = PdfFontHandle::External(title_font_id);
72
73 let ops = vec![
74 Op::SetFillColor {
76 col: Color::Rgb(Rgb::new(1.0, 1.0, 1.0, None)),
77 },
78 Op::DrawPolygon {
79 polygon: Rect {
80 x: Pt(0.0),
81 y: Pt(0.0),
82 width: dimensions.width.into_pt(),
83 height: dimensions.height.into_pt(),
84 mode: Some(PaintMode::Fill),
85 winding_order: Some(WindingOrder::NonZero),
86 }
87 .to_polygon(),
88 },
89 Op::SetFillColor {
91 col: Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)),
92 },
93 ];
94
95 Ok(Document {
96 doc,
97 ops,
98 title_font,
99 code_font,
100 page_size,
101 title: title.clone(),
102 })
103 }
104
105 pub fn insert_title_text(&mut self, title: String) {
107 debug!("Inserting title: {}", title.as_str());
108
109 let font_size = 14.0;
110
111 let margin = {
113 if title.len() <= 37 {
114 self.page_size.qrcode_left_edge()
115 } else {
116 self.page_size.dimensions().margin
117 }
118 };
119
120 let y = self.page_size.dimensions().height
121 - self.page_size.dimensions().margin
122 - Mm::from(Pt(font_size));
123
124 self.ops.push(Op::StartTextSection);
125 self.ops.push(Op::SetFillColor {
126 col: Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)),
127 });
128 self.ops.push(Op::SetTextCursor {
129 pos: Point::new(margin, y),
130 });
131 self.ops.push(Op::SetFont {
132 font: self.title_font.clone(),
133 size: Pt(font_size),
134 });
135 self.ops.push(Op::ShowText {
136 items: vec![TextItem::Text(title)],
137 });
138 self.ops.push(Op::EndTextSection);
139 }
140
141 pub fn insert_pem_text(&mut self, pem: String) {
143 debug!("Inserting PEM encoded ciphertext");
144
145 let mut font_size = 13.0;
146 let mut line_height = 15.0;
147
148 if pem.lines().count() > 42 {
150 font_size = 6.5;
151 line_height = 7.0;
152 } else if pem.lines().count() > 39 {
153 font_size = 7.0;
154 line_height = 8.0;
155 } else if pem.lines().count() > 27 {
156 font_size = 8.0;
157 line_height = 9.0;
158 } else if pem.lines().count() > 22 {
159 font_size = 10.0;
160 line_height = 12.0;
161 }
162
163 self.ops.push(Op::StartTextSection);
164 self.ops.push(Op::SetFillColor {
165 col: Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)),
166 });
167
168 self.ops.push(Op::SetTextCursor {
169 pos: Point::new(
170 self.page_size.dimensions().margin,
171 (self.page_size.dimensions().height / 2.0)
172 - Mm::from(Pt(font_size))
173 - self.page_size.dimensions().margin,
174 ),
175 });
176 self.ops.push(Op::SetLineHeight {
177 lh: Pt(line_height),
178 });
179 self.ops.push(Op::SetFont {
180 font: self.code_font.clone(),
181 size: Pt(font_size),
182 });
183
184 for line in pem.lines() {
185 self.ops.push(Op::ShowText {
186 items: vec![TextItem::Text(line.to_string())],
187 });
188 self.ops.push(Op::AddLineBreak);
189 }
190
191 self.ops.push(Op::EndTextSection);
192 }
193
194 pub fn insert_qr_code(&mut self, text: String) -> Result<(), Box<dyn std::error::Error>> {
196 debug!("Inserting QR code");
197
198 let ops = qrcode_ops::render(text, &self.page_size)?;
199 self.ops.extend(ops);
200
201 Ok(())
202 }
203
204 pub fn draw_grid(&mut self) {
206 debug!("Drawing grid");
207
208 let grid_size = Mm(5.0);
209 let thickness = 0.0;
210
211 let mut x = Mm(0.0);
212 let mut y = self.page_size.dimensions().height;
213 while x < self.page_size.dimensions().width {
214 x += grid_size;
215
216 self.draw_line(
217 vec![
218 Point::new(x, self.page_size.dimensions().height),
219 Point::new(x, Mm(0.0)),
220 ],
221 thickness,
222 LineDashPattern::default(),
223 );
224
225 while y > Mm(0.0) {
226 y -= grid_size;
227
228 self.draw_line(
229 vec![
230 Point::new(self.page_size.dimensions().width, y),
231 Point::new(Mm(0.0), y),
232 ],
233 thickness,
234 LineDashPattern::default(),
235 );
236 }
237 }
238 }
239
240 pub fn draw_line(&mut self, points: Vec<Point>, thickness: f32, dash_pattern: LineDashPattern) {
242 trace!("Drawing line");
243
244 self.ops.push(Op::SetLineDashPattern { dash: dash_pattern });
245
246 let outline_color = Color::Rgb(Rgb::new(0.75, 0.75, 0.75, None));
247 self.ops.push(Op::SetOutlineColor { col: outline_color });
248
249 self.ops.push(Op::SetOutlineThickness { pt: Pt(thickness) });
250
251 let divider = Line {
252 points: points
253 .iter()
254 .map(|p| LinePoint {
255 p: *p,
256 bezier: false,
257 })
258 .collect(),
259 is_closed: false,
260 };
261
262 self.ops.push(Op::DrawLine { line: divider });
263 }
264
265 pub fn insert_notes_field(&mut self, label: String, skip_line: bool) {
267 debug!("Inserting notes/passphrase placeholder");
268 const MAX_LABEL_LEN: usize = 32;
269
270 let baseline =
271 self.page_size.dimensions().height / 2.0 + self.page_size.dimensions().margin;
272
273 let label_len = label.len();
274
275 let font_size = 13.0;
276
277 self.ops.push(Op::StartTextSection);
278 self.ops.push(Op::SetFillColor {
279 col: Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)),
280 });
281 self.ops.push(Op::SetTextCursor {
282 pos: Point::new(self.page_size.qrcode_left_edge(), baseline),
283 });
284 self.ops.push(Op::SetFont {
285 font: self.title_font.clone(),
286 size: Pt(font_size),
287 });
288 self.ops.push(Op::ShowText {
289 items: vec![TextItem::Text(label)],
290 });
291 self.ops.push(Op::EndTextSection);
292
293 if label_len <= MAX_LABEL_LEN && !skip_line {
295 self.draw_line(
296 vec![
297 Point::new(
298 self.page_size.qrcode_left_edge()
299 + Mm::from(Pt(FONT_RATIO * font_size * label_len as f32)),
300 baseline - Mm(1.0),
301 ),
302 Point::new(
303 self.page_size.qrcode_left_edge() + self.page_size.qrcode_size(),
304 baseline - Mm(1.0),
305 ),
306 ],
307 1.0,
308 LineDashPattern::default(),
309 )
310 }
311 }
312
313 pub fn insert_footer(&mut self) {
315 debug!("Inserting footer");
316
317 self.ops.push(Op::StartTextSection);
318 self.ops.push(Op::SetFillColor {
319 col: Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)),
320 });
321 self.ops.push(Op::SetTextCursor {
322 pos: Point::new(
323 self.page_size.dimensions().margin,
324 self.page_size.dimensions().margin,
325 ),
326 });
327 self.ops.push(Op::SetFont {
328 font: self.title_font.clone(),
329 size: Pt(13.0),
330 });
331 self.ops.push(Op::ShowText {
332 items: vec![TextItem::Text(
333 "Scan QR code and decrypt using Age <https://age-encryption.org>".to_string(),
334 )],
335 });
336 self.ops.push(Op::EndTextSection);
337 }
338
339 pub fn save_to_bytes(mut self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
341 let dimensions = self.page_size.dimensions();
342 let page = PdfPage::new(dimensions.width, dimensions.height, self.ops);
343 self.doc.pages.push(page);
344
345 let mut warnings = Vec::new();
346 let bytes = self.doc.save(&PdfSaveOptions::default(), &mut warnings);
347 Ok(bytes)
348 }
349
350 pub fn save_to_writer<W: Write>(
352 mut self,
353 writer: &mut W,
354 ) -> Result<(), Box<dyn std::error::Error>> {
355 let dimensions = self.page_size.dimensions();
356 let page = PdfPage::new(dimensions.width, dimensions.height, self.ops);
357 self.doc.pages.push(page);
358
359 let mut warnings = Vec::new();
360 self.doc
361 .save_writer(writer, &PdfSaveOptions::default(), &mut warnings);
362 Ok(())
363 }
364
365 pub fn create_pdf(
373 mut self,
374 grid: bool,
375 notes_label: String,
376 skip_notes_line: bool,
377 encrypted: String,
378 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
379 if grid {
380 self.draw_grid();
381 }
382
383 self.insert_title_text(self.title.clone());
384
385 self.insert_qr_code(encrypted.clone())?;
386
387 self.insert_notes_field(notes_label, skip_notes_line);
388
389 self.draw_line(
390 vec![
391 self.page_size.dimensions().center_left(),
392 self.page_size.dimensions().center_right(),
393 ],
394 1.0,
395 LineDashPattern {
396 dash_1: Some(5),
397 ..LineDashPattern::default()
398 },
399 );
400
401 self.insert_pem_text(encrypted);
402
403 self.insert_footer();
404
405 self.save_to_bytes()
406 }
407}
408
409#[test]
410fn test_paper_dimensions_default() {
411 let default = PageDimensions::default();
412 assert_eq!(default.width, Mm(210.0));
413 assert_eq!(default.height, Mm(297.0));
414}
415
416#[test]
417fn test_new_document() {
418 let title = String::from("Hello World!");
419 let result = Document::new(title, PageSize::A4);
420 assert!(result.is_ok());
421
422 let doc = result.unwrap();
423 assert_eq!(doc.page_size.dimensions(), crate::page::A4_PAGE);
424}
425
426#[test]
427fn test_new_letter_document() {
428 let title = String::from("Hello Letter!");
429 let result = Document::new(title, PageSize::Letter);
430 assert!(result.is_ok());
431
432 let doc = result.unwrap();
433 assert_eq!(doc.page_size.dimensions(), crate::page::LETTER_PAGE);
434}
435
436#[test]
437fn test_qrcode() {
438 let result = Document::new(String::from("QR code"), PageSize::A4);
439 let mut document = result.unwrap();
440 let result = document.insert_qr_code(String::from("payload"));
441 assert!(result.is_ok());
442}
443
444#[test]
445fn test_qrcode_too_large() {
446 let mut document = Document::new(String::from("QR code"), PageSize::A4).unwrap();
447 let result = document.insert_qr_code(String::from(include_str!("../tests/data/too_large.txt")));
448
449 assert!(result.is_err());
450 assert!(result.unwrap_err().is::<qrcode::types::QrError>());
451}