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, Hash)]
121pub struct FieldSource {
122 pub node: SourceNodeId,
123 pub index: u32,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
129pub struct StructureId(NonZeroU32);
130
131impl StructureId {
132 pub const fn new(value: u32) -> Option<Self> {
134 match NonZeroU32::new(value) {
135 Some(value) => Some(Self(value)),
136 None => None,
137 }
138 }
139
140 pub const fn get(self) -> u32 {
142 self.0.get()
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum StructureRole {
150 Document,
151 Paragraph,
152 Heading(u8),
153 List,
154 ListItem,
155 Table,
156 TableRow,
157 TableHeaderCell,
158 TableCell,
159 Figure,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct StructureNode {
165 pub id: StructureId,
166 pub role: StructureRole,
167 pub children: Vec<StructureId>,
168 pub alternate_text: Option<String>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct DocumentStructure {
174 pub root: StructureId,
175 pub nodes: Vec<StructureNode>,
176}
177
178impl DocumentStructure {
179 pub fn node(&self, id: StructureId) -> Option<&StructureNode> {
181 self.nodes
182 .get(id.get() as usize - 1)
183 .filter(|node| node.id == id)
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum FieldKind {
199 Page,
201 NumPages,
203 Section,
205 SectionPages,
207 TargetPage(usize),
209 Target(usize),
211 SequenceContext(usize),
213 SequenceRepeat,
215}
216
217#[derive(Debug, Clone, PartialEq)]
219pub struct GlyphRun {
220 pub origin: Point,
222 pub font_id: FontId,
224 pub font_size: f64,
226 pub glyph_ids: Vec<u16>,
228 pub advances: Vec<f64>,
230 pub text: String,
232 pub source: Option<SourceSpan>,
234 pub color: Color,
236 pub bold: bool,
238 pub italic: bool,
240 pub field_kind: Option<FieldKind>,
242 pub field_source: Option<FieldSource>,
244 pub note: Option<crate::line::NoteRef>,
246 pub note_reference_source: Option<SourceSpan>,
248 pub tab_aligned: Option<TabAlignedField>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq)]
260pub struct TabAlignedField {
261 pub start: f64,
263 pub end: f64,
265 pub shift: f64,
269 pub gap: f64,
272}
273
274#[derive(Debug, Clone, PartialEq)]
276pub struct MultilingualGlyphRun {
277 pub origin: Point,
278 pub font_id: FontId,
279 pub font_size: f64,
280 pub glyph_ids: Vec<u16>,
281 pub x_advances: Vec<f64>,
282 pub y_advances: Vec<f64>,
283 pub x_offsets: Vec<f64>,
284 pub y_offsets: Vec<f64>,
285 pub clusters: Vec<crate::font::GlyphCluster>,
286 pub logical_text: String,
287 pub logical_index: usize,
288 pub source: Option<SourceSpan>,
289 pub script: crate::font::TextScript,
290 pub language: Option<String>,
291 pub direction: crate::font::TextDirection,
292 pub bidi_level: u8,
293 pub color: Color,
294 pub bold: bool,
295 pub italic: bool,
296 pub field_kind: Option<FieldKind>,
297 pub field_source: Option<FieldSource>,
298 pub note: Option<crate::line::NoteRef>,
299 pub note_reference_source: Option<SourceSpan>,
301}
302
303impl MultilingualGlyphRun {
304 pub fn is_valid(&self) -> bool {
306 let glyph_count = self.glyph_ids.len();
307 self.x_advances.len() == glyph_count
308 && self.y_advances.len() == glyph_count
309 && self.x_offsets.len() == glyph_count
310 && self.y_offsets.len() == glyph_count
311 && unicode_bidi::Level::new(self.bidi_level).is_ok()
312 && crate::font::position_values_are_finite(
313 &self.x_advances,
314 &self.y_advances,
315 &self.x_offsets,
316 &self.y_offsets,
317 self.x_advances.iter().sum(),
318 )
319 && crate::font::cluster_ranges_are_valid(
320 &self.clusters,
321 glyph_count,
322 self.logical_text.chars().count(),
323 self.bidi_level % 2 == 1,
324 )
325 }
326
327 pub fn legacy_projection(&self) -> GlyphRun {
330 GlyphRun {
331 origin: self.origin,
332 font_id: self.font_id,
333 font_size: self.font_size,
334 glyph_ids: self.glyph_ids.clone(),
335 advances: self.x_advances.clone(),
336 text: self.logical_text.clone(),
337 source: self.source,
338 color: self.color,
339 bold: self.bold,
340 italic: self.italic,
341 field_kind: self.field_kind,
342 field_source: self.field_source,
343 note: self.note,
344 note_reference_source: self.note_reference_source,
345 tab_aligned: None,
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq)]
352#[non_exhaustive]
353pub enum PositionedElement {
354 Text(GlyphRun),
356 MultilingualText(MultilingualGlyphRun),
358 Line {
360 start: Point,
361 end: Point,
362 width: f64,
363 color: Color,
364 dash_pattern: Option<(f64, f64)>,
366 },
367 FilledRect { rect: Rect, color: Color },
369 Image {
371 rect: Rect,
372 data: Vec<u8>,
373 content_type: String,
374 media_id: MediaId,
375 },
376 LinkAnnotation { rect: Rect, url: String },
378 Path(PathElement),
380 Group(GroupElement),
382 MarkedContent {
387 structure: Option<StructureId>,
388 children: Vec<PositionedElement>,
389 },
390}
391
392#[derive(Debug, Clone, PartialEq)]
394pub struct PathElement {
395 pub path: Path,
396 pub fill: Option<Paint>,
397 pub stroke: Option<Stroke>,
398}
399
400#[derive(Debug, Clone, PartialEq)]
402pub struct Diagnostic {
403 pub message: String,
404}
405
406#[derive(Debug, Clone, PartialEq)]
408#[non_exhaustive]
409pub enum Effect {
410 OuterShadow {
411 dx: f64,
412 dy: f64,
413 blur: f64,
414 color: Color,
415 },
416}
417
418#[derive(Debug, Clone, PartialEq)]
420pub struct GroupElement {
421 pub transform: Transform,
423 pub clip: Option<Path>,
424 pub opacity: f64,
425 pub effects: Vec<Effect>,
426 pub children: Vec<PositionedElement>,
427}
428
429pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
431 fn visit(
432 elements: &[PositionedElement],
433 accumulated: Transform,
434 f: &mut dyn FnMut(&PositionedElement, &Transform),
435 ) {
436 for element in elements {
437 match element {
438 PositionedElement::Group(group) => {
439 let child_to_page = group.transform.then(accumulated);
440 visit(&group.children, child_to_page, f);
441 }
442 PositionedElement::MarkedContent { children, .. } => {
443 visit(children, accumulated, f);
444 }
445 leaf => f(leaf, &accumulated),
446 }
447 }
448 }
449
450 visit(elements, Transform::IDENTITY, f);
451}
452
453#[derive(Debug, Clone)]
455#[non_exhaustive]
456pub struct PageFrame {
457 pub page_number: usize,
459 pub displayed_page_number: usize,
461 pub width: f64,
463 pub height: f64,
465 pub elements: Vec<PositionedElement>,
467 pub background: Option<Paint>,
469}
470
471impl PageFrame {
472 pub fn new(
481 page_number: usize,
482 width: f64,
483 height: f64,
484 elements: Vec<PositionedElement>,
485 ) -> Self {
486 Self {
487 page_number,
488 displayed_page_number: page_number,
489 width,
490 height,
491 elements,
492 background: None,
493 }
494 }
495}
496
497#[derive(Debug, Clone)]
499pub struct FontData {
500 pub id: FontId,
502 pub family: String,
504 pub data: Arc<[u8]>,
506 pub face_index: u32,
508 pub bold: bool,
510 pub italic: bool,
512 pub variations: Vec<([u8; 4], f32)>,
514 pub synthetic_bold: bool,
516 pub synthetic_italic: bool,
518}
519
520#[derive(Debug, Clone, Default)]
522pub struct DocumentMetadata {
523 pub title: Option<String>,
525 pub author: Option<String>,
527 pub subject: Option<String>,
529 pub keywords: Option<String>,
531 pub creator: Option<String>,
533}
534
535#[derive(Debug, Clone)]
537pub struct OutlineEntry {
538 pub title: String,
540 pub level: u32,
542 pub page_index: usize,
544 pub y_position: f64,
546}
547
548#[derive(Debug, Clone)]
550#[non_exhaustive]
551pub struct LayoutResult {
552 pub pages: Vec<Arc<PageFrame>>,
554 pub fonts: Vec<FontData>,
556 pub metadata: Option<DocumentMetadata>,
558 pub outlines: Vec<OutlineEntry>,
560 pub diagnostics: Vec<Diagnostic>,
562 pub structure: Option<DocumentStructure>,
565}
566
567impl LayoutResult {
568 pub fn new(
577 pages: Vec<Arc<PageFrame>>,
578 fonts: Vec<FontData>,
579 metadata: Option<DocumentMetadata>,
580 outlines: Vec<OutlineEntry>,
581 ) -> Self {
582 Self {
583 pages,
584 fonts,
585 metadata,
586 outlines,
587 diagnostics: Vec::new(),
588 structure: None,
589 }
590 }
591}
592
593#[cfg(test)]
594mod media_id_tests {
595 use std::collections::HashSet;
596
597 use super::{MediaId, PositionedElement, Rect};
598
599 #[test]
600 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
601 let ids = HashSet::from([
602 MediaId::from_bytes(b"same image"),
603 MediaId::from_bytes(b"same image"),
604 ]);
605 assert_eq!(ids.len(), 1);
606 }
607
608 #[test]
609 fn media_id_depends_on_bytes_not_relationship_context() {
610 assert_eq!(
611 MediaId::from_bytes(b"image bytes"),
612 MediaId::from_bytes(b"image bytes")
613 );
614 }
615
616 #[test]
617 fn different_image_bytes_have_different_fixture_ids() {
618 assert_ne!(
619 MediaId::from_bytes(b"first image"),
620 MediaId::from_bytes(b"second image")
621 );
622 }
623
624 #[test]
625 fn staged_output_image_uses_media_id_instead_of_embed_id() {
626 let media_id = MediaId::from_bytes(b"image bytes");
627 let image = PositionedElement::Image {
628 rect: Rect {
629 x: 0.0,
630 y: 0.0,
631 width: 10.0,
632 height: 20.0,
633 },
634 data: b"image bytes".to_vec(),
635 content_type: "image/png".to_owned(),
636 media_id,
637 };
638 let PositionedElement::Image {
639 media_id: actual, ..
640 } = image
641 else {
642 panic!("constructed image should remain an image");
643 };
644 assert_eq!(actual, media_id);
645 }
646}
647
648#[cfg(test)]
649mod tagged_pdf_start_feature_tests {
650 use super::{
651 Color, DocumentStructure, PositionedElement, Rect, StructureId, StructureNode,
652 StructureRole, walk,
653 };
654 use crate::Transform;
655
656 #[test]
657 fn marked_content_is_backend_neutral_and_non_drawing() {
658 let root = StructureId::new(1).expect("non-zero root");
659 let paragraph = StructureId::new(2).expect("non-zero paragraph");
660 let structure = DocumentStructure {
661 root,
662 nodes: vec![
663 StructureNode {
664 id: root,
665 role: StructureRole::Document,
666 children: vec![paragraph],
667 alternate_text: None,
668 },
669 StructureNode {
670 id: paragraph,
671 role: StructureRole::Paragraph,
672 children: Vec::new(),
673 alternate_text: None,
674 },
675 ],
676 };
677 assert_eq!(
678 structure.node(paragraph).map(|node| node.role),
679 Some(StructureRole::Paragraph)
680 );
681
682 let elements = vec![PositionedElement::MarkedContent {
683 structure: Some(paragraph),
684 children: vec![PositionedElement::FilledRect {
685 rect: Rect {
686 x: 1.0,
687 y: 2.0,
688 width: 3.0,
689 height: 4.0,
690 },
691 color: Color::BLACK,
692 }],
693 }];
694 let mut leaves = 0;
695 walk(&elements, &mut |element, transform| {
696 assert!(matches!(element, PositionedElement::FilledRect { .. }));
697 assert_eq!(*transform, Transform::IDENTITY);
698 leaves += 1;
699 });
700 assert_eq!(leaves, 1);
701 }
702}
703
704#[cfg(test)]
705mod group_output_tests {
706 use std::sync::Arc;
707
708 use super::{
709 Color, Diagnostic, Effect, FontData, FontId, GroupElement, LayoutResult, PageFrame,
710 PathElement, PositionedElement, Rect,
711 };
712 use crate::{FillRule, Paint, Path, Stroke, Transform};
713
714 #[test]
715 fn path_and_group_arms_preserve_their_payloads() {
716 let path = Path::rect(Rect {
717 x: 1.0,
718 y: 2.0,
719 width: 3.0,
720 height: 4.0,
721 });
722 let path_element = PathElement {
723 path: path.clone(),
724 fill: Some(Paint::Solid(Color::BLACK)),
725 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
726 };
727 let element = PositionedElement::Path(path_element.clone());
728 assert!(matches!(
729 element,
730 PositionedElement::Path(actual) if actual == path_element
731 ));
732
733 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
734 let clip = Path {
735 commands: Vec::new(),
736 fill_rule: FillRule::EvenOdd,
737 };
738 let effect = Effect::OuterShadow {
739 dx: 1.0,
740 dy: 2.0,
741 blur: 3.0,
742 color: Color::BLACK,
743 };
744 let child_rect = Rect {
745 x: 5.0,
746 y: 6.0,
747 width: 7.0,
748 height: 8.0,
749 };
750 let group = GroupElement {
751 transform,
752 clip: Some(clip.clone()),
753 opacity: 0.5,
754 effects: vec![effect.clone()],
755 children: vec![PositionedElement::FilledRect {
756 rect: child_rect,
757 color: Color::WHITE,
758 }],
759 };
760 let element = PositionedElement::Group(group);
761 let PositionedElement::Group(actual) = element else {
762 panic!("constructed group should remain a group");
763 };
764 assert_eq!(actual.transform, transform);
765 assert_eq!(actual.clip, Some(clip));
766 assert_eq!(actual.opacity, 0.5);
767 assert_eq!(actual.effects, vec![effect]);
768 assert!(matches!(
769 actual.children.as_slice(),
770 [PositionedElement::FilledRect { rect, color }]
771 if *rect == child_rect && *color == Color::WHITE
772 ));
773 }
774
775 #[test]
776 fn page_frame_new_defaults_background_to_none() {
777 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
778 assert_eq!(page.page_number, 1);
779 assert_eq!(page.background, None);
780 }
781
782 #[test]
783 fn layout_result_new_defaults_diagnostics_to_empty() {
784 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
785 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
786 }
787
788 #[test]
789 fn cloned_layout_shares_font_bytes_and_page_frames() {
790 let page = Arc::new(PageFrame::new(1, 612.0, 792.0, Vec::new()));
791 let font_bytes: Arc<[u8]> = Arc::from([1, 2, 3, 4]);
792 let result = LayoutResult::new(
793 vec![Arc::clone(&page)],
794 vec![FontData {
795 id: FontId(1),
796 family: "Shared".to_owned(),
797 data: Arc::clone(&font_bytes),
798 face_index: 0,
799 bold: false,
800 italic: false,
801 variations: vec![],
802 synthetic_bold: false,
803 synthetic_italic: false,
804 }],
805 None,
806 Vec::new(),
807 );
808
809 let cloned = result.clone();
810 assert!(Arc::ptr_eq(&result.pages[0], &cloned.pages[0]));
811 assert!(Arc::ptr_eq(&result.fonts[0].data, &cloned.fonts[0].data));
812 assert_eq!(result.pages[0].page_number, cloned.pages[0].page_number);
813 assert_eq!(result.fonts[0].data.as_ref(), cloned.fonts[0].data.as_ref());
814 }
815
816 #[test]
817 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
818 let child_to_parent = Transform {
819 a: 1.0,
820 b: 0.0,
821 c: 0.0,
822 d: 1.0,
823 e: 10.0,
824 f: 20.0,
825 };
826 let group = GroupElement {
827 transform: child_to_parent,
828 clip: None,
829 opacity: 1.0,
830 effects: Vec::new(),
831 children: Vec::new(),
832 };
833 assert_eq!(
834 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
835 super::Point { x: 11.0, y: 22.0 }
836 );
837 }
838}
839
840#[cfg(test)]
841mod walk_tests {
842 use super::{Color, GroupElement, PositionedElement, Rect, walk};
843 use crate::{Point, Transform};
844
845 fn translate(x: f64, y: f64) -> Transform {
846 Transform {
847 e: x,
848 f: y,
849 ..Transform::IDENTITY
850 }
851 }
852
853 fn scale(value: f64) -> Transform {
854 Transform {
855 a: value,
856 d: value,
857 ..Transform::IDENTITY
858 }
859 }
860
861 fn leaf(id: f64) -> PositionedElement {
862 PositionedElement::FilledRect {
863 rect: Rect {
864 x: id,
865 y: 0.0,
866 width: 1.0,
867 height: 1.0,
868 },
869 color: Color::BLACK,
870 }
871 }
872
873 #[test]
874 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
875 let elements = vec![
876 leaf(1.0),
877 PositionedElement::Group(GroupElement {
878 transform: translate(10.0, 0.0),
879 clip: None,
880 opacity: 1.0,
881 effects: Vec::new(),
882 children: vec![PositionedElement::Group(GroupElement {
883 transform: scale(2.0),
884 clip: None,
885 opacity: 1.0,
886 effects: Vec::new(),
887 children: vec![PositionedElement::Group(GroupElement {
888 transform: translate(0.0, 5.0),
889 clip: None,
890 opacity: 1.0,
891 effects: Vec::new(),
892 children: vec![leaf(2.0)],
893 })],
894 })],
895 }),
896 leaf(3.0),
897 ];
898 let mut visited = Vec::new();
899 walk(&elements, &mut |element, transform| {
900 let PositionedElement::FilledRect { rect, .. } = element else {
901 panic!("walk should yield leaves only");
902 };
903 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
904 });
905 assert_eq!(
906 visited,
907 vec![
908 (1.0, Point { x: 1.0, y: 1.0 }),
909 (2.0, Point { x: 12.0, y: 12.0 }),
910 (3.0, Point { x: 1.0, y: 1.0 }),
911 ]
912 );
913 }
914
915 #[test]
916 fn nested_group_transform_order_applies_child_before_parent() {
917 let group = PositionedElement::Group(GroupElement {
918 transform: translate(10.0, 0.0),
919 clip: None,
920 opacity: 1.0,
921 effects: Vec::new(),
922 children: vec![PositionedElement::Group(GroupElement {
923 transform: scale(2.0),
924 clip: None,
925 opacity: 1.0,
926 effects: Vec::new(),
927 children: vec![leaf(1.0)],
928 })],
929 });
930 let mut points = Vec::new();
931 walk(&[group], &mut |_, transform| {
932 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
933 });
934 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
935 }
936
937 #[test]
938 fn walk_does_not_yield_group_nodes() {
939 let group = PositionedElement::Group(GroupElement {
940 transform: Transform::IDENTITY,
941 clip: None,
942 opacity: 1.0,
943 effects: Vec::new(),
944 children: vec![leaf(1.0)],
945 });
946 walk(&[group], &mut |element, _| {
947 assert!(!matches!(element, PositionedElement::Group(_)));
948 });
949 }
950
951 #[test]
952 fn walk_passes_identity_for_root_leaves() {
953 walk(&[leaf(1.0)], &mut |_, transform| {
954 assert_eq!(*transform, Transform::IDENTITY);
955 });
956 }
957}