1use std::num::NonZeroU32;
4use std::sync::Arc;
5
6use crate::paint::{Paint, Stroke};
7use crate::path::Path;
8use crate::transform::Transform;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Point {
13 pub x: f64,
14 pub y: f64,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Rect {
20 pub x: f64,
21 pub y: f64,
22 pub width: f64,
23 pub height: f64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Color {
29 pub r: f64,
30 pub g: f64,
31 pub b: f64,
32 pub a: f64,
33}
34
35impl Color {
36 pub const BLACK: Color = Color {
37 r: 0.0,
38 g: 0.0,
39 b: 0.0,
40 a: 1.0,
41 };
42 pub const WHITE: Color = Color {
43 r: 1.0,
44 g: 1.0,
45 b: 1.0,
46 a: 1.0,
47 };
48
49 pub fn from_hex(hex: &str) -> Self {
51 let hex = hex.trim_start_matches('#');
52 if hex.len() >= 6 {
53 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
54 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
55 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
56 Color { r, g, b, a: 1.0 }
57 } else {
58 Color::BLACK
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct FontId(pub u32);
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub struct MediaId(pub u64);
76
77impl MediaId {
78 pub fn from_bytes(bytes: &[u8]) -> Self {
80 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
81 for byte in bytes {
82 hash ^= u64::from(*byte);
83 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
84 }
85 Self(hash)
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub struct SourceNodeId(NonZeroU32);
92
93impl SourceNodeId {
94 pub const fn new(value: u32) -> Option<Self> {
96 match NonZeroU32::new(value) {
97 Some(value) => Some(Self(value)),
98 None => None,
99 }
100 }
101
102 pub const fn get(self) -> u32 {
104 self.0.get()
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct SourceSpan {
111 pub node: SourceNodeId,
112 pub char_start: u32,
113 pub char_end: u32,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct StructureId(NonZeroU32);
119
120impl StructureId {
121 pub const fn new(value: u32) -> Option<Self> {
123 match NonZeroU32::new(value) {
124 Some(value) => Some(Self(value)),
125 None => None,
126 }
127 }
128
129 pub const fn get(self) -> u32 {
131 self.0.get()
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137#[non_exhaustive]
138pub enum StructureRole {
139 Document,
140 Paragraph,
141 Heading(u8),
142 List,
143 ListItem,
144 Table,
145 TableRow,
146 TableHeaderCell,
147 TableCell,
148 Figure,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct StructureNode {
154 pub id: StructureId,
155 pub role: StructureRole,
156 pub children: Vec<StructureId>,
157 pub alternate_text: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct DocumentStructure {
163 pub root: StructureId,
164 pub nodes: Vec<StructureNode>,
165}
166
167impl DocumentStructure {
168 pub fn node(&self, id: StructureId) -> Option<&StructureNode> {
170 self.nodes
171 .get(id.get() as usize - 1)
172 .filter(|node| node.id == id)
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum FieldKind {
188 Page,
190 NumPages,
192 TargetPage(usize),
194 Target(usize),
196}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct GlyphRun {
201 pub origin: Point,
203 pub font_id: FontId,
205 pub font_size: f64,
207 pub glyph_ids: Vec<u16>,
209 pub advances: Vec<f64>,
211 pub text: String,
213 pub source: Option<SourceSpan>,
215 pub color: Color,
217 pub bold: bool,
219 pub italic: bool,
221 pub field_kind: Option<FieldKind>,
223 pub note: Option<crate::line::NoteRef>,
225}
226
227#[derive(Debug, Clone, PartialEq)]
229#[non_exhaustive]
230pub enum PositionedElement {
231 Text(GlyphRun),
233 Line {
235 start: Point,
236 end: Point,
237 width: f64,
238 color: Color,
239 dash_pattern: Option<(f64, f64)>,
241 },
242 FilledRect { rect: Rect, color: Color },
244 Image {
246 rect: Rect,
247 data: Vec<u8>,
248 content_type: String,
249 media_id: MediaId,
250 },
251 LinkAnnotation { rect: Rect, url: String },
253 Path(PathElement),
255 Group(GroupElement),
257 MarkedContent {
262 structure: Option<StructureId>,
263 children: Vec<PositionedElement>,
264 },
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct PathElement {
270 pub path: Path,
271 pub fill: Option<Paint>,
272 pub stroke: Option<Stroke>,
273}
274
275#[derive(Debug, Clone, PartialEq)]
277pub struct Diagnostic {
278 pub message: String,
279}
280
281#[derive(Debug, Clone, PartialEq)]
283#[non_exhaustive]
284pub enum Effect {
285 OuterShadow {
286 dx: f64,
287 dy: f64,
288 blur: f64,
289 color: Color,
290 },
291}
292
293#[derive(Debug, Clone, PartialEq)]
295pub struct GroupElement {
296 pub transform: Transform,
298 pub clip: Option<Path>,
299 pub opacity: f64,
300 pub effects: Vec<Effect>,
301 pub children: Vec<PositionedElement>,
302}
303
304pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
306 fn visit(
307 elements: &[PositionedElement],
308 accumulated: Transform,
309 f: &mut dyn FnMut(&PositionedElement, &Transform),
310 ) {
311 for element in elements {
312 match element {
313 PositionedElement::Group(group) => {
314 let child_to_page = group.transform.then(accumulated);
315 visit(&group.children, child_to_page, f);
316 }
317 PositionedElement::MarkedContent { children, .. } => {
318 visit(children, accumulated, f);
319 }
320 leaf => f(leaf, &accumulated),
321 }
322 }
323 }
324
325 visit(elements, Transform::IDENTITY, f);
326}
327
328#[derive(Debug, Clone)]
330#[non_exhaustive]
331pub struct PageFrame {
332 pub page_number: usize,
334 pub width: f64,
336 pub height: f64,
338 pub elements: Vec<PositionedElement>,
340 pub background: Option<Paint>,
342}
343
344impl PageFrame {
345 pub fn new(
354 page_number: usize,
355 width: f64,
356 height: f64,
357 elements: Vec<PositionedElement>,
358 ) -> Self {
359 Self {
360 page_number,
361 width,
362 height,
363 elements,
364 background: None,
365 }
366 }
367}
368
369#[derive(Debug, Clone)]
371pub struct FontData {
372 pub id: FontId,
374 pub family: String,
376 pub data: Arc<[u8]>,
378 pub face_index: u32,
380 pub bold: bool,
382 pub italic: bool,
384}
385
386#[derive(Debug, Clone, Default)]
388pub struct DocumentMetadata {
389 pub title: Option<String>,
391 pub author: Option<String>,
393 pub subject: Option<String>,
395 pub keywords: Option<String>,
397 pub creator: Option<String>,
399}
400
401#[derive(Debug, Clone)]
403pub struct OutlineEntry {
404 pub title: String,
406 pub level: u32,
408 pub page_index: usize,
410 pub y_position: f64,
412}
413
414#[derive(Debug, Clone)]
416#[non_exhaustive]
417pub struct LayoutResult {
418 pub pages: Vec<Arc<PageFrame>>,
420 pub fonts: Vec<FontData>,
422 pub metadata: Option<DocumentMetadata>,
424 pub outlines: Vec<OutlineEntry>,
426 pub diagnostics: Vec<Diagnostic>,
428 pub structure: Option<DocumentStructure>,
431}
432
433impl LayoutResult {
434 pub fn new(
443 pages: Vec<Arc<PageFrame>>,
444 fonts: Vec<FontData>,
445 metadata: Option<DocumentMetadata>,
446 outlines: Vec<OutlineEntry>,
447 ) -> Self {
448 Self {
449 pages,
450 fonts,
451 metadata,
452 outlines,
453 diagnostics: Vec::new(),
454 structure: None,
455 }
456 }
457}
458
459#[cfg(test)]
460mod media_id_tests {
461 use std::collections::HashSet;
462
463 use super::{MediaId, PositionedElement, Rect};
464
465 #[test]
466 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
467 let ids = HashSet::from([
468 MediaId::from_bytes(b"same image"),
469 MediaId::from_bytes(b"same image"),
470 ]);
471 assert_eq!(ids.len(), 1);
472 }
473
474 #[test]
475 fn media_id_depends_on_bytes_not_relationship_context() {
476 assert_eq!(
477 MediaId::from_bytes(b"image bytes"),
478 MediaId::from_bytes(b"image bytes")
479 );
480 }
481
482 #[test]
483 fn different_image_bytes_have_different_fixture_ids() {
484 assert_ne!(
485 MediaId::from_bytes(b"first image"),
486 MediaId::from_bytes(b"second image")
487 );
488 }
489
490 #[test]
491 fn staged_output_image_uses_media_id_instead_of_embed_id() {
492 let media_id = MediaId::from_bytes(b"image bytes");
493 let image = PositionedElement::Image {
494 rect: Rect {
495 x: 0.0,
496 y: 0.0,
497 width: 10.0,
498 height: 20.0,
499 },
500 data: b"image bytes".to_vec(),
501 content_type: "image/png".to_owned(),
502 media_id,
503 };
504 let PositionedElement::Image {
505 media_id: actual, ..
506 } = image
507 else {
508 panic!("constructed image should remain an image");
509 };
510 assert_eq!(actual, media_id);
511 }
512}
513
514#[cfg(test)]
515mod tagged_pdf_start_feature_tests {
516 use super::{
517 Color, DocumentStructure, PositionedElement, Rect, StructureId, StructureNode,
518 StructureRole, walk,
519 };
520 use crate::Transform;
521
522 #[test]
523 fn marked_content_is_backend_neutral_and_non_drawing() {
524 let root = StructureId::new(1).expect("non-zero root");
525 let paragraph = StructureId::new(2).expect("non-zero paragraph");
526 let structure = DocumentStructure {
527 root,
528 nodes: vec![
529 StructureNode {
530 id: root,
531 role: StructureRole::Document,
532 children: vec![paragraph],
533 alternate_text: None,
534 },
535 StructureNode {
536 id: paragraph,
537 role: StructureRole::Paragraph,
538 children: Vec::new(),
539 alternate_text: None,
540 },
541 ],
542 };
543 assert_eq!(
544 structure.node(paragraph).map(|node| node.role),
545 Some(StructureRole::Paragraph)
546 );
547
548 let elements = vec![PositionedElement::MarkedContent {
549 structure: Some(paragraph),
550 children: vec![PositionedElement::FilledRect {
551 rect: Rect {
552 x: 1.0,
553 y: 2.0,
554 width: 3.0,
555 height: 4.0,
556 },
557 color: Color::BLACK,
558 }],
559 }];
560 let mut leaves = 0;
561 walk(&elements, &mut |element, transform| {
562 assert!(matches!(element, PositionedElement::FilledRect { .. }));
563 assert_eq!(*transform, Transform::IDENTITY);
564 leaves += 1;
565 });
566 assert_eq!(leaves, 1);
567 }
568}
569
570#[cfg(test)]
571mod group_output_tests {
572 use std::sync::Arc;
573
574 use super::{
575 Color, Diagnostic, Effect, FontData, FontId, GroupElement, LayoutResult, PageFrame,
576 PathElement, PositionedElement, Rect,
577 };
578 use crate::{FillRule, Paint, Path, Stroke, Transform};
579
580 #[test]
581 fn path_and_group_arms_preserve_their_payloads() {
582 let path = Path::rect(Rect {
583 x: 1.0,
584 y: 2.0,
585 width: 3.0,
586 height: 4.0,
587 });
588 let path_element = PathElement {
589 path: path.clone(),
590 fill: Some(Paint::Solid(Color::BLACK)),
591 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
592 };
593 let element = PositionedElement::Path(path_element.clone());
594 assert!(matches!(
595 element,
596 PositionedElement::Path(actual) if actual == path_element
597 ));
598
599 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
600 let clip = Path {
601 commands: Vec::new(),
602 fill_rule: FillRule::EvenOdd,
603 };
604 let effect = Effect::OuterShadow {
605 dx: 1.0,
606 dy: 2.0,
607 blur: 3.0,
608 color: Color::BLACK,
609 };
610 let child_rect = Rect {
611 x: 5.0,
612 y: 6.0,
613 width: 7.0,
614 height: 8.0,
615 };
616 let group = GroupElement {
617 transform,
618 clip: Some(clip.clone()),
619 opacity: 0.5,
620 effects: vec![effect.clone()],
621 children: vec![PositionedElement::FilledRect {
622 rect: child_rect,
623 color: Color::WHITE,
624 }],
625 };
626 let element = PositionedElement::Group(group);
627 let PositionedElement::Group(actual) = element else {
628 panic!("constructed group should remain a group");
629 };
630 assert_eq!(actual.transform, transform);
631 assert_eq!(actual.clip, Some(clip));
632 assert_eq!(actual.opacity, 0.5);
633 assert_eq!(actual.effects, vec![effect]);
634 assert!(matches!(
635 actual.children.as_slice(),
636 [PositionedElement::FilledRect { rect, color }]
637 if *rect == child_rect && *color == Color::WHITE
638 ));
639 }
640
641 #[test]
642 fn page_frame_new_defaults_background_to_none() {
643 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
644 assert_eq!(page.page_number, 1);
645 assert_eq!(page.background, None);
646 }
647
648 #[test]
649 fn layout_result_new_defaults_diagnostics_to_empty() {
650 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
651 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
652 }
653
654 #[test]
655 fn cloned_layout_shares_font_bytes_and_page_frames() {
656 let page = Arc::new(PageFrame::new(1, 612.0, 792.0, Vec::new()));
657 let font_bytes: Arc<[u8]> = Arc::from([1, 2, 3, 4]);
658 let result = LayoutResult::new(
659 vec![Arc::clone(&page)],
660 vec![FontData {
661 id: FontId(1),
662 family: "Shared".to_owned(),
663 data: Arc::clone(&font_bytes),
664 face_index: 0,
665 bold: false,
666 italic: false,
667 }],
668 None,
669 Vec::new(),
670 );
671
672 let cloned = result.clone();
673 assert!(Arc::ptr_eq(&result.pages[0], &cloned.pages[0]));
674 assert!(Arc::ptr_eq(&result.fonts[0].data, &cloned.fonts[0].data));
675 assert_eq!(result.pages[0].page_number, cloned.pages[0].page_number);
676 assert_eq!(result.fonts[0].data.as_ref(), cloned.fonts[0].data.as_ref());
677 }
678
679 #[test]
680 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
681 let child_to_parent = Transform {
682 a: 1.0,
683 b: 0.0,
684 c: 0.0,
685 d: 1.0,
686 e: 10.0,
687 f: 20.0,
688 };
689 let group = GroupElement {
690 transform: child_to_parent,
691 clip: None,
692 opacity: 1.0,
693 effects: Vec::new(),
694 children: Vec::new(),
695 };
696 assert_eq!(
697 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
698 super::Point { x: 11.0, y: 22.0 }
699 );
700 }
701}
702
703#[cfg(test)]
704mod walk_tests {
705 use super::{Color, GroupElement, PositionedElement, Rect, walk};
706 use crate::{Point, Transform};
707
708 fn translate(x: f64, y: f64) -> Transform {
709 Transform {
710 e: x,
711 f: y,
712 ..Transform::IDENTITY
713 }
714 }
715
716 fn scale(value: f64) -> Transform {
717 Transform {
718 a: value,
719 d: value,
720 ..Transform::IDENTITY
721 }
722 }
723
724 fn leaf(id: f64) -> PositionedElement {
725 PositionedElement::FilledRect {
726 rect: Rect {
727 x: id,
728 y: 0.0,
729 width: 1.0,
730 height: 1.0,
731 },
732 color: Color::BLACK,
733 }
734 }
735
736 #[test]
737 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
738 let elements = vec![
739 leaf(1.0),
740 PositionedElement::Group(GroupElement {
741 transform: translate(10.0, 0.0),
742 clip: None,
743 opacity: 1.0,
744 effects: Vec::new(),
745 children: vec![PositionedElement::Group(GroupElement {
746 transform: scale(2.0),
747 clip: None,
748 opacity: 1.0,
749 effects: Vec::new(),
750 children: vec![PositionedElement::Group(GroupElement {
751 transform: translate(0.0, 5.0),
752 clip: None,
753 opacity: 1.0,
754 effects: Vec::new(),
755 children: vec![leaf(2.0)],
756 })],
757 })],
758 }),
759 leaf(3.0),
760 ];
761 let mut visited = Vec::new();
762 walk(&elements, &mut |element, transform| {
763 let PositionedElement::FilledRect { rect, .. } = element else {
764 panic!("walk should yield leaves only");
765 };
766 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
767 });
768 assert_eq!(
769 visited,
770 vec![
771 (1.0, Point { x: 1.0, y: 1.0 }),
772 (2.0, Point { x: 12.0, y: 12.0 }),
773 (3.0, Point { x: 1.0, y: 1.0 }),
774 ]
775 );
776 }
777
778 #[test]
779 fn nested_group_transform_order_applies_child_before_parent() {
780 let group = PositionedElement::Group(GroupElement {
781 transform: translate(10.0, 0.0),
782 clip: None,
783 opacity: 1.0,
784 effects: Vec::new(),
785 children: vec![PositionedElement::Group(GroupElement {
786 transform: scale(2.0),
787 clip: None,
788 opacity: 1.0,
789 effects: Vec::new(),
790 children: vec![leaf(1.0)],
791 })],
792 });
793 let mut points = Vec::new();
794 walk(&[group], &mut |_, transform| {
795 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
796 });
797 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
798 }
799
800 #[test]
801 fn walk_does_not_yield_group_nodes() {
802 let group = PositionedElement::Group(GroupElement {
803 transform: Transform::IDENTITY,
804 clip: None,
805 opacity: 1.0,
806 effects: Vec::new(),
807 children: vec![leaf(1.0)],
808 });
809 walk(&[group], &mut |element, _| {
810 assert!(!matches!(element, PositionedElement::Group(_)));
811 });
812 }
813
814 #[test]
815 fn walk_passes_identity_for_root_leaves() {
816 walk(&[leaf(1.0)], &mut |_, transform| {
817 assert_eq!(*transform, Transform::IDENTITY);
818 });
819 }
820}