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