| 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 | |
| 11 | use crate::gpu::{Paper, page::PageScene}; |
| 12 | use crate::layout::{LayoutError, TextEngine}; |
| 13 | use draw::{Layer, Primitive, RenderError, Sheet}; |
| 14 | use onestore::page::{Page, text::Paragraph}; |
| 15 | use std::fmt; |
| 16 | |
| 17 | pub const LETTER: [f32; 2] = [612.0, 792.0]; |
| 18 | pub const A4: [f32; 2] = [595.276, 841.89]; |
| 19 | |
| 20 | /// The paper's top and bottom margins, where no page content prints. |
| 21 | const MARGIN: f32 = 36.0; |
| 22 | /// How far from the paper's left edge the margin origin prints at full size. |
| 23 | const LEFT: f32 = 72.0; |
| 24 | /// The room OneNote leaves right of the content when it shrinks a page to the paper's width. |
| 25 | const RIGHT: f32 = 9.0; |
| 26 | /// Pixels per point pictures are decoded at, 300 a inch. |
| 27 | const DENSITY: f32 = 300.0 / 72.0; |
| 28 | |
| 29 | /// How a page lies on its sheets. |
| 30 | struct 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 | |
| 41 | impl 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)] |
| 100 | pub enum PrintError { |
| 101 | Scene(crate::gpu::page::SceneError), |
| 102 | Layout(LayoutError), |
| 103 | Render(RenderError), |
| 104 | } |
| 105 | |
| 106 | impl 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 | |
| 116 | impl 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)] |
| 120 | pub 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)] |
| 131 | pub 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. |
| 141 | pub 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)] |
| 260 | mod 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 | } |