1use graphitepdf_image::{
2 DataImageSource, DataUriImageSource, Image as ImageAsset, ImageFormat,
3 ImageSource as AssetImageSource, LocalImageSource, RemoteImageSource,
4};
5use graphitepdf_layout::{Document as LayoutDocument, Node as LayoutNode, Page as LayoutPage};
6use graphitepdf_primitives::Pt;
7pub use graphitepdf_style::{
8 AlignItems, EdgeInsets, FlexDirection, FontDescriptor, FontSource, FontStyle,
9 FontVariantWeight, JustifyContent, StandardFont, Style, StyleValue, Stylesheet,
10 StylesheetContainer, StylesheetExpandedStyle, StylesheetMap, StylesheetSafeStyle,
11};
12use graphitepdf_textkit::{TextBlock, TextSpan};
13use std::path::PathBuf;
14
15pub type PdfMetadata = graphitepdf_layout::LayoutMetadata;
16
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct Document {
19 metadata: PdfMetadata,
20 pages: Vec<Node>,
21}
22
23impl Document {
24 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn set_metadata(mut self, metadata: PdfMetadata) -> Self {
29 self.metadata = metadata;
30 self
31 }
32
33 pub fn with_metadata(self, metadata: PdfMetadata) -> Self {
34 self.set_metadata(metadata)
35 }
36
37 pub fn add_page(mut self, page: Node) -> Self {
38 self.pages.push(page);
39 self
40 }
41
42 pub fn get_metadata(&self) -> &PdfMetadata {
43 &self.metadata
44 }
45
46 pub fn metadata(&self) -> &PdfMetadata {
47 self.get_metadata()
48 }
49
50 pub fn pages(&self) -> &[Node] {
51 &self.pages
52 }
53
54 pub fn to_layout_document(&self) -> LayoutDocument {
55 let mut document = LayoutDocument::new().with_metadata(self.metadata.clone());
56 for page in &self.pages {
57 document.add_page(page.to_layout_page());
58 }
59 document
60 }
61
62 pub fn into_layout_document(self) -> LayoutDocument {
63 LayoutDocument::from(&self)
64 }
65}
66
67impl From<&Document> for LayoutDocument {
68 fn from(value: &Document) -> Self {
69 value.to_layout_document()
70 }
71}
72
73impl From<Document> for LayoutDocument {
74 fn from(value: Document) -> Self {
75 LayoutDocument::from(&value)
76 }
77}
78
79impl graphitepdf_renderer::RendererDocumentSource for Document {
80 fn build_render_document(
81 &self,
82 layout_engine: &graphitepdf_layout::LayoutEngine,
83 render_engine: &graphitepdf_render::RenderEngine,
84 ) -> graphitepdf_renderer::Result<graphitepdf_render::RenderDocument> {
85 let layout = layout_engine.layout_document(&LayoutDocument::from(self))?;
86 render_engine.build(&layout)
87 }
88}
89
90#[derive(Clone, Debug, PartialEq)]
91pub struct Node {
92 kind: NodeKind,
93 style: Style,
94}
95
96impl Node {
97 pub fn new(kind: NodeKind, style: Style) -> Self {
98 Self { kind, style }
99 }
100
101 pub fn from_stylesheet(
102 kind: NodeKind,
103 container: &StylesheetContainer,
104 stylesheet: &Stylesheet,
105 ) -> Self {
106 Self::new(kind, Style::from_stylesheet(container, stylesheet))
107 }
108
109 pub fn kind(&self) -> &NodeKind {
110 &self.kind
111 }
112
113 pub fn style(&self) -> &Style {
114 &self.style
115 }
116
117 pub fn to_layout_node(&self) -> LayoutNode {
118 let mut style = self.style.to_layout_style();
119
120 match &self.kind {
121 NodeKind::View { children } => {
122 LayoutNode::view(children.iter().map(LayoutNode::from)).with_style(style)
123 }
124 NodeKind::Text(text) => text
125 .to_text_block()
126 .map(LayoutNode::text)
127 .unwrap_or_else(graphitepdf_layout::Node::box_node)
128 .with_style(style),
129 NodeKind::Image(image) => {
130 if image.source.as_asset().is_none() && style.height.is_none() {
131 style.height = Some(Pt::new(120.0));
132 }
133
134 image.to_layout_node().with_style(style)
135 }
136 }
137 }
138
139 pub fn to_layout_page(&self) -> LayoutPage {
140 let style = self.style.to_layout_style();
141
142 match &self.kind {
143 NodeKind::View { children } => {
144 LayoutPage::new(children.iter().map(LayoutNode::from)).with_style(style)
145 }
146 _ => LayoutPage::new([self.to_layout_node()]).with_style(style),
147 }
148 }
149}
150
151impl From<&Node> for LayoutNode {
152 fn from(value: &Node) -> Self {
153 value.to_layout_node()
154 }
155}
156
157impl From<Node> for LayoutNode {
158 fn from(value: Node) -> Self {
159 LayoutNode::from(&value)
160 }
161}
162
163#[derive(Clone, Debug, PartialEq)]
164pub enum NodeKind {
165 View { children: Vec<Node> },
166 Text(TextNode),
167 Image(ImageNode),
168}
169
170#[derive(Clone, Debug, PartialEq)]
171pub struct TextNode {
172 pub content: String,
173}
174
175impl TextNode {
176 pub fn new(content: impl Into<String>) -> Self {
177 Self {
178 content: content.into(),
179 }
180 }
181
182 fn to_text_block(&self) -> Option<TextBlock> {
183 let span = TextSpan::new(self.content.clone()).ok()?;
184 Some(TextBlock::from(span))
185 }
186}
187
188#[derive(Clone, Debug, PartialEq)]
189pub struct ImageNode {
190 pub source: ImageSource,
191}
192
193impl ImageNode {
194 pub fn new(source: impl Into<ImageSource>) -> Self {
195 Self {
196 source: source.into(),
197 }
198 }
199
200 fn to_layout_node(&self) -> LayoutNode {
201 match &self.source {
202 ImageSource::Asset(asset) => LayoutNode::image_asset(asset.clone()),
203 ImageSource::Path(_) | ImageSource::Bytes(_) | ImageSource::AssetSource(_) => {
204 LayoutNode::image_source(self.source.as_asset_source())
205 }
206 }
207 }
208}
209
210#[derive(Clone, Debug, PartialEq)]
211pub enum ImageSource {
212 Path(PathBuf),
213 Bytes(Vec<u8>),
214 AssetSource(AssetImageSource),
215 Asset(ImageAsset),
216}
217
218impl ImageSource {
219 pub fn as_asset_source(&self) -> AssetImageSource {
220 match self {
221 Self::Path(path) => LocalImageSource::new(path.clone()).into(),
222 Self::Bytes(bytes) => bytes.clone().into(),
223 Self::AssetSource(source) => source.clone(),
224 Self::Asset(asset) => asset_image_source(asset),
225 }
226 }
227
228 pub const fn as_asset(&self) -> Option<&ImageAsset> {
229 match self {
230 Self::Asset(asset) => Some(asset),
231 Self::Path(_) | Self::Bytes(_) | Self::AssetSource(_) => None,
232 }
233 }
234}
235
236impl From<PathBuf> for ImageSource {
237 fn from(value: PathBuf) -> Self {
238 Self::Path(value)
239 }
240}
241
242impl From<Vec<u8>> for ImageSource {
243 fn from(value: Vec<u8>) -> Self {
244 Self::Bytes(value)
245 }
246}
247
248impl From<&[u8]> for ImageSource {
249 fn from(value: &[u8]) -> Self {
250 Self::Bytes(value.to_vec())
251 }
252}
253
254impl From<AssetImageSource> for ImageSource {
255 fn from(value: AssetImageSource) -> Self {
256 Self::AssetSource(value)
257 }
258}
259
260impl From<ImageAsset> for ImageSource {
261 fn from(value: ImageAsset) -> Self {
262 Self::Asset(value)
263 }
264}
265
266impl From<DataImageSource> for ImageSource {
267 fn from(value: DataImageSource) -> Self {
268 Self::AssetSource(value.into())
269 }
270}
271
272impl From<DataUriImageSource> for ImageSource {
273 fn from(value: DataUriImageSource) -> Self {
274 Self::AssetSource(value.into())
275 }
276}
277
278impl From<LocalImageSource> for ImageSource {
279 fn from(value: LocalImageSource) -> Self {
280 Self::AssetSource(value.into())
281 }
282}
283
284impl From<RemoteImageSource> for ImageSource {
285 fn from(value: RemoteImageSource) -> Self {
286 Self::AssetSource(value.into())
287 }
288}
289
290impl From<ImageSource> for AssetImageSource {
291 fn from(value: ImageSource) -> Self {
292 match value {
293 ImageSource::Path(path) => LocalImageSource::new(path).into(),
294 ImageSource::Bytes(bytes) => bytes.into(),
295 ImageSource::AssetSource(source) => source,
296 ImageSource::Asset(asset) => asset_image_source(&asset),
297 }
298 }
299}
300
301fn asset_image_source(asset: &ImageAsset) -> AssetImageSource {
302 match asset {
303 ImageAsset::Raster(image) => DataImageSource::new(image.data.clone(), image.format).into(),
304 ImageAsset::Svg(image) => {
305 DataImageSource::new(image.raw_data.clone(), ImageFormat::Svg).into()
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn converts_legacy_and_crate_image_sources_into_asset_sources() {
316 let path = PathBuf::from("/tmp/example.png");
317 let legacy = ImageSource::Path(path.clone());
318 let asset = legacy.as_asset_source();
319
320 assert_eq!(asset, AssetImageSource::from(LocalImageSource::new(path)));
321
322 let remote = RemoteImageSource::new("https://example.com/image.png");
323 let wrapped = ImageSource::from(remote.clone());
324
325 assert_eq!(wrapped.as_asset_source(), AssetImageSource::from(remote));
326 }
327
328 #[test]
329 fn converts_shared_image_assets_into_asset_sources() {
330 let asset = ImageAsset::Raster(graphitepdf_image::RasterImage {
331 width: 2,
332 height: 3,
333 data: vec![1, 2, 3, 4],
334 format: ImageFormat::Png,
335 key: Some(String::from("logo")),
336 });
337 let wrapped = ImageSource::from(asset.clone());
338
339 assert_eq!(wrapped.as_asset(), Some(&asset));
340 assert_eq!(
341 wrapped.as_asset_source(),
342 AssetImageSource::from(DataImageSource::new(vec![1, 2, 3, 4], ImageFormat::Png))
343 );
344 }
345
346 #[test]
347 fn builds_nodes_from_stylesheets() {
348 let container = StylesheetContainer::new(200.0, 300.0);
349 let stylesheet = Stylesheet::new(StyleValue::Object(
350 [("fontFamily".to_string(), "Inter".into())]
351 .into_iter()
352 .collect(),
353 ));
354
355 let node = Node::from_stylesheet(
356 NodeKind::Text(TextNode::new("Hello")),
357 &container,
358 &stylesheet,
359 );
360
361 assert_eq!(node.style().font_family.as_deref(), Some("Inter"));
362 assert_eq!(
363 LayoutNode::from(&node).style().font_family.as_deref(),
364 Some("Inter")
365 );
366 }
367
368 #[test]
369 fn preserves_legacy_default_height_for_unresolved_image_sources() {
370 let node = Node::new(
371 NodeKind::Image(ImageNode::new(RemoteImageSource::new(
372 "https://example.com/image.png",
373 ))),
374 Style::default(),
375 );
376
377 let layout_node = node.to_layout_node();
378
379 assert_eq!(layout_node.style().height, Some(Pt::new(120.0)));
380 }
381
382 #[test]
383 fn converts_compat_document_into_split_layout_document() {
384 let document = Document::new()
385 .set_metadata(PdfMetadata {
386 title: Some(String::from("Compat")),
387 ..PdfMetadata::default()
388 })
389 .add_page(Node::new(
390 NodeKind::View {
391 children: vec![Node::new(
392 NodeKind::Text(TextNode::new("Hello facade")),
393 Style::default(),
394 )],
395 },
396 Style::default(),
397 ));
398
399 let layout_document = document.to_layout_document();
400
401 assert_eq!(layout_document.metadata().title.as_deref(), Some("Compat"));
402 assert_eq!(layout_document.pages().len(), 1);
403 assert_eq!(layout_document.pages()[0].nodes().len(), 1);
404 }
405}