1use crate::paint::{Paint, Stroke};
4use crate::path::Path;
5use crate::transform::Transform;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Point {
10 pub x: f64,
11 pub y: f64,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Rect {
17 pub x: f64,
18 pub y: f64,
19 pub width: f64,
20 pub height: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Color {
26 pub r: f64,
27 pub g: f64,
28 pub b: f64,
29 pub a: f64,
30}
31
32impl Color {
33 pub const BLACK: Color = Color {
34 r: 0.0,
35 g: 0.0,
36 b: 0.0,
37 a: 1.0,
38 };
39 pub const WHITE: Color = Color {
40 r: 1.0,
41 g: 1.0,
42 b: 1.0,
43 a: 1.0,
44 };
45
46 pub fn from_hex(hex: &str) -> Self {
48 let hex = hex.trim_start_matches('#');
49 if hex.len() >= 6 {
50 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
51 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
52 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
53 Color { r, g, b, a: 1.0 }
54 } else {
55 Color::BLACK
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct FontId(pub u32);
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct MediaId(pub u64);
69
70impl MediaId {
71 pub fn from_bytes(bytes: &[u8]) -> Self {
73 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
74 for byte in bytes {
75 hash ^= u64::from(*byte);
76 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
77 }
78 Self(hash)
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum FieldKind {
85 Page,
87 NumPages,
89}
90
91#[derive(Debug, Clone, PartialEq)]
93pub struct GlyphRun {
94 pub origin: Point,
96 pub font_id: FontId,
98 pub font_size: f64,
100 pub glyph_ids: Vec<u16>,
102 pub advances: Vec<f64>,
104 pub text: String,
106 pub color: Color,
108 pub bold: bool,
110 pub italic: bool,
112 pub field_kind: Option<FieldKind>,
114 pub footnote_id: Option<i32>,
116}
117
118#[derive(Debug, Clone, PartialEq)]
120#[non_exhaustive]
121pub enum PositionedElement {
122 Text(GlyphRun),
124 Line {
126 start: Point,
127 end: Point,
128 width: f64,
129 color: Color,
130 dash_pattern: Option<(f64, f64)>,
132 },
133 FilledRect { rect: Rect, color: Color },
135 Image {
137 rect: Rect,
138 data: Vec<u8>,
139 content_type: String,
140 media_id: MediaId,
141 },
142 LinkAnnotation { rect: Rect, url: String },
144 Path(PathElement),
146 Group(GroupElement),
148}
149
150#[derive(Debug, Clone, PartialEq)]
152pub struct PathElement {
153 pub path: Path,
154 pub fill: Option<Paint>,
155 pub stroke: Option<Stroke>,
156}
157
158#[derive(Debug, Clone, PartialEq)]
160pub struct Diagnostic {
161 pub message: String,
162}
163
164#[derive(Debug, Clone, PartialEq)]
166#[non_exhaustive]
167pub enum Effect {
168 OuterShadow {
169 dx: f64,
170 dy: f64,
171 blur: f64,
172 color: Color,
173 },
174}
175
176#[derive(Debug, Clone, PartialEq)]
178pub struct GroupElement {
179 pub transform: Transform,
181 pub clip: Option<Path>,
182 pub opacity: f64,
183 pub effects: Vec<Effect>,
184 pub children: Vec<PositionedElement>,
185}
186
187pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
189 fn visit(
190 elements: &[PositionedElement],
191 accumulated: Transform,
192 f: &mut dyn FnMut(&PositionedElement, &Transform),
193 ) {
194 for element in elements {
195 match element {
196 PositionedElement::Group(group) => {
197 let child_to_page = group.transform.then(accumulated);
198 visit(&group.children, child_to_page, f);
199 }
200 leaf => f(leaf, &accumulated),
201 }
202 }
203 }
204
205 visit(elements, Transform::IDENTITY, f);
206}
207
208#[derive(Debug, Clone)]
210#[non_exhaustive]
211pub struct PageFrame {
212 pub page_number: usize,
214 pub width: f64,
216 pub height: f64,
218 pub elements: Vec<PositionedElement>,
220 pub background: Option<Paint>,
222}
223
224impl PageFrame {
225 pub fn new(
234 page_number: usize,
235 width: f64,
236 height: f64,
237 elements: Vec<PositionedElement>,
238 ) -> Self {
239 Self {
240 page_number,
241 width,
242 height,
243 elements,
244 background: None,
245 }
246 }
247}
248
249#[derive(Debug, Clone)]
251pub struct FontData {
252 pub id: FontId,
254 pub family: String,
256 pub data: Vec<u8>,
258 pub face_index: u32,
260 pub bold: bool,
262 pub italic: bool,
264}
265
266#[derive(Debug, Clone, Default)]
268pub struct DocumentMetadata {
269 pub title: Option<String>,
271 pub author: Option<String>,
273 pub subject: Option<String>,
275 pub keywords: Option<String>,
277 pub creator: Option<String>,
279}
280
281#[derive(Debug, Clone)]
283pub struct OutlineEntry {
284 pub title: String,
286 pub level: u32,
288 pub page_index: usize,
290 pub y_position: f64,
292}
293
294#[derive(Debug, Clone)]
296#[non_exhaustive]
297pub struct LayoutResult {
298 pub pages: Vec<PageFrame>,
300 pub fonts: Vec<FontData>,
302 pub metadata: Option<DocumentMetadata>,
304 pub outlines: Vec<OutlineEntry>,
306 pub diagnostics: Vec<Diagnostic>,
308}
309
310impl LayoutResult {
311 pub fn new(
320 pages: Vec<PageFrame>,
321 fonts: Vec<FontData>,
322 metadata: Option<DocumentMetadata>,
323 outlines: Vec<OutlineEntry>,
324 ) -> Self {
325 Self {
326 pages,
327 fonts,
328 metadata,
329 outlines,
330 diagnostics: Vec::new(),
331 }
332 }
333}
334
335#[cfg(test)]
336mod media_id_tests {
337 use std::collections::HashSet;
338
339 use super::{MediaId, PositionedElement, Rect};
340
341 #[test]
342 fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
343 let ids = HashSet::from([
344 MediaId::from_bytes(b"same image"),
345 MediaId::from_bytes(b"same image"),
346 ]);
347 assert_eq!(ids.len(), 1);
348 }
349
350 #[test]
351 fn media_id_depends_on_bytes_not_relationship_context() {
352 assert_eq!(
353 MediaId::from_bytes(b"image bytes"),
354 MediaId::from_bytes(b"image bytes")
355 );
356 }
357
358 #[test]
359 fn different_image_bytes_have_different_fixture_ids() {
360 assert_ne!(
361 MediaId::from_bytes(b"first image"),
362 MediaId::from_bytes(b"second image")
363 );
364 }
365
366 #[test]
367 fn staged_output_image_uses_media_id_instead_of_embed_id() {
368 let media_id = MediaId::from_bytes(b"image bytes");
369 let image = PositionedElement::Image {
370 rect: Rect {
371 x: 0.0,
372 y: 0.0,
373 width: 10.0,
374 height: 20.0,
375 },
376 data: b"image bytes".to_vec(),
377 content_type: "image/png".to_owned(),
378 media_id,
379 };
380 let PositionedElement::Image {
381 media_id: actual, ..
382 } = image
383 else {
384 panic!("constructed image should remain an image");
385 };
386 assert_eq!(actual, media_id);
387 }
388}
389
390#[cfg(test)]
391mod group_output_tests {
392 use super::{
393 Color, Diagnostic, Effect, GroupElement, LayoutResult, PageFrame, PathElement,
394 PositionedElement, Rect,
395 };
396 use crate::{FillRule, Paint, Path, Stroke, Transform};
397
398 #[test]
399 fn path_and_group_arms_preserve_their_payloads() {
400 let path = Path::rect(Rect {
401 x: 1.0,
402 y: 2.0,
403 width: 3.0,
404 height: 4.0,
405 });
406 let path_element = PathElement {
407 path: path.clone(),
408 fill: Some(Paint::Solid(Color::BLACK)),
409 stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
410 };
411 let element = PositionedElement::Path(path_element.clone());
412 assert!(matches!(
413 element,
414 PositionedElement::Path(actual) if actual == path_element
415 ));
416
417 let transform = Transform::rotate_about(15.0, 2.0, 3.0);
418 let clip = Path {
419 commands: Vec::new(),
420 fill_rule: FillRule::EvenOdd,
421 };
422 let effect = Effect::OuterShadow {
423 dx: 1.0,
424 dy: 2.0,
425 blur: 3.0,
426 color: Color::BLACK,
427 };
428 let child_rect = Rect {
429 x: 5.0,
430 y: 6.0,
431 width: 7.0,
432 height: 8.0,
433 };
434 let group = GroupElement {
435 transform,
436 clip: Some(clip.clone()),
437 opacity: 0.5,
438 effects: vec![effect.clone()],
439 children: vec![PositionedElement::FilledRect {
440 rect: child_rect,
441 color: Color::WHITE,
442 }],
443 };
444 let element = PositionedElement::Group(group);
445 let PositionedElement::Group(actual) = element else {
446 panic!("constructed group should remain a group");
447 };
448 assert_eq!(actual.transform, transform);
449 assert_eq!(actual.clip, Some(clip));
450 assert_eq!(actual.opacity, 0.5);
451 assert_eq!(actual.effects, vec![effect]);
452 assert!(matches!(
453 actual.children.as_slice(),
454 [PositionedElement::FilledRect { rect, color }]
455 if *rect == child_rect && *color == Color::WHITE
456 ));
457 }
458
459 #[test]
460 fn page_frame_new_defaults_background_to_none() {
461 let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
462 assert_eq!(page.page_number, 1);
463 assert_eq!(page.background, None);
464 }
465
466 #[test]
467 fn layout_result_new_defaults_diagnostics_to_empty() {
468 let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
469 assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
470 }
471
472 #[test]
473 fn group_transform_maps_child_coordinates_into_parent_coordinates() {
474 let child_to_parent = Transform {
475 a: 1.0,
476 b: 0.0,
477 c: 0.0,
478 d: 1.0,
479 e: 10.0,
480 f: 20.0,
481 };
482 let group = GroupElement {
483 transform: child_to_parent,
484 clip: None,
485 opacity: 1.0,
486 effects: Vec::new(),
487 children: Vec::new(),
488 };
489 assert_eq!(
490 group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
491 super::Point { x: 11.0, y: 22.0 }
492 );
493 }
494}
495
496#[cfg(test)]
497mod walk_tests {
498 use super::{Color, GroupElement, PositionedElement, Rect, walk};
499 use crate::{Point, Transform};
500
501 fn translate(x: f64, y: f64) -> Transform {
502 Transform {
503 e: x,
504 f: y,
505 ..Transform::IDENTITY
506 }
507 }
508
509 fn scale(value: f64) -> Transform {
510 Transform {
511 a: value,
512 d: value,
513 ..Transform::IDENTITY
514 }
515 }
516
517 fn leaf(id: f64) -> PositionedElement {
518 PositionedElement::FilledRect {
519 rect: Rect {
520 x: id,
521 y: 0.0,
522 width: 1.0,
523 height: 1.0,
524 },
525 color: Color::BLACK,
526 }
527 }
528
529 #[test]
530 fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
531 let elements = vec![
532 leaf(1.0),
533 PositionedElement::Group(GroupElement {
534 transform: translate(10.0, 0.0),
535 clip: None,
536 opacity: 1.0,
537 effects: Vec::new(),
538 children: vec![PositionedElement::Group(GroupElement {
539 transform: scale(2.0),
540 clip: None,
541 opacity: 1.0,
542 effects: Vec::new(),
543 children: vec![PositionedElement::Group(GroupElement {
544 transform: translate(0.0, 5.0),
545 clip: None,
546 opacity: 1.0,
547 effects: Vec::new(),
548 children: vec![leaf(2.0)],
549 })],
550 })],
551 }),
552 leaf(3.0),
553 ];
554 let mut visited = Vec::new();
555 walk(&elements, &mut |element, transform| {
556 let PositionedElement::FilledRect { rect, .. } = element else {
557 panic!("walk should yield leaves only");
558 };
559 visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
560 });
561 assert_eq!(
562 visited,
563 vec![
564 (1.0, Point { x: 1.0, y: 1.0 }),
565 (2.0, Point { x: 12.0, y: 12.0 }),
566 (3.0, Point { x: 1.0, y: 1.0 }),
567 ]
568 );
569 }
570
571 #[test]
572 fn nested_group_transform_order_applies_child_before_parent() {
573 let group = PositionedElement::Group(GroupElement {
574 transform: translate(10.0, 0.0),
575 clip: None,
576 opacity: 1.0,
577 effects: Vec::new(),
578 children: vec![PositionedElement::Group(GroupElement {
579 transform: scale(2.0),
580 clip: None,
581 opacity: 1.0,
582 effects: Vec::new(),
583 children: vec![leaf(1.0)],
584 })],
585 });
586 let mut points = Vec::new();
587 walk(&[group], &mut |_, transform| {
588 points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
589 });
590 assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
591 }
592
593 #[test]
594 fn walk_does_not_yield_group_nodes() {
595 let group = PositionedElement::Group(GroupElement {
596 transform: Transform::IDENTITY,
597 clip: None,
598 opacity: 1.0,
599 effects: Vec::new(),
600 children: vec![leaf(1.0)],
601 });
602 walk(&[group], &mut |element, _| {
603 assert!(!matches!(element, PositionedElement::Group(_)));
604 });
605 }
606
607 #[test]
608 fn walk_passes_identity_for_root_leaves() {
609 walk(&[leaf(1.0)], &mut |_, transform| {
610 assert_eq!(*transform, Transform::IDENTITY);
611 });
612 }
613}