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}
513
514#[derive(Debug, Clone, Default)]
516pub struct DocumentMetadata {
517 pub title: Option<String>,
519 pub author: Option<String>,
521 pub subject: Option<String>,
523 pub keywords: Option<String>,
525 pub creator: Option<String>,
527}
528
529#[derive(Debug, Clone)]
531pub struct OutlineEntry {
532 pub title: String,
534 pub level: u32,
536 pub page_index: usize,
538 pub y_position: f64,
540}
541
542#[derive(Debug, Clone)]
544#[non_exhaustive]
545pub struct LayoutResult {
546 pub pages: Vec<Arc<PageFrame>>,
548 pub fonts: Vec<FontData>,
550 pub metadata: Option<DocumentMetadata>,
552 pub outlines: Vec<OutlineEntry>,
554 pub diagnostics: Vec<Diagnostic>,
556 pub structure: Option<DocumentStructure>,
559}
560
561impl LayoutResult {
562 pub fn new(
571 pages: Vec<Arc<PageFrame>>,
572 fonts: Vec<FontData>,
573 metadata: Option<DocumentMetadata>,
574 outlines: Vec<OutlineEntry>,
575 ) -> Self {
576 Self {
577 pages,
578 fonts,
579 metadata,
580 outlines,
581 diagnostics: Vec::new(),
582 structure: None,
583 }
584 }
585}
586
587#[cfg(test)]
588mod media_id_tests {
589 use std::collections::HashSet;
590
591 use super::{MediaId, PositionedElement, Rect};
592
593 #[test]
594 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
595 let ids = HashSet::from([
596 MediaId::from_bytes(b"same image"),
597 MediaId::from_bytes(b"same image"),
598 ]);
599 assert_eq!(ids.len(), 1);
600 }
601
602 #[test]
603 fn media_id_depends_on_bytes_not_relationship_context() {
604 assert_eq!(
605 MediaId::from_bytes(b"image bytes"),
606 MediaId::from_bytes(b"image bytes")
607 );
608 }
609
610 #[test]
611 fn different_image_bytes_have_different_fixture_ids() {
612 assert_ne!(
613 MediaId::from_bytes(b"first image"),
614 MediaId::from_bytes(b"second image")
615 );
616 }
617
618 #[test]
619 fn staged_output_image_uses_media_id_instead_of_embed_id() {
620 let media_id = MediaId::from_bytes(b"image bytes");
621 let image = PositionedElement::Image {
622 rect: Rect {
623 x: 0.0,
624 y: 0.0,
625 width: 10.0,
626 height: 20.0,
627 },
628 data: b"image bytes".to_vec(),
629 content_type: "image/png".to_owned(),
630 media_id,
631 };
632 let PositionedElement::Image {
633 media_id: actual, ..
634 } = image
635 else {
636 panic!("constructed image should remain an image");
637 };
638 assert_eq!(actual, media_id);
639 }
640}
641
642#[cfg(test)]
643mod tagged_pdf_start_feature_tests {
644 use super::{
645 Color, DocumentStructure, PositionedElement, Rect, StructureId, StructureNode,
646 StructureRole, walk,
647 };
648 use crate::Transform;
649
650 #[test]
651 fn marked_content_is_backend_neutral_and_non_drawing() {
652 let root = StructureId::new(1).expect("non-zero root");
653 let paragraph = StructureId::new(2).expect("non-zero paragraph");
654 let structure = DocumentStructure {
655 root,
656 nodes: vec![
657 StructureNode {
658 id: root,
659 role: StructureRole::Document,
660 children: vec![paragraph],
661 alternate_text: None,
662 },
663 StructureNode {
664 id: paragraph,
665 role: StructureRole::Paragraph,
666 children: Vec::new(),
667 alternate_text: None,
668 },
669 ],
670 };
671 assert_eq!(
672 structure.node(paragraph).map(|node| node.role),
673 Some(StructureRole::Paragraph)
674 );
675
676 let elements = vec![PositionedElement::MarkedContent {
677 structure: Some(paragraph),
678 children: vec![PositionedElement::FilledRect {
679 rect: Rect {
680 x: 1.0,
681 y: 2.0,
682 width: 3.0,
683 height: 4.0,
684 },
685 color: Color::BLACK,
686 }],
687 }];
688 let mut leaves = 0;
689 walk(&elements, &mut |element, transform| {
690 assert!(matches!(element, PositionedElement::FilledRect { .. }));
691 assert_eq!(*transform, Transform::IDENTITY);
692 leaves += 1;
693 });
694 assert_eq!(leaves, 1);
695 }
696}
697
698#[cfg(test)]
699mod group_output_tests {
700 use std::sync::Arc;
701
702 use super::{
703 Color, Diagnostic, Effect, FontData, FontId, GroupElement, LayoutResult, PageFrame,
704 PathElement, PositionedElement, Rect,
705 };
706 use crate::{FillRule, Paint, Path, Stroke, Transform};
707
708 #[test]
709 fn path_and_group_arms_preserve_their_payloads() {
710 let path = Path::rect(Rect {
711 x: 1.0,
712 y: 2.0,
713 width: 3.0,
714 height: 4.0,
715 });
716 let path_element = PathElement {
717 path: path.clone(),
718 fill: Some(Paint::Solid(Color::BLACK)),
719 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
720 };
721 let element = PositionedElement::Path(path_element.clone());
722 assert!(matches!(
723 element,
724 PositionedElement::Path(actual) if actual == path_element
725 ));
726
727 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
728 let clip = Path {
729 commands: Vec::new(),
730 fill_rule: FillRule::EvenOdd,
731 };
732 let effect = Effect::OuterShadow {
733 dx: 1.0,
734 dy: 2.0,
735 blur: 3.0,
736 color: Color::BLACK,
737 };
738 let child_rect = Rect {
739 x: 5.0,
740 y: 6.0,
741 width: 7.0,
742 height: 8.0,
743 };
744 let group = GroupElement {
745 transform,
746 clip: Some(clip.clone()),
747 opacity: 0.5,
748 effects: vec![effect.clone()],
749 children: vec![PositionedElement::FilledRect {
750 rect: child_rect,
751 color: Color::WHITE,
752 }],
753 };
754 let element = PositionedElement::Group(group);
755 let PositionedElement::Group(actual) = element else {
756 panic!("constructed group should remain a group");
757 };
758 assert_eq!(actual.transform, transform);
759 assert_eq!(actual.clip, Some(clip));
760 assert_eq!(actual.opacity, 0.5);
761 assert_eq!(actual.effects, vec![effect]);
762 assert!(matches!(
763 actual.children.as_slice(),
764 [PositionedElement::FilledRect { rect, color }]
765 if *rect == child_rect && *color == Color::WHITE
766 ));
767 }
768
769 #[test]
770 fn page_frame_new_defaults_background_to_none() {
771 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
772 assert_eq!(page.page_number, 1);
773 assert_eq!(page.background, None);
774 }
775
776 #[test]
777 fn layout_result_new_defaults_diagnostics_to_empty() {
778 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
779 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
780 }
781
782 #[test]
783 fn cloned_layout_shares_font_bytes_and_page_frames() {
784 let page = Arc::new(PageFrame::new(1, 612.0, 792.0, Vec::new()));
785 let font_bytes: Arc<[u8]> = Arc::from([1, 2, 3, 4]);
786 let result = LayoutResult::new(
787 vec![Arc::clone(&page)],
788 vec![FontData {
789 id: FontId(1),
790 family: "Shared".to_owned(),
791 data: Arc::clone(&font_bytes),
792 face_index: 0,
793 bold: false,
794 italic: false,
795 }],
796 None,
797 Vec::new(),
798 );
799
800 let cloned = result.clone();
801 assert!(Arc::ptr_eq(&result.pages[0], &cloned.pages[0]));
802 assert!(Arc::ptr_eq(&result.fonts[0].data, &cloned.fonts[0].data));
803 assert_eq!(result.pages[0].page_number, cloned.pages[0].page_number);
804 assert_eq!(result.fonts[0].data.as_ref(), cloned.fonts[0].data.as_ref());
805 }
806
807 #[test]
808 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
809 let child_to_parent = Transform {
810 a: 1.0,
811 b: 0.0,
812 c: 0.0,
813 d: 1.0,
814 e: 10.0,
815 f: 20.0,
816 };
817 let group = GroupElement {
818 transform: child_to_parent,
819 clip: None,
820 opacity: 1.0,
821 effects: Vec::new(),
822 children: Vec::new(),
823 };
824 assert_eq!(
825 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
826 super::Point { x: 11.0, y: 22.0 }
827 );
828 }
829}
830
831#[cfg(test)]
832mod walk_tests {
833 use super::{Color, GroupElement, PositionedElement, Rect, walk};
834 use crate::{Point, Transform};
835
836 fn translate(x: f64, y: f64) -> Transform {
837 Transform {
838 e: x,
839 f: y,
840 ..Transform::IDENTITY
841 }
842 }
843
844 fn scale(value: f64) -> Transform {
845 Transform {
846 a: value,
847 d: value,
848 ..Transform::IDENTITY
849 }
850 }
851
852 fn leaf(id: f64) -> PositionedElement {
853 PositionedElement::FilledRect {
854 rect: Rect {
855 x: id,
856 y: 0.0,
857 width: 1.0,
858 height: 1.0,
859 },
860 color: Color::BLACK,
861 }
862 }
863
864 #[test]
865 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
866 let elements = vec![
867 leaf(1.0),
868 PositionedElement::Group(GroupElement {
869 transform: translate(10.0, 0.0),
870 clip: None,
871 opacity: 1.0,
872 effects: Vec::new(),
873 children: vec![PositionedElement::Group(GroupElement {
874 transform: scale(2.0),
875 clip: None,
876 opacity: 1.0,
877 effects: Vec::new(),
878 children: vec![PositionedElement::Group(GroupElement {
879 transform: translate(0.0, 5.0),
880 clip: None,
881 opacity: 1.0,
882 effects: Vec::new(),
883 children: vec![leaf(2.0)],
884 })],
885 })],
886 }),
887 leaf(3.0),
888 ];
889 let mut visited = Vec::new();
890 walk(&elements, &mut |element, transform| {
891 let PositionedElement::FilledRect { rect, .. } = element else {
892 panic!("walk should yield leaves only");
893 };
894 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
895 });
896 assert_eq!(
897 visited,
898 vec![
899 (1.0, Point { x: 1.0, y: 1.0 }),
900 (2.0, Point { x: 12.0, y: 12.0 }),
901 (3.0, Point { x: 1.0, y: 1.0 }),
902 ]
903 );
904 }
905
906 #[test]
907 fn nested_group_transform_order_applies_child_before_parent() {
908 let group = PositionedElement::Group(GroupElement {
909 transform: translate(10.0, 0.0),
910 clip: None,
911 opacity: 1.0,
912 effects: Vec::new(),
913 children: vec![PositionedElement::Group(GroupElement {
914 transform: scale(2.0),
915 clip: None,
916 opacity: 1.0,
917 effects: Vec::new(),
918 children: vec![leaf(1.0)],
919 })],
920 });
921 let mut points = Vec::new();
922 walk(&[group], &mut |_, transform| {
923 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
924 });
925 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
926 }
927
928 #[test]
929 fn walk_does_not_yield_group_nodes() {
930 let group = PositionedElement::Group(GroupElement {
931 transform: Transform::IDENTITY,
932 clip: None,
933 opacity: 1.0,
934 effects: Vec::new(),
935 children: vec![leaf(1.0)],
936 });
937 walk(&[group], &mut |element, _| {
938 assert!(!matches!(element, PositionedElement::Group(_)));
939 });
940 }
941
942 #[test]
943 fn walk_passes_identity_for_root_leaves() {
944 walk(&[leaf(1.0)], &mut |_, transform| {
945 assert_eq!(*transform, Transform::IDENTITY);
946 });
947 }
948}