1pub mod error;
2
3pub use error::*;
4
5use graphitepdf_font::{
6 FontDescriptor, FontSource, FontStore, FontStyle, FontWeight, StandardFont,
7};
8use graphitepdf_image::{Image, ImageSource as AssetImageSource};
9use graphitepdf_math::{MathOptions, render_math_with_options};
10use graphitepdf_primitives::{Bounds, Color, Pt, Size};
11use graphitepdf_stylesheet::{
12 Container as StylesheetContainer, Style as StylesheetMap, StyleValue, Stylesheet,
13};
14use graphitepdf_svg::SvgNode;
15use graphitepdf_textkit::{
16 TextAttributes, TextBlock, TextContainer, TextEngine, TextEngineConfig, TextLayout, TextRect,
17};
18
19const DEFAULT_PAGE_WIDTH: f32 = 612.0;
20const DEFAULT_PAGE_HEIGHT: f32 = 792.0;
21
22pub const ORDERED_PIPELINE: [LayoutPipelineStep; 10] = [
23 LayoutPipelineStep::PageSizing,
24 LayoutPipelineStep::Styles,
25 LayoutPipelineStep::Inheritance,
26 LayoutPipelineStep::Assets,
27 LayoutPipelineStep::TextLayout,
28 LayoutPipelineStep::SvgResolution,
29 LayoutPipelineStep::Dimensions,
30 LayoutPipelineStep::Pagination,
31 LayoutPipelineStep::Origins,
32 LayoutPipelineStep::ZIndex,
33];
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum LayoutPipelineStep {
37 PageSizing,
38 Styles,
39 Inheritance,
40 Assets,
41 TextLayout,
42 SvgResolution,
43 Dimensions,
44 Pagination,
45 Origins,
46 ZIndex,
47}
48
49#[derive(Clone, Debug, Default, PartialEq)]
50pub struct LayoutMetadata {
51 pub title: Option<String>,
52 pub author: Option<String>,
53 pub subject: Option<String>,
54 pub keywords: Vec<String>,
55 pub creator: Option<String>,
56 pub producer: Option<String>,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq)]
60pub struct EdgeInsets {
61 pub top: Pt,
62 pub right: Pt,
63 pub bottom: Pt,
64 pub left: Pt,
65}
66
67impl EdgeInsets {
68 pub const fn new(top: Pt, right: Pt, bottom: Pt, left: Pt) -> Self {
69 Self {
70 top,
71 right,
72 bottom,
73 left,
74 }
75 }
76
77 pub const fn all(value: Pt) -> Self {
78 Self::new(value, value, value, value)
79 }
80
81 pub const fn horizontal(self) -> f32 {
82 self.left.value() + self.right.value()
83 }
84
85 pub const fn vertical(self) -> f32 {
86 self.top.value() + self.bottom.value()
87 }
88}
89
90#[derive(Clone, Debug, Default, PartialEq)]
91pub struct LayoutStyle {
92 pub width: Option<Pt>,
93 pub height: Option<Pt>,
94 pub margin: Option<EdgeInsets>,
95 pub padding: Option<EdgeInsets>,
96 pub background_color: Option<Color>,
97 pub color: Option<Color>,
98 pub font_family: Option<String>,
99 pub font_style: Option<FontStyle>,
100 pub font_weight: Option<FontWeight>,
101 pub font_source: Option<FontSource>,
102 pub font_size: Option<Pt>,
103 pub line_height: Option<Pt>,
104 pub z_index: Option<i32>,
105 pub page_break_before: Option<bool>,
106 pub page_break_after: Option<bool>,
107}
108
109impl LayoutStyle {
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn with_width(mut self, width: Pt) -> Self {
115 self.width = Some(width);
116 self
117 }
118
119 pub fn with_height(mut self, height: Pt) -> Self {
120 self.height = Some(height);
121 self
122 }
123
124 pub fn with_margin(mut self, margin: EdgeInsets) -> Self {
125 self.margin = Some(margin);
126 self
127 }
128
129 pub fn with_padding(mut self, padding: EdgeInsets) -> Self {
130 self.padding = Some(padding);
131 self
132 }
133
134 pub fn with_background_color(mut self, color: Color) -> Self {
135 self.background_color = Some(color);
136 self
137 }
138
139 pub fn with_color(mut self, color: Color) -> Self {
140 self.color = Some(color);
141 self
142 }
143
144 pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
145 self.font_family = Some(family.into());
146 self
147 }
148
149 pub fn with_font_style(mut self, style: FontStyle) -> Self {
150 self.font_style = Some(style);
151 self
152 }
153
154 pub fn with_font_weight(mut self, weight: FontWeight) -> Self {
155 self.font_weight = Some(weight);
156 self
157 }
158
159 pub fn with_font_source(mut self, source: FontSource) -> Self {
160 self.font_source = Some(source);
161 self
162 }
163
164 pub fn with_font_size(mut self, font_size: Pt) -> Self {
165 self.font_size = Some(font_size);
166 self
167 }
168
169 pub fn with_line_height(mut self, line_height: Pt) -> Self {
170 self.line_height = Some(line_height);
171 self
172 }
173
174 pub fn with_z_index(mut self, z_index: i32) -> Self {
175 self.z_index = Some(z_index);
176 self
177 }
178
179 pub fn with_page_break_before(mut self, value: bool) -> Self {
180 self.page_break_before = Some(value);
181 self
182 }
183
184 pub fn with_page_break_after(mut self, value: bool) -> Self {
185 self.page_break_after = Some(value);
186 self
187 }
188}
189
190#[derive(Clone, Debug, PartialEq)]
191pub struct Document {
192 metadata: LayoutMetadata,
193 pages: Vec<Page>,
194}
195
196impl Document {
197 pub fn new() -> Self {
198 Self {
199 metadata: LayoutMetadata::default(),
200 pages: Vec::new(),
201 }
202 }
203
204 pub fn with_metadata(mut self, metadata: LayoutMetadata) -> Self {
205 self.metadata = metadata;
206 self
207 }
208
209 pub fn metadata(&self) -> &LayoutMetadata {
210 &self.metadata
211 }
212
213 pub fn with_page(mut self, page: Page) -> Self {
214 self.pages.push(page);
215 self
216 }
217
218 pub fn add_page(&mut self, page: Page) {
219 self.pages.push(page);
220 }
221
222 pub fn pages(&self) -> &[Page] {
223 &self.pages
224 }
225}
226
227impl Default for Document {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233#[derive(Clone, Debug, PartialEq)]
234pub struct Page {
235 size: Option<Size>,
236 style: LayoutStyle,
237 stylesheet: Option<Stylesheet>,
238 nodes: Vec<Node>,
239}
240
241impl Page {
242 pub fn new(nodes: impl IntoIterator<Item = Node>) -> Self {
243 Self {
244 size: None,
245 style: LayoutStyle::default(),
246 stylesheet: None,
247 nodes: nodes.into_iter().collect(),
248 }
249 }
250
251 pub fn with_size(mut self, size: Size) -> Self {
252 self.size = Some(size);
253 self
254 }
255
256 pub fn with_style(mut self, style: LayoutStyle) -> Self {
257 self.style = style;
258 self
259 }
260
261 pub fn with_stylesheet(mut self, stylesheet: Stylesheet) -> Self {
262 self.stylesheet = Some(stylesheet);
263 self
264 }
265
266 pub fn with_node(mut self, node: Node) -> Self {
267 self.nodes.push(node);
268 self
269 }
270
271 pub fn size(&self) -> Option<Size> {
272 self.size
273 }
274
275 pub fn style(&self) -> &LayoutStyle {
276 &self.style
277 }
278
279 pub fn stylesheet(&self) -> Option<&Stylesheet> {
280 self.stylesheet.as_ref()
281 }
282
283 pub fn nodes(&self) -> &[Node] {
284 &self.nodes
285 }
286}
287
288#[derive(Clone, Debug, PartialEq)]
289pub struct MathFragment {
290 pub source: String,
291 pub options: MathOptions,
292}
293
294impl MathFragment {
295 pub fn new(source: impl Into<String>) -> Self {
296 Self {
297 source: source.into(),
298 options: MathOptions::default(),
299 }
300 }
301
302 pub fn with_options(mut self, options: MathOptions) -> Self {
303 self.options = options;
304 self
305 }
306}
307
308#[derive(Clone, Debug, PartialEq)]
309pub enum NodeKind {
310 View,
311 Box,
312 Text(TextBlock),
313 ImageAsset(Image),
314 ImageSource(AssetImageSource),
315 Svg(SvgNode),
316 Math(MathFragment),
317}
318
319#[derive(Clone, Debug, PartialEq)]
320pub struct Node {
321 kind: NodeKind,
322 style: LayoutStyle,
323 stylesheet: Option<Stylesheet>,
324 children: Vec<Node>,
325}
326
327impl Node {
328 pub fn new(kind: NodeKind) -> Self {
329 Self {
330 kind,
331 style: LayoutStyle::default(),
332 stylesheet: None,
333 children: Vec::new(),
334 }
335 }
336
337 pub fn view(children: impl IntoIterator<Item = Node>) -> Self {
338 Self {
339 kind: NodeKind::View,
340 style: LayoutStyle::default(),
341 stylesheet: None,
342 children: children.into_iter().collect(),
343 }
344 }
345
346 pub fn box_node() -> Self {
347 Self::new(NodeKind::Box)
348 }
349
350 pub fn text(block: TextBlock) -> Self {
351 Self::new(NodeKind::Text(block))
352 }
353
354 pub fn image_asset(asset: Image) -> Self {
355 Self::new(NodeKind::ImageAsset(asset))
356 }
357
358 pub fn image_source(source: impl Into<AssetImageSource>) -> Self {
359 Self::new(NodeKind::ImageSource(source.into()))
360 }
361
362 pub fn svg(svg: SvgNode) -> Self {
363 Self::new(NodeKind::Svg(svg))
364 }
365
366 pub fn math(source: impl Into<String>) -> Self {
367 Self::new(NodeKind::Math(MathFragment::new(source)))
368 }
369
370 pub fn math_with_options(source: impl Into<String>, options: MathOptions) -> Self {
371 Self::new(NodeKind::Math(MathFragment {
372 source: source.into(),
373 options,
374 }))
375 }
376
377 pub fn with_style(mut self, style: LayoutStyle) -> Self {
378 self.style = style;
379 self
380 }
381
382 pub fn with_stylesheet(mut self, stylesheet: Stylesheet) -> Self {
383 self.stylesheet = Some(stylesheet);
384 self
385 }
386
387 pub fn with_child(mut self, child: Node) -> Self {
388 self.children.push(child);
389 self
390 }
391
392 pub fn with_children(mut self, children: impl IntoIterator<Item = Node>) -> Self {
393 self.children.extend(children);
394 self
395 }
396
397 pub fn kind(&self) -> &NodeKind {
398 &self.kind
399 }
400
401 pub fn style(&self) -> &LayoutStyle {
402 &self.style
403 }
404
405 pub fn stylesheet(&self) -> Option<&Stylesheet> {
406 self.stylesheet.as_ref()
407 }
408
409 pub fn children(&self) -> &[Node] {
410 &self.children
411 }
412}
413
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub struct SafeFont {
416 pub descriptor: FontDescriptor,
417 pub source: Option<FontSource>,
418}
419
420#[derive(Clone, Debug, PartialEq)]
421pub struct SafeLayoutStyle {
422 pub width: Option<Pt>,
423 pub height: Option<Pt>,
424 pub margin: EdgeInsets,
425 pub padding: EdgeInsets,
426 pub background_color: Option<Color>,
427 pub color: Color,
428 pub font: SafeFont,
429 pub font_size: Pt,
430 pub line_height: Pt,
431 pub z_index: i32,
432 pub page_break_before: bool,
433 pub page_break_after: bool,
434}
435
436#[derive(Clone, Debug, PartialEq)]
437pub enum SafeNodeKind {
438 View,
439 Box,
440 Text {
441 block: TextBlock,
442 layout: TextLayout,
443 },
444 ImageAsset {
445 asset: Image,
446 },
447 ImageSource {
448 source: AssetImageSource,
449 },
450 Svg {
451 svg: SvgNode,
452 },
453 Math {
454 source: String,
455 svg: SvgNode,
456 },
457}
458
459#[derive(Clone, Debug, PartialEq)]
460pub struct SafeLayoutNode {
461 pub frame: Bounds,
462 pub content_frame: Bounds,
463 pub style: SafeLayoutStyle,
464 pub kind: SafeNodeKind,
465 pub children: Vec<SafeLayoutNode>,
466 pub page_index: usize,
467}
468
469impl SafeLayoutNode {
470 pub fn font_descriptor(&self) -> Option<&FontDescriptor> {
471 match &self.kind {
472 SafeNodeKind::Text { .. } => Some(&self.style.font.descriptor),
473 SafeNodeKind::View
474 | SafeNodeKind::Box
475 | SafeNodeKind::ImageAsset { .. }
476 | SafeNodeKind::ImageSource { .. }
477 | SafeNodeKind::Svg { .. }
478 | SafeNodeKind::Math { .. } => None,
479 }
480 }
481
482 pub fn children(&self) -> &[SafeLayoutNode] {
483 &self.children
484 }
485
486 pub fn text_layout(&self) -> Option<&TextLayout> {
487 match &self.kind {
488 SafeNodeKind::Text { layout, .. } => Some(layout),
489 SafeNodeKind::View
490 | SafeNodeKind::Box
491 | SafeNodeKind::ImageAsset { .. }
492 | SafeNodeKind::ImageSource { .. }
493 | SafeNodeKind::Svg { .. }
494 | SafeNodeKind::Math { .. } => None,
495 }
496 }
497
498 pub fn z_index(&self) -> i32 {
499 self.style.z_index
500 }
501
502 pub fn style(&self) -> &SafeLayoutStyle {
503 &self.style
504 }
505}
506
507#[derive(Clone, Debug, PartialEq)]
508pub struct SafeLayoutPage {
509 pub size: Size,
510 pub style: SafeLayoutStyle,
511 pub nodes: Vec<SafeLayoutNode>,
512 pub source_page_index: usize,
513}
514
515impl SafeLayoutPage {
516 pub fn nodes(&self) -> &[SafeLayoutNode] {
517 &self.nodes
518 }
519}
520
521#[derive(Clone, Debug, PartialEq)]
522pub struct SafeLayoutDocument {
523 pub metadata: LayoutMetadata,
524 pub pages: Vec<SafeLayoutPage>,
525 pub pipeline: Vec<LayoutPipelineStep>,
526}
527
528impl SafeLayoutDocument {
529 pub fn pages(&self) -> &[SafeLayoutPage] {
530 &self.pages
531 }
532
533 pub fn pipeline(&self) -> &[LayoutPipelineStep] {
534 &self.pipeline
535 }
536}
537
538pub struct LayoutEngine {
539 text_engine: TextEngine,
540 font_store: FontStore,
541 default_page_size: Size,
542}
543
544impl Default for LayoutEngine {
545 fn default() -> Self {
546 Self::new()
547 }
548}
549
550impl LayoutEngine {
551 pub fn new() -> Self {
552 Self {
553 text_engine: TextEngine::default(),
554 font_store: FontStore::default(),
555 default_page_size: Size::new(DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT),
556 }
557 }
558
559 pub fn with_text_engine_config(mut self, config: TextEngineConfig) -> Self {
560 self.text_engine = TextEngine::new(config);
561 self
562 }
563
564 pub fn with_default_page_size(mut self, size: Size) -> Result<Self> {
565 validate_page_size(size)?;
566 self.default_page_size = size;
567 Ok(self)
568 }
569
570 pub fn text_engine(&self) -> &TextEngine {
571 &self.text_engine
572 }
573
574 pub fn font_store(&self) -> &FontStore {
575 &self.font_store
576 }
577
578 pub fn font_store_mut(&mut self) -> &mut FontStore {
579 &mut self.font_store
580 }
581
582 pub fn layout_text_block(&self, page_size: Size, block: TextBlock) -> Result<LayoutDocument> {
583 validate_page_size(page_size)?;
584
585 let node = LayoutNode {
586 frame: Bounds::from_origin_size(0.0, 0.0, page_size.width, page_size.height),
587 content: LayoutContent::Text(block),
588 };
589
590 Ok(LayoutDocument {
591 pages: vec![LayoutPage {
592 size: page_size,
593 nodes: vec![node],
594 }],
595 })
596 }
597
598 pub fn layout_document(&self, document: &Document) -> Result<SafeLayoutDocument> {
599 if document.pages().is_empty() {
600 return Err(Error::EmptyDocument);
601 }
602
603 let mut pages = Vec::new();
604 for (source_page_index, page) in document.pages().iter().enumerate() {
605 pages.extend(self.layout_page(page, source_page_index)?);
606 }
607
608 Ok(SafeLayoutDocument {
609 metadata: document.metadata().clone(),
610 pages,
611 pipeline: ORDERED_PIPELINE.to_vec(),
612 })
613 }
614
615 fn layout_page(&self, page: &Page, source_page_index: usize) -> Result<Vec<SafeLayoutPage>> {
616 let size = self.resolve_page_size(page)?;
617 let page_container = stylesheet_container(size);
618 let page_seed = resolve_style_seed(&page.style, page.stylesheet.as_ref(), &page_container);
619 let page_style = SafeLayoutStyle::from_seed(page_seed, None);
620
621 let available_width = (size.width - page_style.padding.horizontal()).max(0.0);
622 let available_height = (size.height - page_style.padding.vertical()).max(0.0);
623 let child_container =
624 StylesheetContainer::new(available_width as f64, available_height as f64);
625
626 let mut measured = Vec::with_capacity(page.nodes().len());
627 for node in page.nodes() {
628 measured.push(self.measure_node(
629 node,
630 &page_style,
631 &child_container,
632 available_width,
633 available_height,
634 )?);
635 }
636
637 self.paginate_page(size, page_style, measured, source_page_index)
638 }
639
640 fn resolve_page_size(&self, page: &Page) -> Result<Size> {
641 let base_container = stylesheet_container(self.default_page_size);
642 let page_seed = resolve_style_seed(&page.style, page.stylesheet.as_ref(), &base_container);
643
644 let width = page
645 .size()
646 .map(|size| size.width)
647 .or_else(|| page_seed.width.map(Pt::value))
648 .unwrap_or(self.default_page_size.width);
649 let height = page
650 .size()
651 .map(|size| size.height)
652 .or_else(|| page_seed.height.map(Pt::value))
653 .unwrap_or(self.default_page_size.height);
654
655 let size = Size::new(width, height);
656 validate_page_size(size)?;
657 Ok(size)
658 }
659
660 fn measure_node(
661 &self,
662 node: &Node,
663 parent_style: &SafeLayoutStyle,
664 container: &StylesheetContainer,
665 available_width: f32,
666 available_height: f32,
667 ) -> Result<MeasuredNode> {
668 let seed = resolve_style_seed(node.style(), node.stylesheet(), container);
669 let style = SafeLayoutStyle::from_seed(seed, Some(parent_style));
670
671 let width = style.width.map(Pt::value).unwrap_or_else(|| {
672 (available_width - style.margin.left.value() - style.margin.right.value()).max(0.0)
673 });
674
675 let measured = match node.kind() {
676 NodeKind::View => self.measure_view(node, style, width, available_height)?,
677 NodeKind::Box => self.measure_box(style, width),
678 NodeKind::Text(block) => self.measure_text(block, style, width, available_height)?,
679 NodeKind::ImageAsset(asset) => self.measure_image_asset(asset.clone(), style, width)?,
680 NodeKind::ImageSource(source) => {
681 self.measure_image_source(source.clone(), style, width)?
682 }
683 NodeKind::Svg(svg) => self.measure_svg(svg.clone(), style, width)?,
684 NodeKind::Math(fragment) => self.measure_math(fragment, style, width)?,
685 };
686
687 Ok(measured)
688 }
689
690 fn measure_view(
691 &self,
692 node: &Node,
693 style: SafeLayoutStyle,
694 width: f32,
695 available_height: f32,
696 ) -> Result<MeasuredNode> {
697 let child_available_width = (width - style.padding.horizontal()).max(0.0);
698 let child_available_height = style
699 .height
700 .map(Pt::value)
701 .unwrap_or(available_height)
702 .max(style.line_height.value());
703 let child_container =
704 StylesheetContainer::new(child_available_width as f64, child_available_height as f64);
705
706 let mut children = Vec::with_capacity(node.children().len());
707 for child in node.children() {
708 children.push(self.measure_node(
709 child,
710 &style,
711 &child_container,
712 child_available_width,
713 child_available_height,
714 )?);
715 }
716
717 let content_height = children.iter().map(MeasuredNode::outer_height).sum::<f32>();
718 let height = style
719 .height
720 .map(Pt::value)
721 .unwrap_or(content_height + style.padding.vertical());
722
723 Ok(MeasuredNode {
724 kind: SafeNodeKind::View,
725 style,
726 size: Size::new(width, height.max(0.0)),
727 children,
728 })
729 }
730
731 fn measure_box(&self, style: SafeLayoutStyle, width: f32) -> MeasuredNode {
732 let height = style.height.map(Pt::value).unwrap_or(0.0);
733
734 MeasuredNode {
735 kind: SafeNodeKind::Box,
736 style,
737 size: Size::new(width, height),
738 children: Vec::new(),
739 }
740 }
741
742 fn measure_text(
743 &self,
744 block: &TextBlock,
745 style: SafeLayoutStyle,
746 width: f32,
747 available_height: f32,
748 ) -> Result<MeasuredNode> {
749 let attributes = TextAttributes::default()
750 .with_font(style.font.descriptor.clone())
751 .with_font_size(style.font_size)?;
752 let attributed = block
753 .to_attributed_string()?
754 .with_default_attributes(attributes)?;
755 let container_height = style
756 .height
757 .map(Pt::value)
758 .unwrap_or_else(|| available_height.max(style.line_height.value()));
759 let container = TextContainer::new(TextRect::from_values(
760 0.0,
761 0.0,
762 width.max(style.line_height.value()).max(1.0),
763 container_height.max(style.line_height.value()).max(1.0),
764 ))?;
765 let layout = self
766 .text_engine
767 .layout(&attributed, &container, Some(&self.font_store))?;
768 let height = style
769 .height
770 .map(Pt::value)
771 .unwrap_or_else(|| layout.bounds().height.value());
772 let line_height = style.line_height.value();
773
774 Ok(MeasuredNode {
775 kind: SafeNodeKind::Text {
776 block: block.clone(),
777 layout,
778 },
779 style,
780 size: Size::new(width, height.max(line_height)),
781 children: Vec::new(),
782 })
783 }
784
785 fn measure_image_asset(
786 &self,
787 asset: Image,
788 style: SafeLayoutStyle,
789 width: f32,
790 ) -> Result<MeasuredNode> {
791 let size = resolve_replaced_size(
792 Size::new(asset.width(), asset.height()),
793 style.width.map(Pt::value).unwrap_or(width),
794 style.width.map(Pt::value),
795 style.height.map(Pt::value),
796 "image asset",
797 )?;
798
799 Ok(MeasuredNode {
800 kind: SafeNodeKind::ImageAsset { asset },
801 style,
802 size,
803 children: Vec::new(),
804 })
805 }
806
807 fn measure_image_source(
808 &self,
809 source: AssetImageSource,
810 style: SafeLayoutStyle,
811 width: f32,
812 ) -> Result<MeasuredNode> {
813 let resolved_width = style.width.map(Pt::value).unwrap_or(width);
814 let resolved_height =
815 style
816 .height
817 .map(Pt::value)
818 .ok_or(Error::UnresolvedAssetDimensions {
819 kind: "image source",
820 })?;
821
822 Ok(MeasuredNode {
823 kind: SafeNodeKind::ImageSource { source },
824 style,
825 size: Size::new(resolved_width, resolved_height),
826 children: Vec::new(),
827 })
828 }
829
830 fn measure_svg(
831 &self,
832 svg: SvgNode,
833 style: SafeLayoutStyle,
834 width: f32,
835 ) -> Result<MeasuredNode> {
836 let natural = resolve_svg_size(&svg)?;
837 let size = resolve_replaced_size(
838 natural,
839 style.width.map(Pt::value).unwrap_or(width),
840 style.width.map(Pt::value),
841 style.height.map(Pt::value),
842 "svg",
843 )?;
844
845 Ok(MeasuredNode {
846 kind: SafeNodeKind::Svg { svg },
847 style,
848 size,
849 children: Vec::new(),
850 })
851 }
852
853 fn measure_math(
854 &self,
855 fragment: &MathFragment,
856 style: SafeLayoutStyle,
857 width: f32,
858 ) -> Result<MeasuredNode> {
859 let rendered = render_math_with_options(&fragment.source, &fragment.options)?;
860 let natural = resolve_svg_size(&rendered.svg)?;
861 let size = resolve_replaced_size(
862 natural,
863 style.width.map(Pt::value).unwrap_or(width),
864 style.width.map(Pt::value),
865 style.height.map(Pt::value),
866 "math",
867 )?;
868
869 Ok(MeasuredNode {
870 kind: SafeNodeKind::Math {
871 source: fragment.source.clone(),
872 svg: rendered.svg,
873 },
874 style,
875 size,
876 children: Vec::new(),
877 })
878 }
879
880 fn paginate_page(
881 &self,
882 size: Size,
883 page_style: SafeLayoutStyle,
884 measured: Vec<MeasuredNode>,
885 source_page_index: usize,
886 ) -> Result<Vec<SafeLayoutPage>> {
887 let page_top = page_style.padding.top.value();
888 let page_left = page_style.padding.left.value();
889 let page_bottom = size.height - page_style.padding.bottom.value();
890
891 let mut chunked = Vec::<Vec<MeasuredNode>>::new();
892 let mut current = Vec::<MeasuredNode>::new();
893 let mut cursor_y = page_top;
894
895 for node in measured {
896 if node.style.page_break_before && !current.is_empty() {
897 chunked.push(std::mem::take(&mut current));
898 cursor_y = page_top;
899 }
900
901 let outer_height = node.outer_height();
902 let next_bottom = cursor_y + outer_height;
903 let overflows = next_bottom > page_bottom;
904 if overflows && !current.is_empty() {
905 chunked.push(std::mem::take(&mut current));
906 cursor_y = page_top;
907 }
908
909 cursor_y += outer_height;
910 let break_after = node.style.page_break_after;
911 current.push(node);
912
913 if break_after {
914 chunked.push(std::mem::take(&mut current));
915 cursor_y = page_top;
916 }
917 }
918
919 if !current.is_empty() {
920 chunked.push(current);
921 }
922
923 if chunked.is_empty() {
924 chunked.push(Vec::new());
925 }
926
927 let mut pages = Vec::with_capacity(chunked.len());
928 for (page_index, nodes) in chunked.into_iter().enumerate() {
929 let positioned = position_nodes(
930 nodes,
931 page_left,
932 page_top,
933 size.width - page_style.padding.horizontal(),
934 page_index,
935 );
936 pages.push(SafeLayoutPage {
937 size,
938 style: page_style.clone(),
939 nodes: sort_by_z_index(positioned),
940 source_page_index,
941 });
942 }
943
944 Ok(pages)
945 }
946}
947
948#[derive(Clone, Debug, Default, PartialEq)]
949pub struct LayoutDocument {
950 pub pages: Vec<LayoutPage>,
951}
952
953#[derive(Clone, Debug, PartialEq)]
954pub struct LayoutPage {
955 pub size: Size,
956 pub nodes: Vec<LayoutNode>,
957}
958
959#[derive(Clone, Debug, PartialEq)]
960pub struct LayoutNode {
961 pub frame: Bounds,
962 pub content: LayoutContent,
963}
964
965impl LayoutNode {
966 pub fn font_descriptor(&self) -> Option<&FontDescriptor> {
967 match &self.content {
968 LayoutContent::Text(block) => block.spans().iter().find_map(|span| span.font()),
969 LayoutContent::Box => None,
970 }
971 }
972}
973
974#[derive(Clone, Debug, PartialEq)]
975pub enum LayoutContent {
976 Text(TextBlock),
977 Box,
978}
979
980#[derive(Clone, Debug)]
981struct MeasuredNode {
982 kind: SafeNodeKind,
983 style: SafeLayoutStyle,
984 size: Size,
985 children: Vec<MeasuredNode>,
986}
987
988impl MeasuredNode {
989 fn outer_height(&self) -> f32 {
990 self.style.margin.top.value() + self.size.height + self.style.margin.bottom.value()
991 }
992}
993
994#[derive(Clone, Debug, Default)]
995struct StyleSeed {
996 width: Option<Pt>,
997 height: Option<Pt>,
998 margin: Option<EdgeInsets>,
999 padding: Option<EdgeInsets>,
1000 background_color: Option<Color>,
1001 color: Option<Color>,
1002 font_family: Option<String>,
1003 font_style: Option<FontStyle>,
1004 font_weight: Option<FontWeight>,
1005 font_source: Option<FontSource>,
1006 font_size: Option<Pt>,
1007 line_height: Option<Pt>,
1008 z_index: Option<i32>,
1009 page_break_before: Option<bool>,
1010 page_break_after: Option<bool>,
1011}
1012
1013impl SafeLayoutStyle {
1014 fn from_seed(seed: StyleSeed, parent: Option<&SafeLayoutStyle>) -> Self {
1015 let fallback_descriptor = parent
1016 .map(|style| style.font.descriptor.clone())
1017 .unwrap_or_else(|| FontDescriptor::new(StandardFont::Helvetica.family_name()));
1018 let family = seed
1019 .font_family
1020 .clone()
1021 .or_else(|| seed.font_source.as_ref().and_then(font_source_family))
1022 .unwrap_or_else(|| fallback_descriptor.family().to_string());
1023 let font_style = seed
1024 .font_style
1025 .or_else(|| parent.map(|style| style.font.descriptor.font_style()))
1026 .unwrap_or_else(|| fallback_descriptor.font_style());
1027 let font_weight = seed
1028 .font_weight
1029 .or_else(|| parent.map(|style| style.font.descriptor.font_weight()))
1030 .unwrap_or_else(|| fallback_descriptor.font_weight());
1031 let mut descriptor = FontDescriptor::new(family).with_style(font_style);
1032 descriptor = descriptor.with_weight(font_weight);
1033
1034 let font_size = seed
1035 .font_size
1036 .or_else(|| parent.map(|style| style.font_size))
1037 .unwrap_or(Pt::new(12.0));
1038 let line_height = seed
1039 .line_height
1040 .or_else(|| parent.map(|style| style.line_height))
1041 .unwrap_or_else(|| Pt::new(font_size.value() * 1.2));
1042
1043 Self {
1044 width: seed.width,
1045 height: seed.height,
1046 margin: seed.margin.unwrap_or_default(),
1047 padding: seed.padding.unwrap_or_default(),
1048 background_color: seed.background_color,
1049 color: seed
1050 .color
1051 .or_else(|| parent.map(|style| style.color))
1052 .unwrap_or(Color::BLACK),
1053 font: SafeFont {
1054 descriptor,
1055 source: seed
1056 .font_source
1057 .or_else(|| parent.and_then(|style| style.font.source.clone())),
1058 },
1059 font_size,
1060 line_height,
1061 z_index: seed.z_index.unwrap_or(0),
1062 page_break_before: seed.page_break_before.unwrap_or(false),
1063 page_break_after: seed.page_break_after.unwrap_or(false),
1064 }
1065 }
1066}
1067
1068fn resolve_style_seed(
1069 input: &LayoutStyle,
1070 stylesheet: Option<&Stylesheet>,
1071 container: &StylesheetContainer,
1072) -> StyleSeed {
1073 let mut seed = StyleSeed::default();
1074 if let Some(stylesheet) = stylesheet {
1075 apply_resolved_stylesheet(&mut seed, &stylesheet.resolve(container));
1076 }
1077 apply_input_style(&mut seed, input);
1078 seed
1079}
1080
1081fn apply_input_style(seed: &mut StyleSeed, input: &LayoutStyle) {
1082 if let Some(value) = input.width {
1083 seed.width = Some(value);
1084 }
1085 if let Some(value) = input.height {
1086 seed.height = Some(value);
1087 }
1088 if let Some(value) = input.margin {
1089 seed.margin = Some(value);
1090 }
1091 if let Some(value) = input.padding {
1092 seed.padding = Some(value);
1093 }
1094 if let Some(value) = input.background_color {
1095 seed.background_color = Some(value);
1096 }
1097 if let Some(value) = input.color {
1098 seed.color = Some(value);
1099 }
1100 if let Some(value) = &input.font_family {
1101 seed.font_family = Some(value.clone());
1102 }
1103 if let Some(value) = input.font_style {
1104 seed.font_style = Some(value);
1105 }
1106 if let Some(value) = input.font_weight {
1107 seed.font_weight = Some(value);
1108 }
1109 if let Some(value) = &input.font_source {
1110 seed.font_source = Some(value.clone());
1111 }
1112 if let Some(value) = input.font_size {
1113 seed.font_size = Some(value);
1114 }
1115 if let Some(value) = input.line_height {
1116 seed.line_height = Some(value);
1117 }
1118 if let Some(value) = input.z_index {
1119 seed.z_index = Some(value);
1120 }
1121 if let Some(value) = input.page_break_before {
1122 seed.page_break_before = Some(value);
1123 }
1124 if let Some(value) = input.page_break_after {
1125 seed.page_break_after = Some(value);
1126 }
1127}
1128
1129fn apply_resolved_stylesheet(seed: &mut StyleSeed, style: &StylesheetMap) {
1130 if let Some(value) = stylesheet_pt(style, "width") {
1131 seed.width = Some(value);
1132 }
1133 if let Some(value) = stylesheet_pt(style, "height") {
1134 seed.height = Some(value);
1135 }
1136 if let Some(value) = stylesheet_color(style, "backgroundColor") {
1137 seed.background_color = Some(value);
1138 }
1139 if let Some(value) = stylesheet_color(style, "color") {
1140 seed.color = Some(value);
1141 }
1142 if let Some(value) = stylesheet_string(style, "fontFamily") {
1143 seed.font_family = Some(value.to_string());
1144 }
1145 if let Some(value) = stylesheet_font_style(style, "fontStyle") {
1146 seed.font_style = Some(value);
1147 }
1148 if let Some(value) = stylesheet_font_weight(style, "fontWeight") {
1149 seed.font_weight = Some(value);
1150 }
1151 if let Some(value) = stylesheet_string(style, "fontSource") {
1152 seed.font_source = Some(FontSource::remote(value));
1153 }
1154 if let Some(value) = stylesheet_string(style, "fontSourceLocal") {
1155 seed.font_source = Some(FontSource::local(value));
1156 }
1157 if let Some(value) = stylesheet_string(style, "fontSourceDataUri") {
1158 seed.font_source = Some(FontSource::data_uri(value));
1159 }
1160 if let Some(value) = stylesheet_standard_font(style, "fontSourceStandard") {
1161 seed.font_source = Some(FontSource::standard(value));
1162 }
1163 if let Some(value) = stylesheet_pt(style, "fontSize") {
1164 seed.font_size = Some(value);
1165 }
1166 if let Some(value) = stylesheet_pt(style, "lineHeight") {
1167 seed.line_height = Some(value);
1168 }
1169 if let Some(value) = stylesheet_i32(style, "zIndex") {
1170 seed.z_index = Some(value);
1171 }
1172 if let Some(value) = stylesheet_bool(style, "pageBreakBefore") {
1173 seed.page_break_before = Some(value);
1174 }
1175 if let Some(value) = stylesheet_bool(style, "pageBreakAfter") {
1176 seed.page_break_after = Some(value);
1177 }
1178
1179 apply_edge_insets(
1180 &mut seed.margin,
1181 style,
1182 ["marginTop", "marginRight", "marginBottom", "marginLeft"],
1183 );
1184 apply_edge_insets(
1185 &mut seed.padding,
1186 style,
1187 ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
1188 );
1189}
1190
1191fn apply_edge_insets(target: &mut Option<EdgeInsets>, style: &StylesheetMap, keys: [&str; 4]) {
1192 let mut value = target.unwrap_or_default();
1193 let mut changed = false;
1194
1195 if let Some(edge) = stylesheet_pt(style, keys[0]) {
1196 value.top = edge;
1197 changed = true;
1198 }
1199 if let Some(edge) = stylesheet_pt(style, keys[1]) {
1200 value.right = edge;
1201 changed = true;
1202 }
1203 if let Some(edge) = stylesheet_pt(style, keys[2]) {
1204 value.bottom = edge;
1205 changed = true;
1206 }
1207 if let Some(edge) = stylesheet_pt(style, keys[3]) {
1208 value.left = edge;
1209 changed = true;
1210 }
1211
1212 if changed {
1213 *target = Some(value);
1214 }
1215}
1216
1217fn stylesheet_pt(style: &StylesheetMap, key: &str) -> Option<Pt> {
1218 stylesheet_f32(style, key).map(Pt::new)
1219}
1220
1221fn stylesheet_f32(style: &StylesheetMap, key: &str) -> Option<f32> {
1222 match style.get(key)? {
1223 StyleValue::Number(value) => Some(*value as f32),
1224 StyleValue::String(value) => value.trim().parse::<f32>().ok(),
1225 _ => None,
1226 }
1227}
1228
1229fn stylesheet_i32(style: &StylesheetMap, key: &str) -> Option<i32> {
1230 match style.get(key)? {
1231 StyleValue::Number(value) => Some(*value as i32),
1232 StyleValue::String(value) => value.trim().parse::<i32>().ok(),
1233 _ => None,
1234 }
1235}
1236
1237fn stylesheet_bool(style: &StylesheetMap, key: &str) -> Option<bool> {
1238 match style.get(key)? {
1239 StyleValue::Bool(value) => Some(*value),
1240 StyleValue::String(value) => match value.trim().to_ascii_lowercase().as_str() {
1241 "true" => Some(true),
1242 "false" => Some(false),
1243 _ => None,
1244 },
1245 _ => None,
1246 }
1247}
1248
1249fn stylesheet_string<'a>(style: &'a StylesheetMap, key: &str) -> Option<&'a str> {
1250 match style.get(key)? {
1251 StyleValue::String(value) => Some(value.as_str()),
1252 _ => None,
1253 }
1254}
1255
1256fn stylesheet_color(style: &StylesheetMap, key: &str) -> Option<Color> {
1257 parse_color(stylesheet_string(style, key)?)
1258}
1259
1260fn stylesheet_font_style(style: &StylesheetMap, key: &str) -> Option<FontStyle> {
1261 match stylesheet_string(style, key)?
1262 .trim()
1263 .to_ascii_lowercase()
1264 .as_str()
1265 {
1266 "normal" => Some(FontStyle::Normal),
1267 "italic" => Some(FontStyle::Italic),
1268 "oblique" => Some(FontStyle::Oblique),
1269 _ => None,
1270 }
1271}
1272
1273fn stylesheet_font_weight(style: &StylesheetMap, key: &str) -> Option<FontWeight> {
1274 let value = match style.get(key)? {
1275 StyleValue::Number(value) => *value as u16,
1276 StyleValue::String(value) => value.trim().parse::<u16>().ok()?,
1277 _ => return None,
1278 };
1279
1280 FontWeight::new(value).ok()
1281}
1282
1283fn stylesheet_standard_font(style: &StylesheetMap, key: &str) -> Option<StandardFont> {
1284 match stylesheet_string(style, key)?.trim() {
1285 "Times-Roman" => Some(StandardFont::TimesRoman),
1286 "Times-Bold" => Some(StandardFont::TimesBold),
1287 "Times-Italic" => Some(StandardFont::TimesItalic),
1288 "Times-BoldItalic" => Some(StandardFont::TimesBoldItalic),
1289 "Helvetica" => Some(StandardFont::Helvetica),
1290 "Helvetica-Bold" => Some(StandardFont::HelveticaBold),
1291 "Helvetica-Oblique" => Some(StandardFont::HelveticaOblique),
1292 "Helvetica-BoldOblique" => Some(StandardFont::HelveticaBoldOblique),
1293 "Courier" => Some(StandardFont::Courier),
1294 "Courier-Bold" => Some(StandardFont::CourierBold),
1295 "Courier-Oblique" => Some(StandardFont::CourierOblique),
1296 "Courier-BoldOblique" => Some(StandardFont::CourierBoldOblique),
1297 "Symbol" => Some(StandardFont::Symbol),
1298 "ZapfDingbats" => Some(StandardFont::ZapfDingbats),
1299 _ => None,
1300 }
1301}
1302
1303fn font_source_family(source: &FontSource) -> Option<String> {
1304 match source {
1305 FontSource::Standard(font) => Some(font.family_name().to_string()),
1306 FontSource::Local(_) | FontSource::Remote(_) | FontSource::DataUri(_) => None,
1307 }
1308}
1309
1310fn parse_color(value: &str) -> Option<Color> {
1311 let trimmed = value.trim();
1312 match trimmed {
1313 "black" => return Some(Color::BLACK),
1314 "white" => return Some(Color::WHITE),
1315 _ => {}
1316 }
1317
1318 let hex = trimmed.strip_prefix('#')?;
1319 match hex.len() {
1320 6 => Some(Color::rgb(
1321 u8::from_str_radix(&hex[0..2], 16).ok()?,
1322 u8::from_str_radix(&hex[2..4], 16).ok()?,
1323 u8::from_str_radix(&hex[4..6], 16).ok()?,
1324 )),
1325 8 => Some(Color::rgba(
1326 u8::from_str_radix(&hex[0..2], 16).ok()?,
1327 u8::from_str_radix(&hex[2..4], 16).ok()?,
1328 u8::from_str_radix(&hex[4..6], 16).ok()?,
1329 u8::from_str_radix(&hex[6..8], 16).ok()?,
1330 )),
1331 _ => None,
1332 }
1333}
1334
1335fn resolve_replaced_size(
1336 natural: Size,
1337 fallback_width: f32,
1338 width: Option<f32>,
1339 height: Option<f32>,
1340 kind: &'static str,
1341) -> Result<Size> {
1342 let natural_width = natural.width.abs();
1343 let natural_height = natural.height.abs();
1344 let aspect_ratio = if natural_width > 0.0 && natural_height > 0.0 {
1345 Some(natural_width / natural_height)
1346 } else {
1347 None
1348 };
1349
1350 match (width, height) {
1351 (Some(width), Some(height)) => Ok(Size::new(width, height)),
1352 (Some(width), None) => {
1353 let ratio = aspect_ratio.ok_or(Error::InvalidNaturalDimensions { kind })?;
1354 Ok(Size::new(width, width / ratio))
1355 }
1356 (None, Some(height)) => {
1357 let ratio = aspect_ratio.ok_or(Error::InvalidNaturalDimensions { kind })?;
1358 Ok(Size::new(height * ratio, height))
1359 }
1360 (None, None) if natural_width > 0.0 && natural_height > 0.0 => Ok(Size::new(
1361 natural_width.min(fallback_width).max(0.0),
1362 natural_height,
1363 )),
1364 (None, None) => Err(Error::InvalidNaturalDimensions { kind }),
1365 }
1366}
1367
1368fn resolve_svg_size(svg: &SvgNode) -> Result<Size> {
1369 let view_box = svg
1370 .props
1371 .get("viewBox")
1372 .and_then(|value| parse_view_box(value));
1373 let width = svg
1374 .props
1375 .get("width")
1376 .and_then(|value| parse_numeric_dimension(value).ok())
1377 .or_else(|| view_box.map(|(_, _, width, _)| width))
1378 .unwrap_or(0.0);
1379 let height = svg
1380 .props
1381 .get("height")
1382 .and_then(|value| parse_numeric_dimension(value).ok())
1383 .or_else(|| view_box.map(|(_, _, _, height)| height))
1384 .unwrap_or(0.0);
1385
1386 if width <= 0.0 || height <= 0.0 {
1387 Err(Error::InvalidSvgDimensions)
1388 } else {
1389 Ok(Size::new(width, height))
1390 }
1391}
1392
1393fn parse_view_box(value: &str) -> Option<(f32, f32, f32, f32)> {
1394 let values: Vec<f32> = value
1395 .split(|character: char| character.is_ascii_whitespace() || character == ',')
1396 .filter(|part| !part.is_empty())
1397 .filter_map(|part| part.parse::<f32>().ok())
1398 .collect();
1399
1400 match values.as_slice() {
1401 [x, y, width, height] if *width > 0.0 && *height > 0.0 => Some((*x, *y, *width, *height)),
1402 _ => None,
1403 }
1404}
1405
1406fn parse_numeric_dimension(value: &str) -> Result<f32> {
1407 let trimmed = value.trim();
1408 let mut end = 0usize;
1409 let mut has_digit = false;
1410 let mut has_decimal_point = false;
1411
1412 for (index, character) in trimmed.char_indices() {
1413 let is_first = index == 0;
1414 let is_sign = is_first && (character == '+' || character == '-');
1415
1416 if character.is_ascii_digit() {
1417 has_digit = true;
1418 end = index + character.len_utf8();
1419 continue;
1420 }
1421 if character == '.' && !has_decimal_point {
1422 has_decimal_point = true;
1423 end = index + character.len_utf8();
1424 continue;
1425 }
1426 if is_sign {
1427 end = index + character.len_utf8();
1428 continue;
1429 }
1430 break;
1431 }
1432
1433 if !has_digit || end == 0 {
1434 return Err(Error::InvalidDimension {
1435 input: value.to_string(),
1436 });
1437 }
1438
1439 let (number, suffix) = trimmed.split_at(end);
1440 let parsed = number.parse::<f32>().map_err(|_| Error::InvalidDimension {
1441 input: value.to_string(),
1442 })?;
1443 let scaled = match suffix.trim().to_ascii_lowercase().as_str() {
1444 "" | "px" | "pt" => parsed,
1445 "in" => parsed * 72.0,
1446 "cm" => parsed * 72.0 / 2.54,
1447 "mm" => parsed * 72.0 / 25.4,
1448 _ => parsed,
1449 };
1450 Ok(scaled.abs())
1451}
1452
1453fn position_nodes(
1454 measured: Vec<MeasuredNode>,
1455 origin_x: f32,
1456 origin_y: f32,
1457 available_width: f32,
1458 page_index: usize,
1459) -> Vec<SafeLayoutNode> {
1460 let mut cursor_y = origin_y;
1461 let mut positioned = Vec::with_capacity(measured.len());
1462
1463 for node in measured {
1464 let top_margin = node.style.margin.top.value();
1465 let x = origin_x + node.style.margin.left.value();
1466 let y = cursor_y + top_margin;
1467 let frame = Bounds::from_origin_size(x, y, node.size.width, node.size.height);
1468 let content_x = x + node.style.padding.left.value();
1469 let content_y = y + node.style.padding.top.value();
1470 let content_width = (node.size.width - node.style.padding.horizontal()).max(0.0);
1471 let children = position_nodes(
1472 node.children,
1473 content_x,
1474 content_y,
1475 available_width.min(content_width),
1476 page_index,
1477 );
1478
1479 positioned.push(SafeLayoutNode {
1480 frame,
1481 content_frame: Bounds::from_origin_size(
1482 content_x,
1483 content_y,
1484 content_width,
1485 (node.size.height - node.style.padding.vertical()).max(0.0),
1486 ),
1487 style: node.style.clone(),
1488 kind: node.kind,
1489 children: sort_by_z_index(children),
1490 page_index,
1491 });
1492
1493 cursor_y += top_margin + node.size.height + node.style.margin.bottom.value();
1494 }
1495
1496 positioned
1497}
1498
1499fn sort_by_z_index(mut nodes: Vec<SafeLayoutNode>) -> Vec<SafeLayoutNode> {
1500 let mut indexed: Vec<_> = nodes.drain(..).enumerate().collect();
1501 indexed.sort_by_key(|(index, node)| (node.style.z_index, *index));
1502 indexed.into_iter().map(|(_, node)| node).collect()
1503}
1504
1505fn stylesheet_container(size: Size) -> StylesheetContainer {
1506 StylesheetContainer::new(size.width as f64, size.height as f64)
1507}
1508
1509fn validate_page_size(size: Size) -> Result<()> {
1510 if size.width <= 0.0 || size.height <= 0.0 {
1511 Err(Error::InvalidPageSize {
1512 width: size.width,
1513 height: size.height,
1514 })
1515 } else {
1516 Ok(())
1517 }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522 use super::*;
1523 use graphitepdf_image::{ImageFormat, RasterImage};
1524 use graphitepdf_svg::parse_svg;
1525
1526 fn stylesheet(entries: impl IntoIterator<Item = (&'static str, StyleValue)>) -> Stylesheet {
1527 Stylesheet::new(StyleValue::Object(
1528 entries
1529 .into_iter()
1530 .map(|(key, value)| (key.to_string(), value))
1531 .collect::<StylesheetMap>(),
1532 ))
1533 }
1534
1535 #[test]
1536 fn resolves_styles_inheritance_and_text_layout_in_pipeline_order() {
1537 let block = TextBlock::from(
1538 graphitepdf_textkit::TextSpan::new("Hello layout pipeline")
1539 .expect("text span should be valid"),
1540 );
1541 let document = Document::new().with_page(
1542 Page::new([Node::text(block)])
1543 .with_size(Size::new(220.0, 160.0))
1544 .with_style(LayoutStyle::new().with_padding(EdgeInsets::all(Pt::new(12.0))))
1545 .with_stylesheet(stylesheet([
1546 ("fontFamily", "Helvetica".into()),
1547 ("fontSize", 18.into()),
1548 ("color", "#112233".into()),
1549 ])),
1550 );
1551
1552 let layout = LayoutEngine::new()
1553 .layout_document(&document)
1554 .expect("document should layout");
1555
1556 assert_eq!(layout.pipeline(), ORDERED_PIPELINE.as_slice());
1557 assert_eq!(layout.pages().len(), 1);
1558
1559 let node = &layout.pages()[0].nodes()[0];
1560 assert_eq!(node.style().font.descriptor.family(), "Helvetica");
1561 assert_eq!(node.style().font_size, Pt::new(18.0));
1562 assert_eq!(node.style().color, Color::rgb(0x11, 0x22, 0x33));
1563 assert!(node.text_layout().is_some());
1564 assert!(node.frame.origin.x >= 12.0);
1565 assert!(node.frame.origin.y >= 12.0);
1566 }
1567
1568 #[test]
1569 fn paginates_top_level_nodes_and_resets_origins() {
1570 let page = Page::new([
1571 Node::box_node().with_style(LayoutStyle::new().with_height(Pt::new(60.0))),
1572 Node::box_node().with_style(LayoutStyle::new().with_height(Pt::new(60.0))),
1573 ])
1574 .with_size(Size::new(140.0, 100.0))
1575 .with_style(LayoutStyle::new().with_padding(EdgeInsets::all(Pt::new(10.0))));
1576
1577 let layout = LayoutEngine::new()
1578 .layout_document(&Document::new().with_page(page))
1579 .expect("layout should paginate");
1580
1581 assert_eq!(layout.pages().len(), 2);
1582 assert_eq!(layout.pages()[0].nodes().len(), 1);
1583 assert_eq!(layout.pages()[1].nodes().len(), 1);
1584 assert_eq!(layout.pages()[0].nodes()[0].frame.origin.y, 10.0);
1585 assert_eq!(layout.pages()[1].nodes()[0].frame.origin.y, 10.0);
1586 }
1587
1588 #[test]
1589 fn resolves_svg_math_and_z_index_order() {
1590 let svg = parse_svg(r#"<svg viewBox="0 0 20 10"><rect width="20" height="10"/></svg>"#);
1591 let page = Page::new([
1592 Node::svg(svg.clone()).with_style(LayoutStyle::new().with_z_index(5)),
1593 Node::math("x^2 + y^2")
1594 .with_style(LayoutStyle::new().with_width(Pt::new(60.0)).with_z_index(1)),
1595 ])
1596 .with_size(Size::new(200.0, 200.0));
1597
1598 let layout = LayoutEngine::new()
1599 .layout_document(&Document::new().with_page(page))
1600 .expect("layout should resolve math and svg");
1601
1602 let nodes = layout.pages()[0].nodes();
1603 assert_eq!(nodes.len(), 2);
1604 assert!(matches!(nodes[0].kind, SafeNodeKind::Math { .. }));
1605 assert!(matches!(nodes[1].kind, SafeNodeKind::Svg { .. }));
1606 assert_eq!(nodes[1].frame.size.width, 20.0);
1607 assert_eq!(nodes[1].frame.size.height, 10.0);
1608 assert_eq!(nodes[0].frame.size.width, 60.0);
1609 assert!(nodes[0].frame.size.height > 0.0);
1610 }
1611
1612 #[test]
1613 fn resolves_image_asset_dimensions_from_intrinsic_size() {
1614 let image = Image::Raster(RasterImage {
1615 width: 200,
1616 height: 100,
1617 data: vec![1, 2, 3, 4],
1618 format: ImageFormat::Png,
1619 key: None,
1620 });
1621 let page = Page::new([
1622 Node::image_asset(image).with_style(LayoutStyle::new().with_width(Pt::new(50.0)))
1623 ])
1624 .with_size(Size::new(200.0, 200.0));
1625
1626 let layout = LayoutEngine::new()
1627 .layout_document(&Document::new().with_page(page))
1628 .expect("layout should resolve image asset");
1629
1630 let node = &layout.pages()[0].nodes()[0];
1631 assert_eq!(node.frame.size.width, 50.0);
1632 assert_eq!(node.frame.size.height, 25.0);
1633 }
1634}