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 TargetPage(usize),
205 Target(usize),
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct GlyphRun {
212 pub origin: Point,
214 pub font_id: FontId,
216 pub font_size: f64,
218 pub glyph_ids: Vec<u16>,
220 pub advances: Vec<f64>,
222 pub text: String,
224 pub source: Option<SourceSpan>,
226 pub color: Color,
228 pub bold: bool,
230 pub italic: bool,
232 pub field_kind: Option<FieldKind>,
234 pub field_source: Option<FieldSource>,
236 pub note: Option<crate::line::NoteRef>,
238 pub tab_aligned: Option<TabAlignedField>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq)]
250pub struct TabAlignedField {
251 pub start: f64,
253 pub end: f64,
255 pub shift: f64,
259 pub gap: f64,
262}
263
264#[derive(Debug, Clone, PartialEq)]
266pub struct MultilingualGlyphRun {
267 pub origin: Point,
268 pub font_id: FontId,
269 pub font_size: f64,
270 pub glyph_ids: Vec<u16>,
271 pub x_advances: Vec<f64>,
272 pub y_advances: Vec<f64>,
273 pub x_offsets: Vec<f64>,
274 pub y_offsets: Vec<f64>,
275 pub clusters: Vec<crate::font::GlyphCluster>,
276 pub logical_text: String,
277 pub logical_index: usize,
278 pub source: Option<SourceSpan>,
279 pub script: crate::font::TextScript,
280 pub language: Option<String>,
281 pub direction: crate::font::TextDirection,
282 pub bidi_level: u8,
283 pub color: Color,
284 pub bold: bool,
285 pub italic: bool,
286 pub field_kind: Option<FieldKind>,
287 pub field_source: Option<FieldSource>,
288 pub note: Option<crate::line::NoteRef>,
289}
290
291impl MultilingualGlyphRun {
292 pub fn is_valid(&self) -> bool {
294 let glyph_count = self.glyph_ids.len();
295 self.x_advances.len() == glyph_count
296 && self.y_advances.len() == glyph_count
297 && self.x_offsets.len() == glyph_count
298 && self.y_offsets.len() == glyph_count
299 && unicode_bidi::Level::new(self.bidi_level).is_ok()
300 && crate::font::position_values_are_finite(
301 &self.x_advances,
302 &self.y_advances,
303 &self.x_offsets,
304 &self.y_offsets,
305 self.x_advances.iter().sum(),
306 )
307 && crate::font::cluster_ranges_are_valid(
308 &self.clusters,
309 glyph_count,
310 self.logical_text.chars().count(),
311 self.bidi_level % 2 == 1,
312 )
313 }
314
315 pub fn legacy_projection(&self) -> GlyphRun {
318 GlyphRun {
319 origin: self.origin,
320 font_id: self.font_id,
321 font_size: self.font_size,
322 glyph_ids: self.glyph_ids.clone(),
323 advances: self.x_advances.clone(),
324 text: self.logical_text.clone(),
325 source: self.source,
326 color: self.color,
327 bold: self.bold,
328 italic: self.italic,
329 field_kind: self.field_kind,
330 field_source: self.field_source,
331 note: self.note,
332 tab_aligned: None,
333 }
334 }
335}
336
337#[derive(Debug, Clone, PartialEq)]
339#[non_exhaustive]
340pub enum PositionedElement {
341 Text(GlyphRun),
343 MultilingualText(MultilingualGlyphRun),
345 Line {
347 start: Point,
348 end: Point,
349 width: f64,
350 color: Color,
351 dash_pattern: Option<(f64, f64)>,
353 },
354 FilledRect { rect: Rect, color: Color },
356 Image {
358 rect: Rect,
359 data: Vec<u8>,
360 content_type: String,
361 media_id: MediaId,
362 },
363 LinkAnnotation { rect: Rect, url: String },
365 Path(PathElement),
367 Group(GroupElement),
369 MarkedContent {
374 structure: Option<StructureId>,
375 children: Vec<PositionedElement>,
376 },
377}
378
379#[derive(Debug, Clone, PartialEq)]
381pub struct PathElement {
382 pub path: Path,
383 pub fill: Option<Paint>,
384 pub stroke: Option<Stroke>,
385}
386
387#[derive(Debug, Clone, PartialEq)]
389pub struct Diagnostic {
390 pub message: String,
391}
392
393#[derive(Debug, Clone, PartialEq)]
395#[non_exhaustive]
396pub enum Effect {
397 OuterShadow {
398 dx: f64,
399 dy: f64,
400 blur: f64,
401 color: Color,
402 },
403}
404
405#[derive(Debug, Clone, PartialEq)]
407pub struct GroupElement {
408 pub transform: Transform,
410 pub clip: Option<Path>,
411 pub opacity: f64,
412 pub effects: Vec<Effect>,
413 pub children: Vec<PositionedElement>,
414}
415
416pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
418 fn visit(
419 elements: &[PositionedElement],
420 accumulated: Transform,
421 f: &mut dyn FnMut(&PositionedElement, &Transform),
422 ) {
423 for element in elements {
424 match element {
425 PositionedElement::Group(group) => {
426 let child_to_page = group.transform.then(accumulated);
427 visit(&group.children, child_to_page, f);
428 }
429 PositionedElement::MarkedContent { children, .. } => {
430 visit(children, accumulated, f);
431 }
432 leaf => f(leaf, &accumulated),
433 }
434 }
435 }
436
437 visit(elements, Transform::IDENTITY, f);
438}
439
440#[derive(Debug, Clone)]
442#[non_exhaustive]
443pub struct PageFrame {
444 pub page_number: usize,
446 pub displayed_page_number: usize,
448 pub width: f64,
450 pub height: f64,
452 pub elements: Vec<PositionedElement>,
454 pub background: Option<Paint>,
456}
457
458impl PageFrame {
459 pub fn new(
468 page_number: usize,
469 width: f64,
470 height: f64,
471 elements: Vec<PositionedElement>,
472 ) -> Self {
473 Self {
474 page_number,
475 displayed_page_number: page_number,
476 width,
477 height,
478 elements,
479 background: None,
480 }
481 }
482}
483
484#[derive(Debug, Clone)]
486pub struct FontData {
487 pub id: FontId,
489 pub family: String,
491 pub data: Arc<[u8]>,
493 pub face_index: u32,
495 pub bold: bool,
497 pub italic: bool,
499}
500
501#[derive(Debug, Clone, Default)]
503pub struct DocumentMetadata {
504 pub title: Option<String>,
506 pub author: Option<String>,
508 pub subject: Option<String>,
510 pub keywords: Option<String>,
512 pub creator: Option<String>,
514}
515
516#[derive(Debug, Clone)]
518pub struct OutlineEntry {
519 pub title: String,
521 pub level: u32,
523 pub page_index: usize,
525 pub y_position: f64,
527}
528
529#[derive(Debug, Clone)]
531#[non_exhaustive]
532pub struct LayoutResult {
533 pub pages: Vec<Arc<PageFrame>>,
535 pub fonts: Vec<FontData>,
537 pub metadata: Option<DocumentMetadata>,
539 pub outlines: Vec<OutlineEntry>,
541 pub diagnostics: Vec<Diagnostic>,
543 pub structure: Option<DocumentStructure>,
546}
547
548impl LayoutResult {
549 pub fn new(
558 pages: Vec<Arc<PageFrame>>,
559 fonts: Vec<FontData>,
560 metadata: Option<DocumentMetadata>,
561 outlines: Vec<OutlineEntry>,
562 ) -> Self {
563 Self {
564 pages,
565 fonts,
566 metadata,
567 outlines,
568 diagnostics: Vec::new(),
569 structure: None,
570 }
571 }
572}
573
574#[cfg(test)]
575mod media_id_tests {
576 use std::collections::HashSet;
577
578 use super::{MediaId, PositionedElement, Rect};
579
580 #[test]
581 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
582 let ids = HashSet::from([
583 MediaId::from_bytes(b"same image"),
584 MediaId::from_bytes(b"same image"),
585 ]);
586 assert_eq!(ids.len(), 1);
587 }
588
589 #[test]
590 fn media_id_depends_on_bytes_not_relationship_context() {
591 assert_eq!(
592 MediaId::from_bytes(b"image bytes"),
593 MediaId::from_bytes(b"image bytes")
594 );
595 }
596
597 #[test]
598 fn different_image_bytes_have_different_fixture_ids() {
599 assert_ne!(
600 MediaId::from_bytes(b"first image"),
601 MediaId::from_bytes(b"second image")
602 );
603 }
604
605 #[test]
606 fn staged_output_image_uses_media_id_instead_of_embed_id() {
607 let media_id = MediaId::from_bytes(b"image bytes");
608 let image = PositionedElement::Image {
609 rect: Rect {
610 x: 0.0,
611 y: 0.0,
612 width: 10.0,
613 height: 20.0,
614 },
615 data: b"image bytes".to_vec(),
616 content_type: "image/png".to_owned(),
617 media_id,
618 };
619 let PositionedElement::Image {
620 media_id: actual, ..
621 } = image
622 else {
623 panic!("constructed image should remain an image");
624 };
625 assert_eq!(actual, media_id);
626 }
627}
628
629#[cfg(test)]
630mod tagged_pdf_start_feature_tests {
631 use super::{
632 Color, DocumentStructure, PositionedElement, Rect, StructureId, StructureNode,
633 StructureRole, walk,
634 };
635 use crate::Transform;
636
637 #[test]
638 fn marked_content_is_backend_neutral_and_non_drawing() {
639 let root = StructureId::new(1).expect("non-zero root");
640 let paragraph = StructureId::new(2).expect("non-zero paragraph");
641 let structure = DocumentStructure {
642 root,
643 nodes: vec![
644 StructureNode {
645 id: root,
646 role: StructureRole::Document,
647 children: vec![paragraph],
648 alternate_text: None,
649 },
650 StructureNode {
651 id: paragraph,
652 role: StructureRole::Paragraph,
653 children: Vec::new(),
654 alternate_text: None,
655 },
656 ],
657 };
658 assert_eq!(
659 structure.node(paragraph).map(|node| node.role),
660 Some(StructureRole::Paragraph)
661 );
662
663 let elements = vec![PositionedElement::MarkedContent {
664 structure: Some(paragraph),
665 children: vec![PositionedElement::FilledRect {
666 rect: Rect {
667 x: 1.0,
668 y: 2.0,
669 width: 3.0,
670 height: 4.0,
671 },
672 color: Color::BLACK,
673 }],
674 }];
675 let mut leaves = 0;
676 walk(&elements, &mut |element, transform| {
677 assert!(matches!(element, PositionedElement::FilledRect { .. }));
678 assert_eq!(*transform, Transform::IDENTITY);
679 leaves += 1;
680 });
681 assert_eq!(leaves, 1);
682 }
683}
684
685#[cfg(test)]
686mod group_output_tests {
687 use std::sync::Arc;
688
689 use super::{
690 Color, Diagnostic, Effect, FontData, FontId, GroupElement, LayoutResult, PageFrame,
691 PathElement, PositionedElement, Rect,
692 };
693 use crate::{FillRule, Paint, Path, Stroke, Transform};
694
695 #[test]
696 fn path_and_group_arms_preserve_their_payloads() {
697 let path = Path::rect(Rect {
698 x: 1.0,
699 y: 2.0,
700 width: 3.0,
701 height: 4.0,
702 });
703 let path_element = PathElement {
704 path: path.clone(),
705 fill: Some(Paint::Solid(Color::BLACK)),
706 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
707 };
708 let element = PositionedElement::Path(path_element.clone());
709 assert!(matches!(
710 element,
711 PositionedElement::Path(actual) if actual == path_element
712 ));
713
714 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
715 let clip = Path {
716 commands: Vec::new(),
717 fill_rule: FillRule::EvenOdd,
718 };
719 let effect = Effect::OuterShadow {
720 dx: 1.0,
721 dy: 2.0,
722 blur: 3.0,
723 color: Color::BLACK,
724 };
725 let child_rect = Rect {
726 x: 5.0,
727 y: 6.0,
728 width: 7.0,
729 height: 8.0,
730 };
731 let group = GroupElement {
732 transform,
733 clip: Some(clip.clone()),
734 opacity: 0.5,
735 effects: vec![effect.clone()],
736 children: vec![PositionedElement::FilledRect {
737 rect: child_rect,
738 color: Color::WHITE,
739 }],
740 };
741 let element = PositionedElement::Group(group);
742 let PositionedElement::Group(actual) = element else {
743 panic!("constructed group should remain a group");
744 };
745 assert_eq!(actual.transform, transform);
746 assert_eq!(actual.clip, Some(clip));
747 assert_eq!(actual.opacity, 0.5);
748 assert_eq!(actual.effects, vec![effect]);
749 assert!(matches!(
750 actual.children.as_slice(),
751 [PositionedElement::FilledRect { rect, color }]
752 if *rect == child_rect && *color == Color::WHITE
753 ));
754 }
755
756 #[test]
757 fn page_frame_new_defaults_background_to_none() {
758 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
759 assert_eq!(page.page_number, 1);
760 assert_eq!(page.background, None);
761 }
762
763 #[test]
764 fn layout_result_new_defaults_diagnostics_to_empty() {
765 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
766 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
767 }
768
769 #[test]
770 fn cloned_layout_shares_font_bytes_and_page_frames() {
771 let page = Arc::new(PageFrame::new(1, 612.0, 792.0, Vec::new()));
772 let font_bytes: Arc<[u8]> = Arc::from([1, 2, 3, 4]);
773 let result = LayoutResult::new(
774 vec![Arc::clone(&page)],
775 vec![FontData {
776 id: FontId(1),
777 family: "Shared".to_owned(),
778 data: Arc::clone(&font_bytes),
779 face_index: 0,
780 bold: false,
781 italic: false,
782 }],
783 None,
784 Vec::new(),
785 );
786
787 let cloned = result.clone();
788 assert!(Arc::ptr_eq(&result.pages[0], &cloned.pages[0]));
789 assert!(Arc::ptr_eq(&result.fonts[0].data, &cloned.fonts[0].data));
790 assert_eq!(result.pages[0].page_number, cloned.pages[0].page_number);
791 assert_eq!(result.fonts[0].data.as_ref(), cloned.fonts[0].data.as_ref());
792 }
793
794 #[test]
795 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
796 let child_to_parent = Transform {
797 a: 1.0,
798 b: 0.0,
799 c: 0.0,
800 d: 1.0,
801 e: 10.0,
802 f: 20.0,
803 };
804 let group = GroupElement {
805 transform: child_to_parent,
806 clip: None,
807 opacity: 1.0,
808 effects: Vec::new(),
809 children: Vec::new(),
810 };
811 assert_eq!(
812 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
813 super::Point { x: 11.0, y: 22.0 }
814 );
815 }
816}
817
818#[cfg(test)]
819mod walk_tests {
820 use super::{Color, GroupElement, PositionedElement, Rect, walk};
821 use crate::{Point, Transform};
822
823 fn translate(x: f64, y: f64) -> Transform {
824 Transform {
825 e: x,
826 f: y,
827 ..Transform::IDENTITY
828 }
829 }
830
831 fn scale(value: f64) -> Transform {
832 Transform {
833 a: value,
834 d: value,
835 ..Transform::IDENTITY
836 }
837 }
838
839 fn leaf(id: f64) -> PositionedElement {
840 PositionedElement::FilledRect {
841 rect: Rect {
842 x: id,
843 y: 0.0,
844 width: 1.0,
845 height: 1.0,
846 },
847 color: Color::BLACK,
848 }
849 }
850
851 #[test]
852 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
853 let elements = vec![
854 leaf(1.0),
855 PositionedElement::Group(GroupElement {
856 transform: translate(10.0, 0.0),
857 clip: None,
858 opacity: 1.0,
859 effects: Vec::new(),
860 children: vec![PositionedElement::Group(GroupElement {
861 transform: scale(2.0),
862 clip: None,
863 opacity: 1.0,
864 effects: Vec::new(),
865 children: vec![PositionedElement::Group(GroupElement {
866 transform: translate(0.0, 5.0),
867 clip: None,
868 opacity: 1.0,
869 effects: Vec::new(),
870 children: vec![leaf(2.0)],
871 })],
872 })],
873 }),
874 leaf(3.0),
875 ];
876 let mut visited = Vec::new();
877 walk(&elements, &mut |element, transform| {
878 let PositionedElement::FilledRect { rect, .. } = element else {
879 panic!("walk should yield leaves only");
880 };
881 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
882 });
883 assert_eq!(
884 visited,
885 vec![
886 (1.0, Point { x: 1.0, y: 1.0 }),
887 (2.0, Point { x: 12.0, y: 12.0 }),
888 (3.0, Point { x: 1.0, y: 1.0 }),
889 ]
890 );
891 }
892
893 #[test]
894 fn nested_group_transform_order_applies_child_before_parent() {
895 let group = PositionedElement::Group(GroupElement {
896 transform: translate(10.0, 0.0),
897 clip: None,
898 opacity: 1.0,
899 effects: Vec::new(),
900 children: vec![PositionedElement::Group(GroupElement {
901 transform: scale(2.0),
902 clip: None,
903 opacity: 1.0,
904 effects: Vec::new(),
905 children: vec![leaf(1.0)],
906 })],
907 });
908 let mut points = Vec::new();
909 walk(&[group], &mut |_, transform| {
910 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
911 });
912 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
913 }
914
915 #[test]
916 fn walk_does_not_yield_group_nodes() {
917 let group = PositionedElement::Group(GroupElement {
918 transform: Transform::IDENTITY,
919 clip: None,
920 opacity: 1.0,
921 effects: Vec::new(),
922 children: vec![leaf(1.0)],
923 });
924 walk(&[group], &mut |element, _| {
925 assert!(!matches!(element, PositionedElement::Group(_)));
926 });
927 }
928
929 #[test]
930 fn walk_passes_identity_for_root_leaves() {
931 walk(&[leaf(1.0)], &mut |_, transform| {
932 assert_eq!(*transform, Transform::IDENTITY);
933 });
934 }
935}