Skip to main content

graphitepdf_document/
lib.rs

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}