Skip to main content

graphitepdf_layout/
lib.rs

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}