use crate::paint::{Paint, Stroke};
use crate::path::Path;
use crate::transform::Transform;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f64,
pub g: f64,
pub b: f64,
pub a: f64,
}
impl Color {
pub const BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const WHITE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub fn from_hex(hex: &str) -> Self {
let hex = hex.trim_start_matches('#');
if hex.len() >= 6 {
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
Color { r, g, b, a: 1.0 }
} else {
Color::BLACK
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MediaId(pub u64);
impl MediaId {
pub fn from_bytes(bytes: &[u8]) -> Self {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
Self(hash)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Page,
NumPages,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GlyphRun {
pub origin: Point,
pub font_id: FontId,
pub font_size: f64,
pub glyph_ids: Vec<u16>,
pub advances: Vec<f64>,
pub text: String,
pub color: Color,
pub bold: bool,
pub italic: bool,
pub field_kind: Option<FieldKind>,
pub footnote_id: Option<i32>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PositionedElement {
Text(GlyphRun),
Line {
start: Point,
end: Point,
width: f64,
color: Color,
dash_pattern: Option<(f64, f64)>,
},
FilledRect { rect: Rect, color: Color },
Image {
rect: Rect,
data: Vec<u8>,
content_type: String,
media_id: MediaId,
},
LinkAnnotation { rect: Rect, url: String },
Path(PathElement),
Group(GroupElement),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathElement {
pub path: Path,
pub fill: Option<Paint>,
pub stroke: Option<Stroke>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostic {
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Effect {
OuterShadow {
dx: f64,
dy: f64,
blur: f64,
color: Color,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct GroupElement {
pub transform: Transform,
pub clip: Option<Path>,
pub opacity: f64,
pub effects: Vec<Effect>,
pub children: Vec<PositionedElement>,
}
pub fn walk(elements: &[PositionedElement], f: &mut impl FnMut(&PositionedElement, &Transform)) {
fn visit(
elements: &[PositionedElement],
accumulated: Transform,
f: &mut dyn FnMut(&PositionedElement, &Transform),
) {
for element in elements {
match element {
PositionedElement::Group(group) => {
let child_to_page = group.transform.then(accumulated);
visit(&group.children, child_to_page, f);
}
leaf => f(leaf, &accumulated),
}
}
}
visit(elements, Transform::IDENTITY, f);
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PageFrame {
pub page_number: usize,
pub width: f64,
pub height: f64,
pub elements: Vec<PositionedElement>,
pub background: Option<Paint>,
}
impl PageFrame {
pub fn new(
page_number: usize,
width: f64,
height: f64,
elements: Vec<PositionedElement>,
) -> Self {
Self {
page_number,
width,
height,
elements,
background: None,
}
}
}
#[derive(Debug, Clone)]
pub struct FontData {
pub id: FontId,
pub family: String,
pub data: Vec<u8>,
pub face_index: u32,
pub bold: bool,
pub italic: bool,
}
#[derive(Debug, Clone, Default)]
pub struct DocumentMetadata {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub creator: Option<String>,
}
#[derive(Debug, Clone)]
pub struct OutlineEntry {
pub title: String,
pub level: u32,
pub page_index: usize,
pub y_position: f64,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct LayoutResult {
pub pages: Vec<PageFrame>,
pub fonts: Vec<FontData>,
pub metadata: Option<DocumentMetadata>,
pub outlines: Vec<OutlineEntry>,
pub diagnostics: Vec<Diagnostic>,
}
impl LayoutResult {
pub fn new(
pages: Vec<PageFrame>,
fonts: Vec<FontData>,
metadata: Option<DocumentMetadata>,
outlines: Vec<OutlineEntry>,
) -> Self {
Self {
pages,
fonts,
metadata,
outlines,
diagnostics: Vec::new(),
}
}
}
#[cfg(test)]
mod media_id_tests {
use std::collections::HashSet;
use super::{MediaId, PositionedElement, Rect};
#[test]
fn the_same_image_bytes_inserted_twice_produce_one_media_id() {
let ids = HashSet::from([
MediaId::from_bytes(b"same image"),
MediaId::from_bytes(b"same image"),
]);
assert_eq!(ids.len(), 1);
}
#[test]
fn media_id_depends_on_bytes_not_relationship_context() {
assert_eq!(
MediaId::from_bytes(b"image bytes"),
MediaId::from_bytes(b"image bytes")
);
}
#[test]
fn different_image_bytes_have_different_fixture_ids() {
assert_ne!(
MediaId::from_bytes(b"first image"),
MediaId::from_bytes(b"second image")
);
}
#[test]
fn staged_output_image_uses_media_id_instead_of_embed_id() {
let media_id = MediaId::from_bytes(b"image bytes");
let image = PositionedElement::Image {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 20.0,
},
data: b"image bytes".to_vec(),
content_type: "image/png".to_owned(),
media_id,
};
let PositionedElement::Image {
media_id: actual, ..
} = image
else {
panic!("constructed image should remain an image");
};
assert_eq!(actual, media_id);
}
}
#[cfg(test)]
mod group_output_tests {
use super::{
Color, Diagnostic, Effect, GroupElement, LayoutResult, PageFrame, PathElement,
PositionedElement, Rect,
};
use crate::{FillRule, Paint, Path, Stroke, Transform};
#[test]
fn path_and_group_arms_preserve_their_payloads() {
let path = Path::rect(Rect {
x: 1.0,
y: 2.0,
width: 3.0,
height: 4.0,
});
let path_element = PathElement {
path: path.clone(),
fill: Some(Paint::Solid(Color::BLACK)),
stroke: Some(Stroke::new(Paint::Solid(Color::WHITE), 2.0)),
};
let element = PositionedElement::Path(path_element.clone());
assert!(matches!(
element,
PositionedElement::Path(actual) if actual == path_element
));
let transform = Transform::rotate_about(15.0, 2.0, 3.0);
let clip = Path {
commands: Vec::new(),
fill_rule: FillRule::EvenOdd,
};
let effect = Effect::OuterShadow {
dx: 1.0,
dy: 2.0,
blur: 3.0,
color: Color::BLACK,
};
let child_rect = Rect {
x: 5.0,
y: 6.0,
width: 7.0,
height: 8.0,
};
let group = GroupElement {
transform,
clip: Some(clip.clone()),
opacity: 0.5,
effects: vec![effect.clone()],
children: vec![PositionedElement::FilledRect {
rect: child_rect,
color: Color::WHITE,
}],
};
let element = PositionedElement::Group(group);
let PositionedElement::Group(actual) = element else {
panic!("constructed group should remain a group");
};
assert_eq!(actual.transform, transform);
assert_eq!(actual.clip, Some(clip));
assert_eq!(actual.opacity, 0.5);
assert_eq!(actual.effects, vec![effect]);
assert!(matches!(
actual.children.as_slice(),
[PositionedElement::FilledRect { rect, color }]
if *rect == child_rect && *color == Color::WHITE
));
}
#[test]
fn page_frame_new_defaults_background_to_none() {
let page = PageFrame::new(1, 612.0, 792.0, Vec::new());
assert_eq!(page.page_number, 1);
assert_eq!(page.background, None);
}
#[test]
fn layout_result_new_defaults_diagnostics_to_empty() {
let result = LayoutResult::new(Vec::new(), Vec::new(), None, Vec::new());
assert_eq!(result.diagnostics, Vec::<Diagnostic>::new());
}
#[test]
fn group_transform_maps_child_coordinates_into_parent_coordinates() {
let child_to_parent = Transform {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 10.0,
f: 20.0,
};
let group = GroupElement {
transform: child_to_parent,
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: Vec::new(),
};
assert_eq!(
group.transform.apply(super::Point { x: 1.0, y: 2.0 }),
super::Point { x: 11.0, y: 22.0 }
);
}
}
#[cfg(test)]
mod walk_tests {
use super::{Color, GroupElement, PositionedElement, Rect, walk};
use crate::{Point, Transform};
fn translate(x: f64, y: f64) -> Transform {
Transform {
e: x,
f: y,
..Transform::IDENTITY
}
}
fn scale(value: f64) -> Transform {
Transform {
a: value,
d: value,
..Transform::IDENTITY
}
}
fn leaf(id: f64) -> PositionedElement {
PositionedElement::FilledRect {
rect: Rect {
x: id,
y: 0.0,
width: 1.0,
height: 1.0,
},
color: Color::BLACK,
}
}
#[test]
fn three_deep_groups_yield_every_leaf_once_with_the_correct_accumulated_transform() {
let elements = vec![
leaf(1.0),
PositionedElement::Group(GroupElement {
transform: translate(10.0, 0.0),
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![PositionedElement::Group(GroupElement {
transform: scale(2.0),
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![PositionedElement::Group(GroupElement {
transform: translate(0.0, 5.0),
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![leaf(2.0)],
})],
})],
}),
leaf(3.0),
];
let mut visited = Vec::new();
walk(&elements, &mut |element, transform| {
let PositionedElement::FilledRect { rect, .. } = element else {
panic!("walk should yield leaves only");
};
visited.push((rect.x, transform.apply(Point { x: 1.0, y: 1.0 })));
});
assert_eq!(
visited,
vec![
(1.0, Point { x: 1.0, y: 1.0 }),
(2.0, Point { x: 12.0, y: 12.0 }),
(3.0, Point { x: 1.0, y: 1.0 }),
]
);
}
#[test]
fn nested_group_transform_order_applies_child_before_parent() {
let group = PositionedElement::Group(GroupElement {
transform: translate(10.0, 0.0),
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![PositionedElement::Group(GroupElement {
transform: scale(2.0),
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![leaf(1.0)],
})],
});
let mut points = Vec::new();
walk(&[group], &mut |_, transform| {
points.push(transform.apply(Point { x: 1.0, y: 1.0 }));
});
assert_eq!(points, vec![Point { x: 12.0, y: 2.0 }]);
}
#[test]
fn walk_does_not_yield_group_nodes() {
let group = PositionedElement::Group(GroupElement {
transform: Transform::IDENTITY,
clip: None,
opacity: 1.0,
effects: Vec::new(),
children: vec![leaf(1.0)],
});
walk(&[group], &mut |element, _| {
assert!(!matches!(element, PositionedElement::Group(_)));
});
}
#[test]
fn walk_passes_identity_for_root_leaves() {
walk(&[leaf(1.0)], &mut |_, transform| {
assert_eq!(*transform, Transform::IDENTITY);
});
}
}