1//! Pages on sheets of paper as OneNote 2010 prints them and saves them as PDF.
2//!
3//! OneNote draws a page between half-inch top and bottom margins, the page's margin origin
4//! an inch from the paper's left edge and at the top margin, shrinking the whole page
5//! when its content reaches past the paper's right edge ("Scale content to paper width").
6//! A page longer than a sheet continues on the next, which starts at the first line of text
7//! or picture the sheet's foot would have cut. Rule lines and template art print across the
8//! paper's width; the page colour does not. Each sheet's footer names the section, numbers
9//! the sheet counting from the first page printed, or both, in Times New Roman.
10
11use crate::gpu::{Paper, page::PageScene};
12use crate::layout::{LayoutError, TextEngine};
13use draw::{Layer, Primitive, RenderError, Sheet};
14use onestore::page::{Page, text::Paragraph};
15use std::fmt;
16
17pub const LETTER: [f32; 2] = [612.0, 792.0];
18pub const A4: [f32; 2] = [595.276, 841.89];
19
20/// The paper's top and bottom margins, where no page content prints.
21const MARGIN: f32 = 36.0;
22/// How far from the paper's left edge the margin origin prints at full size.
23const LEFT: f32 = 72.0;
24/// The room OneNote leaves right of the content when it shrinks a page to the paper's width.
25const RIGHT: f32 = 9.0;
26/// Pixels per point pictures are decoded at, 300 a inch.
27const DENSITY: f32 = 300.0 / 72.0;
28
29/// How a page lies on its sheets.
30struct Pagination {
31 /// Paper points per page point.
32 scale: f32,
33 /// The page x at the paper's left edge.
34 left: f32,
35 /// Where each sheet starts down the page.
36 tops: Vec<f32>,
37 /// How much of the page a sheet holds, in page points.
38 band: f32,
39}
40
41impl Pagination {
42 /// Lays content covering `bounds`, whose `rows` (lines of text, pictures) no sheet
43 /// should cut, on `paper` from the page's `margin_origin`, shrunk to the paper's width
44 /// with `fit_width`.
45 fn new(
46 bounds: [f32; 4],
47 rows: &[[f32; 2]],
48 margin_origin: [f32; 2],
49 paper: [f32; 2],
50 fit_width: bool,
51 ) -> Self {
52 let left = margin_origin[0] - LEFT;
53 let scale = if fit_width && bounds[2].is_finite() {
54 (paper[0] / (bounds[2] - left + RIGHT)).min(1.0)
55 } else {
56 1.0
57 };
58 let band = (paper[1] - 2.0 * MARGIN) / scale;
59 let first = if bounds[1].is_finite() {
60 margin_origin[1].min(bounds[1])
61 } else {
62 margin_origin[1]
63 };
64 let bottom = bounds[3].max(first);
65 let mut tops = vec![first];
66 let mut top = first;
67 while top + band < bottom {
68 // Moves the cut up to the top of whatever it would cross, until nothing shorter
69 // than a sheet crosses it.
70 let mut cut = top + band;
71 while let Some(row) = rows.iter().find(|[start, end]| {
72 *start < cut && *end > cut && *start > top + 1.0 && end - start <= band
73 }) {
74 cut = row[0];
75 }
76 tops.push(cut);
77 top = cut;
78 }
79 Self {
80 scale,
81 left,
82 tops,
83 band,
84 }
85 }
86
87 /// Each sheet's rows of the page, top and bottom: to where the next sheet starts, or
88 /// the paper's bottom margin on the last.
89 fn sheets(&self) -> impl Iterator<Item = [f32; 2]> + '_ {
90 self.tops.iter().enumerate().map(|(at, top)| {
91 [
92 *top,
93 self.tops.get(at + 1).copied().unwrap_or(top + self.band),
94 ]
95 })
96 }
97}
98
99#[derive(Debug)]
100pub enum PrintError {
101 Scene(crate::gpu::page::SceneError),
102 Layout(LayoutError),
103 Render(RenderError),
104}
105
106impl fmt::Display for PrintError {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::Scene(error) => write!(formatter, "The page could not be laid out: {error}"),
110 Self::Layout(error) => write!(formatter, "The footer could not be laid out: {error:?}"),
111 Self::Render(error) => write!(formatter, "The PDF could not be written: {error:?}"),
112 }
113 }
114}
115
116impl std::error::Error for PrintError {}
117
118/// What each sheet's footer says, as OneNote's Print Preview offers it.
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
120pub enum Footer {
121 /// "Section Page 3", OneNote's default.
122 #[default]
123 SectionAndPage,
124 Page,
125 Section,
126 None,
127}
128
129/// How pages go on paper.
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub struct Setup {
132 /// Width and height in points, turned as the sheet prints.
133 pub paper: [f32; 2],
134 /// Shrinks a page whose content reaches past the paper's right edge to fit it.
135 pub fit_width: bool,
136 pub footer: Footer,
137}
138
139/// `sections`' pages in order as a PDF, each on as many sheets as it takes, the footers
140/// naming the section a page is in and numbering the sheets from the first.
141pub fn pdf(
142 sections: Vec<(String, Vec<Page>)>,
143 engine: &mut TextEngine,
144 setup: &Setup,
145) -> Result<Vec<u8>, PrintError> {
146 let paper = setup.paper;
147 let title = sections
148 .iter()
149 .flat_map(|(_, pages)| pages.first())
150 .next()
151 .map(|page| page.title.clone())
152 .unwrap_or_default();
153 let mut printed = Vec::new();
154 for (section, pages) in sections {
155 for page in pages {
156 let (margin_origin, rules) = (page.margin_origin, page.rule_lines);
157 let mut scene = PageScene::new(page, engine).map_err(PrintError::Scene)?;
158 scene.settle(None, DENSITY, Paper::WHITE);
159 let (bounds, rows) = scene.printed_extent().map_err(PrintError::Scene)?;
160 let pagination = Pagination::new(bounds, &rows, margin_origin, paper, setup.fit_width);
161 printed.push((scene, pagination, margin_origin, rules, section.clone()));
162 }
163 }
164 let mut contents = Vec::with_capacity(printed.len());
165 for (scene, pagination, margin_origin, rules, _) in &printed {
166 let mut primitives = Vec::new();
167 scene
168 .append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)
169 .map_err(PrintError::Scene)?;
170 let rules: Vec<_> = pagination
171 .sheets()
172 .map(|[top, _]| {
173 let right = pagination.left + paper[0] / pagination.scale;
174 crate::interaction::rule_primitives(
175 *rules,
176 *margin_origin,
177 [pagination.left, top, right, top + pagination.band],
178 0.0,
179 Paper::WHITE,
180 )
181 })
182 .collect();
183 contents.push((primitives, rules));
184 }
185 let format = onestore::document::Format {
186 font: Some("Times New Roman".into()),
187 font_size: Some(10.0),
188 ..Default::default()
189 };
190 let mut footers = Vec::new();
191 for (sheet, section) in printed
192 .iter()
193 .flat_map(|(_, pagination, .., section)| pagination.sheets().map(move |_| section))
194 .enumerate()
195 {
196 let number = sheet + 1;
197 let text = match (setup.footer, section.as_str()) {
198 (Footer::None, _) => continue,
199 (Footer::Section, section) => section.to_owned(),
200 (Footer::Page, _) | (Footer::SectionAndPage, "") => format!("Page {number}"),
201 (Footer::SectionAndPage, section) => format!("{section} Page {number}"),
202 };
203 let layout = engine
204 .layout(&Paragraph::new(text, format.clone()), f32::MAX)
205 .map_err(PrintError::Layout)?;
206 footers.push(layout);
207 }
208 let footer_primitives: Vec<_> = footers
209 .iter()
210 .map(|layout| {
211 let (line, bounds) = layout.lines().next().expect("A laid-out footer has a line");
212 [Primitive::Text {
213 text: layout,
214 origin: [-line.metrics().advance / 2.0, -bounds.baseline],
215 clip: None,
216 ink: Paper::WHITE.ink,
217 }]
218 })
219 .collect();
220 let mut sheets = Vec::new();
221 let mut footer = footer_primitives.iter();
222 for ((_, pagination, ..), (primitives, rules)) in printed.iter().zip(&contents) {
223 let scale = pagination.scale;
224 for ([top, bottom], rules) in pagination.sheets().zip(rules) {
225 let origin = [-pagination.left * scale, MARGIN - top * scale];
226 let layer = |origin, clip, primitives| Layer {
227 scale,
228 origin,
229 clip,
230 backdrop: None,
231 round: None,
232 motion: None,
233 primitives,
234 };
235 // Rule lines run to the bottom margin, the page's content to where the next
236 // sheet takes it up.
237 let page = |primitives, bottom: f32| {
238 let clip = [0.0, MARGIN, paper[0], MARGIN + (bottom - top) * scale];
239 layer(origin, Some(clip), primitives)
240 };
241 let mut layers = vec![page(rules, top + pagination.band), page(primitives, bottom)];
242 // OneNote shrinks the footer with the page, towards the paper's bottom left.
243 if let Some(footer) = footer.next() {
244 let at = [
245 paper[0] / 2.0 * scale,
246 paper[1] - 2.0 - (1.0 - scale) * 16.0,
247 ];
248 layers.push(layer(at, None, footer));
249 }
250 sheets.push(Sheet {
251 size: paper,
252 layers,
253 });
254 }
255 }
256 draw::pdf(&title, &sheets).map_err(PrintError::Render)
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use onestore::{RevisionIndex, Store, document::Document};
263 use std::collections::HashMap;
264
265 /// OneNote 2010's defaults on Letter paper.
266 const ONENOTE: Setup = Setup {
267 paper: LETTER,
268 fit_width: true,
269 footer: Footer::SectionAndPage,
270 };
271
272 fn section(path: &str) -> Vec<Page> {
273 let bytes = std::fs::read(format!("{}/../../{path}", env!("CARGO_MANIFEST_DIR"))).unwrap();
274 let store = Store::parse(&bytes).unwrap();
275 let index = RevisionIndex::parse(&store).unwrap();
276 let document = Document::parse(&index).unwrap();
277 let mut spaces: Vec<_> = document
278 .pages()
279 .unwrap()
280 .into_iter()
281 .map(|(space, _)| space)
282 .collect();
283 spaces.dedup();
284 spaces
285 .into_iter()
286 .map(|space| Page::from_space(&document, space).unwrap())
287 .collect()
288 }
289
290 /// Each sheet's size and text, as a reader extracts it through the fonts' ToUnicode maps.
291 fn sheets(pdf: &[u8]) -> Vec<([f32; 2], String)> {
292 let find = |haystack: &[u8], needle: &[u8]| {
293 haystack
294 .windows(needle.len())
295 .position(|window| window == needle)
296 };
297 let mut objects: HashMap<u32, (&[u8], Vec<u8>)> = HashMap::new();
298 let mut rest = pdf;
299 while let Some(start) = find(rest, b" 0 obj\n") {
300 let number = rest[..start]
301 .rsplit(|byte| !byte.is_ascii_digit())
302 .next()
303 .unwrap();
304 let number: u32 = std::str::from_utf8(number).unwrap().parse().unwrap();
305 rest = &rest[start + 7..];
306 let end = find(rest, b"endobj").unwrap();
307 let object = &rest[..end];
308 let (dictionary, stream) = match find(object, b"stream\n") {
309 Some(at) => {
310 let data = &object[at + 7..find(object, b"\nendstream").unwrap()];
311 (
312 &object[..at],
313 miniz_oxide::inflate::decompress_to_vec_zlib(data).unwrap_or_default(),
314 )
315 }
316 None => (object, Vec::new()),
317 };
318 objects.insert(number, (dictionary, stream));
319 rest = &rest[end..];
320 }
321 let text = |bytes: &[u8]| String::from_utf8_lossy(bytes).into_owned();
322 let reference = |dictionary: &[u8], key: &str| -> Option<u32> {
323 let dictionary = text(dictionary);
324 let at = dictionary.find(&format!("{key} "))? + key.len() + 1;
325 dictionary[at..].split(' ').next()?.parse().ok()
326 };
327 let tree = objects
328 .values()
329 .find(|(dictionary, _)| text(dictionary).contains("/Type /Pages"))
330 .unwrap();
331 let kids = text(tree.0);
332 let kids = &kids[kids.find("/Kids [").unwrap() + 7..];
333 let kids: Vec<u32> = kids[..kids.find(']').unwrap()]
334 .split(" 0 R")
335 .filter_map(|kid| kid.trim().parse().ok())
336 .collect();
337 kids.into_iter()
338 .map(|page| {
339 let dictionary = text(objects[&page].0);
340 let media: Vec<f32> = dictionary[dictionary.find("/MediaBox [").unwrap() + 11..]
341 .split(']')
342 .next()
343 .unwrap()
344 .split_whitespace()
345 .map(|value| value.parse().unwrap())
346 .collect();
347 let mut maps: HashMap<String, HashMap<u16, String>> = HashMap::new();
348 for (at, _) in dictionary.match_indices("/F") {
349 let mut words = dictionary[at + 1..].split_whitespace();
350 let (Some(name), Some(font)) = (
351 words.next(),
352 words.next().and_then(|id| id.parse::<u32>().ok()),
353 ) else {
354 continue;
355 };
356 let Some(unicode) = reference(objects[&font].0, "/ToUnicode") else {
357 continue;
358 };
359 let cmap = text(&objects[&unicode].1);
360 let mut map = HashMap::new();
361 for line in cmap.lines().filter(|line| line.starts_with('<')) {
362 let hex: Vec<&str> = line
363 .split(['<', '>', ' '])
364 .filter(|part| !part.is_empty())
365 .collect();
366 let units: Vec<u16> = hex[1]
367 .as_bytes()
368 .chunks(4)
369 .map(|unit| {
370 u16::from_str_radix(std::str::from_utf8(unit).unwrap(), 16).unwrap()
371 })
372 .collect();
373 map.insert(
374 u16::from_str_radix(hex[0], 16).unwrap(),
375 String::from_utf16_lossy(&units),
376 );
377 }
378 maps.insert(name.to_owned(), map);
379 }
380 let content = &objects[&reference(objects[&page].0, "/Contents").unwrap()].1;
381 let (mut shown, mut font, mut name, mut at) =
382 (String::new(), String::new(), String::new(), 0);
383 while at < content.len() {
384 let byte = content[at];
385 at += 1;
386 let mut string = Vec::new();
387 match byte {
388 b'(' => {
389 while content[at] != b')' {
390 if content[at] == b'\\' {
391 at += 1;
392 if content[at].is_ascii_digit() {
393 let octal =
394 std::str::from_utf8(&content[at..at + 3]).unwrap();
395 string.push(u8::from_str_radix(octal, 8).unwrap());
396 at += 3;
397 continue;
398 }
399 string.push(match content[at] {
400 b'n' => b'\n',
401 b'r' => b'\r',
402 b't' => b'\t',
403 b'b' => 8,
404 b'f' => 12,
405 other => other,
406 });
407 } else {
408 string.push(content[at]);
409 }
410 at += 1;
411 }
412 at += 1;
413 }
414 b'<' if content[at] != b'<' => {
415 let end =
416 at + content[at..].iter().position(|byte| *byte == b'>').unwrap();
417 string = content[at..end]
418 .chunks(2)
419 .map(|pair| {
420 u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16)
421 .unwrap()
422 })
423 .collect();
424 at = end + 1;
425 }
426 b'/' => {
427 let end = at
428 + content[at..]
429 .iter()
430 .position(|byte| byte.is_ascii_whitespace())
431 .unwrap();
432 name = text(&content[at..end]);
433 at = end;
434 }
435 b'T' if content.get(at) == Some(&b'f') => font = name.clone(),
436 b'T' if content.get(at) == Some(&b'm') => shown.push('\n'),
437 _ => {}
438 }
439 if let Some(map) = maps.get(&font) {
440 for cid in string.chunks(2).filter(|pair| pair.len() == 2) {
441 if let Some(text) = map.get(&u16::from_be_bytes([cid[0], cid[1]])) {
442 shown.push_str(text);
443 }
444 }
445 }
446 }
447 ([media[2], media[3]], shown)
448 })
449 .collect()
450 }
451
452 /// A ligature's glyph reads back as the letters it joins.
453 #[test]
454 fn glyphs_read_back_as_their_text() {
455 let mut engine = TextEngine::default();
456 let format = onestore::document::Format {
457 font: Some("Calibri".into()),
458 font_size: Some(11.0),
459 ..Default::default()
460 };
461 let layout = engine
462 .layout(&Paragraph::new("office fish ffi".into(), format), f32::MAX)
463 .unwrap();
464 let primitives = [Primitive::Text {
465 text: &layout,
466 origin: [72.0, 72.0],
467 clip: None,
468 ink: Paper::WHITE.ink,
469 }];
470 let sheet = Sheet {
471 size: LETTER,
472 layers: vec![Layer {
473 scale: 1.0,
474 origin: [0.0; 2],
475 clip: None,
476 backdrop: None,
477 round: None,
478 motion: None,
479 primitives: &primitives,
480 }],
481 };
482 let pdf = draw::pdf("", &[sheet]).unwrap();
483 assert_eq!(sheets(&pdf)[0].1.trim(), "office fish ffi");
484 }
485
486 #[test]
487 fn sheets_break_between_lines() {
488 // Lines 20 points tall from 90 down; a Letter sheet holds 720 points of page.
489 let rows: Vec<[f32; 2]> = (0..60)
490 .map(|line| [90.0 + line as f32 * 20.0, 110.0 + line as f32 * 20.0])
491 .collect();
492 let pagination = Pagination::new(
493 [36.0, 14.4, 400.0, 1290.0],
494 &rows,
495 [36.0, 14.4],
496 LETTER,
497 true,
498 );
499 assert_eq!(pagination.scale, 1.0);
500 // 14.4 + 720 cuts the line from 730 to 750, which starts the next sheet.
501 assert_eq!(pagination.tops, [14.4, 730.0]);
502 let sheets: Vec<_> = pagination.sheets().collect();
503 assert_eq!(sheets, [[14.4, 730.0], [730.0, 1450.0]]);
504 }
505
506 #[test]
507 fn a_line_taller_than_a_sheet_is_cut() {
508 let pagination = Pagination::new(
509 [36.0, 14.4, 400.0, 1000.0],
510 &[[20.0, 1000.0]],
511 [36.0, 14.4],
512 LETTER,
513 true,
514 );
515 assert_eq!(pagination.tops, [14.4, 734.4]);
516 }
517
518 /// OneNote's scales for pages whose content ends 866.7, 581.8 and 1221.4 points across,
519 /// on its 612.36-point Letter sheet (`corpus/print`).
520 #[test]
521 fn wide_pages_shrink_to_the_paper_as_onenote_shrinks_them() {
522 for (right, onenote) in [
523 (866.7, 0.67167),
524 (581.8, 0.978),
525 (1221.4, 0.48361),
526 (400.0, 1.0),
527 ] {
528 let scale = Pagination::new(
529 [36.0, 14.4, right, 100.0],
530 &[],
531 [36.0, 14.4],
532 [612.36, 790.92],
533 true,
534 )
535 .scale;
536 assert!(
537 (scale - onenote).abs() < 0.002,
538 "{right}: {scale} for {onenote}"
539 );
540 }
541 }
542
543 /// `corpus/print`: the section OneNote 2010 saved as `native/section.pdf`, on the
544 /// same sheets with the same text.
545 #[test]
546 fn a_section_prints_on_onenotes_sheets() {
547 let pdf = pdf(
548 vec![("Print".into(), section("corpus/print/native/Print.one"))],
549 &mut TextEngine::default(),
550 &ONENOTE,
551 )
552 .unwrap();
553 let sheets = sheets(&pdf);
554 assert_eq!(sheets.len(), 5);
555 for (at, (size, text)) in sheets.iter().enumerate() {
556 assert_eq!(*size, LETTER);
557 assert!(text.contains(&format!("Print Page {}", at + 1)), "{text}");
558 }
559 let [first, second] = [&sheets[0].1, &sheets[1].1];
560 assert!(first.contains("Printing test"));
561 assert!(
562 first.contains(
563 "Line 37 of the long outline, with enough words to show wrapping where the"
564 )
565 );
566 assert!(first.contains("outline is narrow. Searchable-37"));
567 assert!(first.contains("Far right outline past the paper width"));
568 assert!(!first.contains("Searchable-38"));
569 assert!(second.contains("Line 38 of the long outline"));
570 assert!(second.contains("Searchable-70"));
571 assert!(!second.contains("Searchable-37\n"));
572 assert!(sheets[2].1.contains("Short second page"));
573 }
574
575 /// `corpus/page-background`'s pages, ruled and with template art, take the twelve
576 /// sheets OneNote 2010 prints them on (`corpus/print/native/rules-section.pdf`), the
577 /// art reaching past the Very Large Grid page's first.
578 #[test]
579 fn ruled_pages_and_art_print_on_onenotes_sheets() {
580 let pdf = pdf(
581 vec![(
582 "Rules".into(),
583 section("corpus/page-background/candidate/Rules.one"),
584 )],
585 &mut TextEngine::default(),
586 &ONENOTE,
587 )
588 .unwrap();
589 let sheets = sheets(&pdf);
590 assert_eq!(sheets.len(), 12);
591 assert!(sheets[8].1.contains("VeryLargeGrid"));
592 assert!(sheets[9].1.contains("Rules Page 10"));
593 assert!(!sheets[9].1.contains("VeryLargeGrid"));
594 }
595
596 #[test]
597 fn unscaled_pages_keep_their_size() {
598 let pagination = Pagination::new(
599 [36.0, 14.4, 1200.0, 100.0],
600 &[],
601 [36.0, 14.4],
602 LETTER,
603 false,
604 );
605 assert_eq!(pagination.scale, 1.0);
606 }
607
608 /// Sheets number on across sections, each footer naming its page's section, or say
609 /// what the footer choice asks for.
610 #[test]
611 fn footers_say_what_was_chosen() {
612 let print = section("corpus/print/native/Print.one");
613 let rules = section("corpus/page-background/candidate/Rules.one");
614 let two = vec![
615 ("Print".into(), print[2..3].to_vec()),
616 ("Rules".into(), rules[..1].to_vec()),
617 ];
618 let text = |footer| {
619 let setup = Setup { footer, ..ONENOTE };
620 sheets(&pdf(two.clone(), &mut TextEngine::default(), &setup).unwrap())
621 .into_iter()
622 .map(|(_, text)| text)
623 .collect::<Vec<_>>()
624 };
625 let both = text(Footer::SectionAndPage);
626 assert!(both[0].contains("Print Page 1") && both[1].contains("Rules Page 2"));
627 let pages = text(Footer::Page);
628 assert!(pages[1].contains("Page 2") && !pages[1].contains("Rules"));
629 assert!(
630 text(Footer::Section)[1].contains("Rules")
631 && !text(Footer::Section)[1].contains("Page 2")
632 );
633 assert!(!text(Footer::None)[1].contains("Rules"));
634 }
635}