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