1use std::collections::HashMap;
13
14use lopdf::{Document, Object, ObjectId};
15
16use super::geometry::{normalize_degrees, PageRotation};
17use super::page_box::{visible_page_box, PageBox};
18use crate::types::{ItemType, TextItem};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum PositionFrame {
24 #[default]
31 Sheet,
32 Display,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub(crate) enum PageRotate {
44 #[default]
45 Rotate0,
46 Rotate90,
47 Rotate180,
48 Rotate270,
49}
50
51impl PageRotate {
52 pub(crate) fn from_degrees(degrees: f64) -> PageRotate {
57 if !degrees.is_finite() {
58 return PageRotate::Rotate0;
59 }
60 let folded = degrees.rem_euclid(360.0);
61 match (((folded + 45.0) / 90.0).floor() as i64).rem_euclid(4) {
62 0 => PageRotate::Rotate0,
63 1 => PageRotate::Rotate90,
64 2 => PageRotate::Rotate180,
65 _ => PageRotate::Rotate270,
66 }
67 }
68
69 pub(crate) fn degrees(self) -> f32 {
71 match self {
72 PageRotate::Rotate0 => 0.0,
73 PageRotate::Rotate90 => 90.0,
74 PageRotate::Rotate180 => 180.0,
75 PageRotate::Rotate270 => 270.0,
76 }
77 }
78
79 pub(crate) fn display_size(self, sheet: &PageBox) -> (f32, f32) {
82 let (width, height) = (sheet.width(), sheet.height());
83 match self {
84 PageRotate::Rotate0 | PageRotate::Rotate180 => (width, height),
85 PageRotate::Rotate90 | PageRotate::Rotate270 => (height, width),
86 }
87 }
88
89 pub(crate) fn sheet_box_to_display(
93 self,
94 sheet: &PageBox,
95 x: f32,
96 y: f32,
97 width: f32,
98 height: f32,
99 ) -> (f32, f32, f32, f32) {
100 let (x0, x1) = (x.min(x + width), x.max(x + width));
101 let (y0, y1) = (y.min(y + height), y.max(y + height));
102 let (w, h) = (x1 - x0, y1 - y0);
103 let (sheet_w, sheet_h) = (sheet.width(), sheet.height());
104 match self {
105 PageRotate::Rotate0 => (x0, y0, w, h),
106 PageRotate::Rotate90 => (y0, sheet_w - x1, h, w),
107 PageRotate::Rotate180 => (sheet_w - x1, sheet_h - y1, w, h),
108 PageRotate::Rotate270 => (sheet_h - y1, x0, h, w),
109 }
110 }
111
112 pub(crate) fn display_rect_to_sheet(self, sheet: &PageBox, rect: [f32; 4]) -> [f32; 4] {
116 let [ax, ay, bx, by] = rect;
117 let (x1, x2) = (ax.min(bx), ax.max(bx));
118 let (y1, y2) = (ay.min(by), ay.max(by));
119 let (display_w, display_h) = self.display_size(sheet);
120 match self {
121 PageRotate::Rotate0 => [x1, y1, x2, y2],
122 PageRotate::Rotate90 => [y1, display_w - x2, y2, display_w - x1],
123 PageRotate::Rotate180 => [
124 display_w - x2,
125 display_h - y2,
126 display_w - x1,
127 display_h - y1,
128 ],
129 PageRotate::Rotate270 => [display_h - y2, x1, display_h - y1, x2],
130 }
131 }
132}
133
134pub(crate) fn page_rotate(doc: &Document, page_id: ObjectId) -> PageRotate {
138 let mut id = page_id;
139 for _ in 0..32 {
140 let Ok(dict) = doc.get_dictionary(id) else {
141 return PageRotate::Rotate0;
142 };
143 if let Ok(value) = dict.get(b"Rotate") {
144 let value = match value {
145 Object::Reference(reference) => match doc.get_object(*reference) {
146 Ok(object) => object,
147 Err(_) => return PageRotate::Rotate0,
148 },
149 direct => direct,
150 };
151 return super::get_number(value)
152 .map(|degrees| PageRotate::from_degrees(degrees as f64))
153 .unwrap_or_default();
154 }
155 match dict.get(b"Parent") {
156 Ok(Object::Reference(parent)) => id = *parent,
157 _ => return PageRotate::Rotate0,
158 }
159 }
160 PageRotate::Rotate0
161}
162
163#[derive(Debug, Clone, Copy, PartialEq)]
166pub(crate) struct DisplayPage {
167 pub(crate) rotate: PageRotate,
168 pub(crate) sheet: PageBox,
169}
170
171impl DisplayPage {
172 pub(crate) fn new(doc: &Document, page_id: ObjectId, sheet: PageBox) -> DisplayPage {
175 DisplayPage {
176 rotate: page_rotate(doc, page_id),
177 sheet,
178 }
179 }
180
181 pub(crate) fn region_to_sheet(&self, rect: [f32; 4]) -> [f32; 4] {
184 self.rotate.display_rect_to_sheet(&self.sheet, rect)
185 }
186}
187
188pub(crate) fn items_to_display_frame(
196 items: &mut [TextItem],
197 turn: PageRotation,
198 rotate: PageRotate,
199 sheet: &PageBox,
200) {
201 if turn == PageRotation::Upright && rotate == PageRotate::Rotate0 {
202 return;
203 }
204 for item in items {
205 turn.unrotate_box(&mut item.x, &mut item.y, &mut item.width, &mut item.height);
206 let (x, y, width, height) =
207 rotate.sheet_box_to_display(sheet, item.x, item.y, item.width, item.height);
208 item.x = x;
209 item.y = y;
210 item.width = width;
211 item.height = height;
212 if matches!(item.item_type, ItemType::Text) {
215 item.rotation = normalize_degrees(
216 item.rotation - turn.baseline_rebase_degrees() - rotate.degrees(),
217 );
218 }
219 }
220}
221
222pub(crate) fn document_items_to_display_frame(
227 doc: &Document,
228 items: &mut [TextItem],
229 page_rotations: &HashMap<u32, PageRotation>,
230) {
231 let pages = doc.get_pages();
232 let mut frames: HashMap<u32, (PageRotation, PageRotate, PageBox)> = HashMap::new();
233 for item in items {
234 let (turn, rotate, sheet) = *frames.entry(item.page).or_insert_with(|| {
235 let turn = page_rotations
236 .get(&item.page)
237 .copied()
238 .unwrap_or(PageRotation::Upright);
239 let (rotate, sheet) = match pages.get(&item.page) {
242 Some(&page_id) => (
243 page_rotate(doc, page_id),
244 visible_page_box(doc, page_id).unwrap_or(PageBox::LETTER),
245 ),
246 None => (PageRotate::Rotate0, PageBox::LETTER),
247 };
248 (turn, rotate, sheet)
249 });
250 items_to_display_frame(std::slice::from_mut(item), turn, rotate, &sheet);
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use lopdf::dictionary;
258
259 fn item(x: f32, y: f32, width: f32, height: f32, rotation: f32) -> TextItem {
260 TextItem {
261 text: "a".into(),
262 x,
263 y,
264 width,
265 height,
266 font: String::new(),
267 font_tag: String::new(),
268 legacy_symbol_rewrite: false,
269 font_size: 12.0,
270 page: 1,
271 is_bold: false,
272 is_italic: false,
273 font_weight: None,
274 bold_source: None,
275 fixed_pitch: None,
276 fill_color: None,
277 stroke_color: None,
278 render_mode: None,
279 is_underline: false,
280 is_strikeout: false,
281 item_type: ItemType::Text,
282 mcid: None,
283 baseline_shift: 0.0,
284 rotation,
285 advance_known: true,
286 }
287 }
288
289 fn geometry(item: &TextItem) -> (f32, f32, f32, f32, f32) {
290 (item.x, item.y, item.width, item.height, item.rotation)
291 }
292
293 fn doc_with_rotate(page: Option<Object>, parent: Option<Object>) -> (Document, ObjectId) {
296 let mut doc = Document::with_version("1.5");
297 let pages_id = doc.new_object_id();
298 let mut page_dict = dictionary! {
299 "Type" => "Page",
300 "Parent" => Object::Reference(pages_id),
301 "MediaBox" => Object::Array(vec![0.into(), 0.into(), 612.into(), 792.into()]),
302 };
303 if let Some(rotate) = page {
304 page_dict.set("Rotate", rotate);
305 }
306 let page_id = doc.add_object(page_dict);
307 let mut pages = dictionary! {
308 "Type" => "Pages",
309 "Count" => 1,
310 "Kids" => vec![Object::Reference(page_id)],
311 };
312 if let Some(rotate) = parent {
313 pages.set("Rotate", rotate);
314 }
315 doc.objects.insert(pages_id, pages.into());
316 let catalog_id = doc.add_object(dictionary! {
317 "Type" => "Catalog",
318 "Pages" => Object::Reference(pages_id),
319 });
320 doc.trailer.set("Root", Object::Reference(catalog_id));
321 (doc, page_id)
322 }
323
324 #[test]
325 fn rotate_snaps_to_a_right_angle_in_a_full_turn() {
326 use PageRotate::*;
327 for (degrees, expected) in [
328 (0.0, Rotate0),
329 (90.0, Rotate90),
330 (180.0, Rotate180),
331 (270.0, Rotate270),
332 (360.0, Rotate0),
333 (-90.0, Rotate270),
334 (-180.0, Rotate180),
335 (-270.0, Rotate90),
336 (450.0, Rotate90),
337 (-450.0, Rotate270),
338 (720.0, Rotate0),
339 (810.0, Rotate90),
340 (89.6, Rotate90),
341 (44.0, Rotate0),
342 (45.0, Rotate90),
343 (134.9, Rotate90),
344 (135.0, Rotate180),
345 (f64::NAN, Rotate0),
346 (f64::INFINITY, Rotate0),
347 (f64::NEG_INFINITY, Rotate0),
348 ] {
349 assert_eq!(PageRotate::from_degrees(degrees), expected, "{degrees}");
350 }
351 }
352
353 #[test]
354 fn rotate_is_read_from_the_page_and_normalised() {
355 for (value, expected) in [
356 (Object::Integer(90), PageRotate::Rotate90),
357 (Object::Real(270.0), PageRotate::Rotate270),
358 (Object::Integer(-90), PageRotate::Rotate270),
359 (Object::Integer(450), PageRotate::Rotate90),
360 (Object::Integer(180), PageRotate::Rotate180),
361 ] {
362 let (doc, page_id) = doc_with_rotate(Some(value.clone()), None);
363 assert_eq!(page_rotate(&doc, page_id), expected, "{value:?}");
364 }
365 }
366
367 #[test]
368 fn rotate_is_inherited_from_the_page_tree() {
369 let (doc, page_id) = doc_with_rotate(None, Some(Object::Integer(90)));
370 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate90);
371 let (doc, page_id) = doc_with_rotate(Some(Object::Integer(180)), Some(Object::Integer(90)));
373 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate180);
374 }
375
376 #[test]
377 fn missing_or_malformed_rotate_reads_as_zero() {
378 let (doc, page_id) = doc_with_rotate(None, None);
379 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate0);
380 let (doc, page_id) = doc_with_rotate(Some(Object::Name(b"ninety".to_vec())), None);
381 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate0);
382 let (doc, page_id) = doc_with_rotate(
384 Some(Object::Name(b"ninety".to_vec())),
385 Some(Object::Integer(90)),
386 );
387 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate0);
388 }
389
390 #[test]
391 fn indirect_rotate_is_resolved() {
392 let (mut doc, page_id) = doc_with_rotate(None, None);
393 let rotate_id = doc.add_object(Object::Integer(270));
394 doc.get_dictionary_mut(page_id)
395 .unwrap()
396 .set("Rotate", Object::Reference(rotate_id));
397 assert_eq!(page_rotate(&doc, page_id), PageRotate::Rotate270);
398 }
399
400 #[test]
401 fn sheet_boxes_land_where_a_renderer_draws_them() {
402 let sheet = PageBox::LETTER;
403 let (x, y, w, h) = (100.0, 200.0, 50.0, 10.0);
404 assert_eq!(
405 PageRotate::Rotate0.sheet_box_to_display(&sheet, x, y, w, h),
406 (100.0, 200.0, 50.0, 10.0)
407 );
408 assert_eq!(
409 PageRotate::Rotate90.sheet_box_to_display(&sheet, x, y, w, h),
410 (200.0, 462.0, 10.0, 50.0)
411 );
412 assert_eq!(
413 PageRotate::Rotate180.sheet_box_to_display(&sheet, x, y, w, h),
414 (462.0, 582.0, 50.0, 10.0)
415 );
416 assert_eq!(
417 PageRotate::Rotate270.sheet_box_to_display(&sheet, x, y, w, h),
418 (582.0, 100.0, 10.0, 50.0)
419 );
420 assert_eq!(
422 PageRotate::Rotate90.sheet_box_to_display(&sheet, 150.0, 210.0, -50.0, -10.0),
423 (200.0, 462.0, 10.0, 50.0)
424 );
425 assert_eq!(PageRotate::Rotate0.display_size(&sheet), (612.0, 792.0));
426 assert_eq!(PageRotate::Rotate90.display_size(&sheet), (792.0, 612.0));
427 assert_eq!(PageRotate::Rotate180.display_size(&sheet), (612.0, 792.0));
428 assert_eq!(PageRotate::Rotate270.display_size(&sheet), (792.0, 612.0));
429 }
430
431 fn top_left_rect(x: f32, y: f32, w: f32, h: f32, height: f32) -> [f32; 4] {
433 [x, height - y - h, x + w, height - y]
434 }
435
436 #[test]
437 fn display_rects_come_back_to_the_sheet_rect_of_the_same_box() {
438 for sheet in [
439 PageBox::LETTER,
440 PageBox::from_corners(50.0, 60.0, 350.0, 460.0).unwrap(),
441 ] {
442 let (x, y, w, h) = (20.0, 30.0, 40.0, 12.0);
443 let expected = top_left_rect(x, y, w, h, sheet.height());
444 for rotate in [
445 PageRotate::Rotate0,
446 PageRotate::Rotate90,
447 PageRotate::Rotate180,
448 PageRotate::Rotate270,
449 ] {
450 let (dx, dy, dw, dh) = rotate.sheet_box_to_display(&sheet, x, y, w, h);
451 let (_, display_h) = rotate.display_size(&sheet);
452 let display_rect = top_left_rect(dx, dy, dw, dh, display_h);
453 assert_eq!(
454 rotate.display_rect_to_sheet(&sheet, display_rect),
455 expected,
456 "{rotate:?} on {sheet:?}"
457 );
458 let [a, b, c, d] = display_rect;
460 assert_eq!(rotate.display_rect_to_sheet(&sheet, [c, d, a, b]), expected);
461 }
462 }
463 }
464
465 #[test]
466 fn display_rects_follow_the_documented_formulas() {
467 let sheet = PageBox::LETTER;
469 let rect = [200.0, 100.0, 210.0, 150.0];
470 assert_eq!(
471 PageRotate::Rotate90.display_rect_to_sheet(&sheet, rect),
472 [100.0, 792.0 - 210.0, 150.0, 792.0 - 200.0]
473 );
474 assert_eq!(
475 PageRotate::Rotate180.display_rect_to_sheet(&sheet, rect),
476 [612.0 - 210.0, 792.0 - 150.0, 612.0 - 200.0, 792.0 - 100.0]
477 );
478 assert_eq!(
479 PageRotate::Rotate270.display_rect_to_sheet(&sheet, rect),
480 [612.0 - 150.0, 200.0, 612.0 - 100.0, 210.0]
481 );
482 assert_eq!(
483 PageRotate::Rotate0.display_rect_to_sheet(&sheet, rect),
484 rect
485 );
486 }
487
488 #[test]
489 fn offset_crop_box_uses_the_visible_extents_only() {
490 let sheet = PageBox::from_corners(50.0, 60.0, 350.0, 460.0).unwrap();
493 assert_eq!(
494 PageRotate::Rotate90.sheet_box_to_display(&sheet, 20.0, 30.0, 40.0, 12.0),
495 (30.0, 240.0, 12.0, 40.0)
496 );
497 assert_eq!(PageRotate::Rotate90.display_size(&sheet), (400.0, 300.0));
498 assert_eq!(
499 PageRotate::Rotate180.sheet_box_to_display(&sheet, 20.0, 30.0, 40.0, 12.0),
500 (240.0, 358.0, 40.0, 12.0)
501 );
502 }
503
504 #[test]
505 fn turned_pages_are_unturned_before_the_display_turn() {
506 let sheet = PageBox::LETTER;
507 let (mut x, mut y, mut w, mut h) = (28.0, 420.0, 12.0, 200.0);
511 PageRotation::Ccw.rotate_box(&mut x, &mut y, &mut w, &mut h);
512 assert_eq!((x, y, w, h), (420.0, -40.0, 200.0, 12.0));
513 let mut items = vec![item(x, y, w, h, 0.0)];
514 items_to_display_frame(&mut items, PageRotation::Ccw, PageRotate::Rotate90, &sheet);
518 assert_eq!(geometry(&items[0]), (420.0, 572.0, 200.0, 12.0, 0.0));
519 assert_eq!(
520 PageRotate::Rotate90.sheet_box_to_display(&sheet, 28.0, 420.0, 12.0, 200.0),
521 (420.0, 572.0, 200.0, 12.0)
522 );
523
524 let (mut x, mut y, mut w, mut h) = (72.0, 700.0, 100.0, 11.0);
527 PageRotation::Ccw.rotate_box(&mut x, &mut y, &mut w, &mut h);
528 let mut items = vec![item(x, y, w, h, 270.0)];
529 items_to_display_frame(&mut items, PageRotation::Ccw, PageRotate::Rotate90, &sheet);
530 assert_eq!(geometry(&items[0]), (700.0, 440.0, 11.0, 100.0, 270.0));
531
532 let (mut x, mut y, mut w, mut h) = (300.0, 500.0, 12.0, 200.0);
536 PageRotation::Cw.rotate_box(&mut x, &mut y, &mut w, &mut h);
537 assert_eq!((x, y, w, h), (-700.0, 300.0, 200.0, 12.0));
538 let mut items = vec![item(x, y, w, h, 0.0)];
539 items_to_display_frame(&mut items, PageRotation::Cw, PageRotate::Rotate270, &sheet);
540 assert_eq!(geometry(&items[0]), (92.0, 300.0, 200.0, 12.0, 0.0));
541
542 let mut items = vec![item(x, y, w, h, 0.0)];
544 items_to_display_frame(&mut items, PageRotation::Cw, PageRotate::Rotate0, &sheet);
545 assert_eq!(geometry(&items[0]), (300.0, 500.0, 12.0, 200.0, 270.0));
546
547 let mut placeholder = item(-700.0, 300.0, 200.0, 12.0, 0.0);
549 placeholder.item_type = ItemType::Image;
550 let mut items = vec![placeholder];
551 items_to_display_frame(&mut items, PageRotation::Cw, PageRotate::Rotate270, &sheet);
552 assert_eq!(geometry(&items[0]), (92.0, 300.0, 200.0, 12.0, 0.0));
553 }
554
555 #[test]
556 fn every_turn_and_rotate_pair_unturns_then_turns_with_the_page() {
557 let sheet = PageBox::LETTER;
558 let cases = [
564 (PageRotation::Upright, (72.0, 700.0, 100.0, 11.0), 0.0),
565 (PageRotation::Ccw, (28.0, 420.0, 12.0, 200.0), 90.0),
566 (PageRotation::Ccw, (72.0, 700.0, 100.0, 11.0), 0.0),
567 (PageRotation::Cw, (300.0, 500.0, 12.0, 200.0), 270.0),
568 (PageRotation::Cw, (72.0, 700.0, 100.0, 11.0), 0.0),
569 ];
570 let rotates = [
571 PageRotate::Rotate0,
572 PageRotate::Rotate90,
573 PageRotate::Rotate180,
574 PageRotate::Rotate270,
575 ];
576 for (turn, (x, y, w, h), sheet_rotation) in cases {
577 for rotate in rotates {
578 let (mut tx, mut ty, mut tw, mut th) = (x, y, w, h);
581 turn.rotate_box(&mut tx, &mut ty, &mut tw, &mut th);
582 let reported = normalize_degrees(sheet_rotation + turn.baseline_rebase_degrees());
583 let mut items = vec![item(tx, ty, tw, th, reported)];
584 items_to_display_frame(&mut items, turn, rotate, &sheet);
585 let (dx, dy, dw, dh) = rotate.sheet_box_to_display(&sheet, x, y, w, h);
588 let display_rotation = normalize_degrees(sheet_rotation - rotate.degrees());
589 assert_eq!(
590 geometry(&items[0]),
591 (dx, dy, dw, dh, display_rotation),
592 "{turn:?} under {rotate:?}"
593 );
594 }
595 }
596 }
597
598 #[test]
599 fn upright_text_turns_with_the_page() {
600 let sheet = PageBox::LETTER;
601 for (rotate, expected) in [
602 (PageRotate::Rotate90, (700.0, 440.0, 11.0, 100.0, 270.0)),
603 (PageRotate::Rotate180, (440.0, 81.0, 100.0, 11.0, 180.0)),
604 (PageRotate::Rotate270, (81.0, 72.0, 11.0, 100.0, 90.0)),
605 ] {
606 let mut items = vec![item(72.0, 700.0, 100.0, 11.0, 0.0)];
607 items_to_display_frame(&mut items, PageRotation::Upright, rotate, &sheet);
608 assert_eq!(geometry(&items[0]), expected, "{rotate:?}");
609 }
610 }
611
612 #[test]
613 fn untouched_when_neither_turned_nor_rotated() {
614 let mut items = vec![item(150.0, 210.0, -50.0, -10.0, 0.0)];
617 items_to_display_frame(
618 &mut items,
619 PageRotation::Upright,
620 PageRotate::Rotate0,
621 &PageBox::LETTER,
622 );
623 assert_eq!(geometry(&items[0]), (150.0, 210.0, -50.0, -10.0, 0.0));
624 }
625
626 #[test]
627 fn document_items_follow_their_own_page() {
628 let mut doc = Document::with_version("1.5");
632 let pages_id = doc.new_object_id();
633 let page1 = doc.add_object(dictionary! {
634 "Type" => "Page",
635 "Parent" => Object::Reference(pages_id),
636 "MediaBox" => Object::Array(vec![0.into(), 0.into(), 400.into(), 500.into()]),
637 "CropBox" => Object::Array(vec![50.into(), 60.into(), 350.into(), 460.into()]),
638 "Rotate" => Object::Integer(90),
639 });
640 let page2 = doc.add_object(dictionary! {
641 "Type" => "Page",
642 "Parent" => Object::Reference(pages_id),
643 "MediaBox" => Object::Array(vec![0.into(), 0.into(), 612.into(), 792.into()]),
644 });
645 doc.objects.insert(
646 pages_id,
647 dictionary! {
648 "Type" => "Pages",
649 "Count" => 2,
650 "Kids" => vec![Object::Reference(page1), Object::Reference(page2)],
651 }
652 .into(),
653 );
654 let catalog_id = doc.add_object(dictionary! {
655 "Type" => "Catalog",
656 "Pages" => Object::Reference(pages_id),
657 });
658 doc.trailer.set("Root", Object::Reference(catalog_id));
659
660 let mut first = item(20.0, 30.0, 40.0, 12.0, 0.0);
661 first.page = 1;
662 let mut second = item(20.0, 30.0, 40.0, 12.0, 0.0);
663 second.page = 2;
664 let mut items = vec![first, second];
665 document_items_to_display_frame(&doc, &mut items, &HashMap::new());
666 assert_eq!(geometry(&items[0]), (30.0, 240.0, 12.0, 40.0, 270.0));
667 assert_eq!(geometry(&items[1]), (20.0, 30.0, 40.0, 12.0, 0.0));
668
669 let mut turned = item(30.0, -60.0, 12.0, 40.0, 0.0);
671 turned.page = 2;
672 let mut items = vec![turned];
673 let rotations = HashMap::from([(2, PageRotation::Ccw)]);
674 document_items_to_display_frame(&doc, &mut items, &rotations);
675 assert_eq!(geometry(&items[0]), (20.0, 30.0, 40.0, 12.0, 90.0));
676 }
677}