1use std::num::NonZeroU32;
4
5use crate::paint::{Paint, Stroke};
6use crate::path::Path;
7use crate::transform::Transform;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Point {
12 pub x: f64,
13 pub y: f64,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Rect {
19 pub x: f64,
20 pub y: f64,
21 pub width: f64,
22 pub height: f64,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Color {
28 pub r: f64,
29 pub g: f64,
30 pub b: f64,
31 pub a: f64,
32}
33
34impl Color {
35 pub const BLACK: Color = Color {
36 r: 0.0,
37 g: 0.0,
38 b: 0.0,
39 a: 1.0,
40 };
41 pub const WHITE: Color = Color {
42 r: 1.0,
43 g: 1.0,
44 b: 1.0,
45 a: 1.0,
46 };
47
48 pub fn from_hex(hex: &str) -> Self {
50 let hex = hex.trim_start_matches('#');
51 if hex.len() >= 6 {
52 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
53 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
54 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
55 Color { r, g, b, a: 1.0 }
56 } else {
57 Color::BLACK
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct FontId(pub u32);
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub struct MediaId(pub u64);
75
76impl MediaId {
77 pub fn from_bytes(bytes: &[u8]) -> Self {
79 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
80 for byte in bytes {
81 hash ^= u64::from(*byte);
82 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
83 }
84 Self(hash)
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub struct SourceNodeId(NonZeroU32);
91
92impl SourceNodeId {
93 pub const fn new(value: u32) -> Option<Self> {
95 match NonZeroU32::new(value) {
96 Some(value) => Some(Self(value)),
97 None => None,
98 }
99 }
100
101 pub const fn get(self) -> u32 {
103 self.0.get()
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct SourceSpan {
110 pub node: SourceNodeId,
111 pub char_start: u32,
112 pub char_end: u32,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum FieldKind {
127 Page,
129 NumPages,
131 TargetPage(usize),
133 Target(usize),
135}
136
137#[derive(Debug, Clone, PartialEq)]
139pub struct GlyphRun {
140 pub origin: Point,
142 pub font_id: FontId,
144 pub font_size: f64,
146 pub glyph_ids: Vec<u16>,
148 pub advances: Vec<f64>,
150 pub text: String,
152 pub source: Option<SourceSpan>,
154 pub color: Color,
156 pub bold: bool,
158 pub italic: bool,
160 pub field_kind: Option<FieldKind>,
162 pub note: Option<crate::line::NoteRef>,
164}
165
166#[derive(Debug, Clone, PartialEq)]
168#[non_exhaustive]
169pub enum PositionedElement {
170 Text(GlyphRun),
172 Line {
174 start: Point,
175 end: Point,
176 width: f64,
177 color: Color,
178 dash_pattern: Option<(f64, f64)>,
180 },
181 FilledRect { rect: Rect, color: Color },
183 Image {
185 rect: Rect,
186 data: Vec<u8>,
187 content_type: String,
188 media_id: MediaId,
189 },
190 LinkAnnotation { rect: Rect, url: String },
192 Path(PathElement),
194 Group(GroupElement),
196}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct PathElement {
201 pub path: Path,
202 pub fill: Option<Paint>,
203 pub stroke: Option<Stroke>,
204}
205
206#[derive(Debug, Clone, PartialEq)]
208pub struct Diagnostic {
209 pub message: String,
210}
211
212#[derive(Debug, Clone, PartialEq)]
214#[non_exhaustive]
215pub enum Effect {
216 OuterShadow {
217 dx: f64,
218 dy: f64,
219 blur: f64,
220 color: Color,
221 },
222}
223
224#[derive(Debug, Clone, PartialEq)]
226pub struct GroupElement {
227 pub transform: Transform,
229 pub clip: Option<Path>,
230 pub opacity: f64,
231 pub effects: Vec<Effect>,
232 pub children: Vec<PositionedElement>,
233}
234
235pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
237 fn visit(
238 elements: &[PositionedElement],
239 accumulated: Transform,
240 f: &mut dyn FnMut(&PositionedElement, &Transform),
241 ) {
242 for element in elements {
243 match element {
244 PositionedElement::Group(group) => {
245 let child_to_page = group.transform.then(accumulated);
246 visit(&group.children, child_to_page, f);
247 }
248 leaf => f(leaf, &accumulated),
249 }
250 }
251 }
252
253 visit(elements, Transform::IDENTITY, f);
254}
255
256#[derive(Debug, Clone)]
258#[non_exhaustive]
259pub struct PageFrame {
260 pub page_number: usize,
262 pub width: f64,
264 pub height: f64,
266 pub elements: Vec<PositionedElement>,
268 pub background: Option<Paint>,
270}
271
272impl PageFrame {
273 pub fn new(
282 page_number: usize,
283 width: f64,
284 height: f64,
285 elements: Vec<PositionedElement>,
286 ) -> Self {
287 Self {
288 page_number,
289 width,
290 height,
291 elements,
292 background: None,
293 }
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct FontData {
300 pub id: FontId,
302 pub family: String,
304 pub data: Vec<u8>,
306 pub face_index: u32,
308 pub bold: bool,
310 pub italic: bool,
312}
313
314#[derive(Debug, Clone, Default)]
316pub struct DocumentMetadata {
317 pub title: Option<String>,
319 pub author: Option<String>,
321 pub subject: Option<String>,
323 pub keywords: Option<String>,
325 pub creator: Option<String>,
327}
328
329#[derive(Debug, Clone)]
331pub struct OutlineEntry {
332 pub title: String,
334 pub level: u32,
336 pub page_index: usize,
338 pub y_position: f64,
340}
341
342#[derive(Debug, Clone)]
344#[non_exhaustive]
345pub struct LayoutResult {
346 pub pages: Vec<PageFrame>,
348 pub fonts: Vec<FontData>,
350 pub metadata: Option<DocumentMetadata>,
352 pub outlines: Vec<OutlineEntry>,
354 pub diagnostics: Vec<Diagnostic>,
356}
357
358impl LayoutResult {
359 pub fn new(
368 pages: Vec<PageFrame>,
369 fonts: Vec<FontData>,
370 metadata: Option<DocumentMetadata>,
371 outlines: Vec<OutlineEntry>,
372 ) -> Self {
373 Self {
374 pages,
375 fonts,
376 metadata,
377 outlines,
378 diagnostics: Vec::new(),
379 }
380 }
381}
382
383#[cfg(test)]
384mod media_id_tests {
385 use std::collections::HashSet;
386
387 use super::{MediaId, PositionedElement, Rect};
388
389 #[test]
390 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
391 let ids = HashSet::from([
392 MediaId::from_bytes(b"same image"),
393 MediaId::from_bytes(b"same image"),
394 ]);
395 assert_eq!(ids.len(), 1);
396 }
397
398 #[test]
399 fn media_id_depends_on_bytes_not_relationship_context() {
400 assert_eq!(
401 MediaId::from_bytes(b"image bytes"),
402 MediaId::from_bytes(b"image bytes")
403 );
404 }
405
406 #[test]
407 fn different_image_bytes_have_different_fixture_ids() {
408 assert_ne!(
409 MediaId::from_bytes(b"first image"),
410 MediaId::from_bytes(b"second image")
411 );
412 }
413
414 #[test]
415 fn staged_output_image_uses_media_id_instead_of_embed_id() {
416 let media_id = MediaId::from_bytes(b"image bytes");
417 let image = PositionedElement::Image {
418 rect: Rect {
419 x: 0.0,
420 y: 0.0,
421 width: 10.0,
422 height: 20.0,
423 },
424 data: b"image bytes".to_vec(),
425 content_type: "image/png".to_owned(),
426 media_id,
427 };
428 let PositionedElement::Image {
429 media_id: actual, ..
430 } = image
431 else {
432 panic!("constructed image should remain an image");
433 };
434 assert_eq!(actual, media_id);
435 }
436}
437
438#[cfg(test)]
439mod group_output_tests {
440 use super::{
441 Color, Diagnostic, Effect, GroupElement, LayoutResult, PageFrame, PathElement,
442 PositionedElement, Rect,
443 };
444 use crate::{FillRule, Paint, Path, Stroke, Transform};
445
446 #[test]
447 fn path_and_group_arms_preserve_their_payloads() {
448 let path = Path::rect(Rect {
449 x: 1.0,
450 y: 2.0,
451 width: 3.0,
452 height: 4.0,
453 });
454 let path_element = PathElement {
455 path: path.clone(),
456 fill: Some(Paint::Solid(Color::BLACK)),
457 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
458 };
459 let element = PositionedElement::Path(path_element.clone());
460 assert!(matches!(
461 element,
462 PositionedElement::Path(actual) if actual == path_element
463 ));
464
465 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
466 let clip = Path {
467 commands: Vec::new(),
468 fill_rule: FillRule::EvenOdd,
469 };
470 let effect = Effect::OuterShadow {
471 dx: 1.0,
472 dy: 2.0,
473 blur: 3.0,
474 color: Color::BLACK,
475 };
476 let child_rect = Rect {
477 x: 5.0,
478 y: 6.0,
479 width: 7.0,
480 height: 8.0,
481 };
482 let group = GroupElement {
483 transform,
484 clip: Some(clip.clone()),
485 opacity: 0.5,
486 effects: vec![effect.clone()],
487 children: vec![PositionedElement::FilledRect {
488 rect: child_rect,
489 color: Color::WHITE,
490 }],
491 };
492 let element = PositionedElement::Group(group);
493 let PositionedElement::Group(actual) = element else {
494 panic!("constructed group should remain a group");
495 };
496 assert_eq!(actual.transform, transform);
497 assert_eq!(actual.clip, Some(clip));
498 assert_eq!(actual.opacity, 0.5);
499 assert_eq!(actual.effects, vec![effect]);
500 assert!(matches!(
501 actual.children.as_slice(),
502 [PositionedElement::FilledRect { rect, color }]
503 if *rect == child_rect && *color == Color::WHITE
504 ));
505 }
506
507 #[test]
508 fn page_frame_new_defaults_background_to_none() {
509 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
510 assert_eq!(page.page_number, 1);
511 assert_eq!(page.background, None);
512 }
513
514 #[test]
515 fn layout_result_new_defaults_diagnostics_to_empty() {
516 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
517 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
518 }
519
520 #[test]
521 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
522 let child_to_parent = Transform {
523 a: 1.0,
524 b: 0.0,
525 c: 0.0,
526 d: 1.0,
527 e: 10.0,
528 f: 20.0,
529 };
530 let group = GroupElement {
531 transform: child_to_parent,
532 clip: None,
533 opacity: 1.0,
534 effects: Vec::new(),
535 children: Vec::new(),
536 };
537 assert_eq!(
538 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
539 super::Point { x: 11.0, y: 22.0 }
540 );
541 }
542}
543
544#[cfg(test)]
545mod walk_tests {
546 use super::{Color, GroupElement, PositionedElement, Rect, walk};
547 use crate::{Point, Transform};
548
549 fn translate(x: f64, y: f64) -> Transform {
550 Transform {
551 e: x,
552 f: y,
553 ..Transform::IDENTITY
554 }
555 }
556
557 fn scale(value: f64) -> Transform {
558 Transform {
559 a: value,
560 d: value,
561 ..Transform::IDENTITY
562 }
563 }
564
565 fn leaf(id: f64) -> PositionedElement {
566 PositionedElement::FilledRect {
567 rect: Rect {
568 x: id,
569 y: 0.0,
570 width: 1.0,
571 height: 1.0,
572 },
573 color: Color::BLACK,
574 }
575 }
576
577 #[test]
578 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
579 let elements = vec![
580 leaf(1.0),
581 PositionedElement::Group(GroupElement {
582 transform: translate(10.0, 0.0),
583 clip: None,
584 opacity: 1.0,
585 effects: Vec::new(),
586 children: vec![PositionedElement::Group(GroupElement {
587 transform: scale(2.0),
588 clip: None,
589 opacity: 1.0,
590 effects: Vec::new(),
591 children: vec![PositionedElement::Group(GroupElement {
592 transform: translate(0.0, 5.0),
593 clip: None,
594 opacity: 1.0,
595 effects: Vec::new(),
596 children: vec![leaf(2.0)],
597 })],
598 })],
599 }),
600 leaf(3.0),
601 ];
602 let mut visited = Vec::new();
603 walk(&elements, &mut |element, transform| {
604 let PositionedElement::FilledRect { rect, .. } = element else {
605 panic!("walk should yield leaves only");
606 };
607 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
608 });
609 assert_eq!(
610 visited,
611 vec![
612 (1.0, Point { x: 1.0, y: 1.0 }),
613 (2.0, Point { x: 12.0, y: 12.0 }),
614 (3.0, Point { x: 1.0, y: 1.0 }),
615 ]
616 );
617 }
618
619 #[test]
620 fn nested_group_transform_order_applies_child_before_parent() {
621 let group = PositionedElement::Group(GroupElement {
622 transform: translate(10.0, 0.0),
623 clip: None,
624 opacity: 1.0,
625 effects: Vec::new(),
626 children: vec![PositionedElement::Group(GroupElement {
627 transform: scale(2.0),
628 clip: None,
629 opacity: 1.0,
630 effects: Vec::new(),
631 children: vec![leaf(1.0)],
632 })],
633 });
634 let mut points = Vec::new();
635 walk(&[group], &mut |_, transform| {
636 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
637 });
638 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
639 }
640
641 #[test]
642 fn walk_does_not_yield_group_nodes() {
643 let group = PositionedElement::Group(GroupElement {
644 transform: Transform::IDENTITY,
645 clip: None,
646 opacity: 1.0,
647 effects: Vec::new(),
648 children: vec![leaf(1.0)],
649 });
650 walk(&[group], &mut |element, _| {
651 assert!(!matches!(element, PositionedElement::Group(_)));
652 });
653 }
654
655 #[test]
656 fn walk_passes_identity_for_root_leaves() {
657 walk(&[leaf(1.0)], &mut |_, transform| {
658 assert_eq!(*transform, Transform::IDENTITY);
659 });
660 }
661}