Skip to main content

forme/model/
mod.rs

1//! # Document Model
2//!
3//! The input representation for the rendering engine. A document is a tree of
4//! nodes, each with a type, style properties, and children. This is designed
5//! to be easily produced by a React reconciler, an HTML parser, or direct
6//! JSON construction.
7//!
8//! The model is intentionally close to the DOM/React mental model: you have
9//! containers (View), text (Text), images (Image), and tables (Table). But
10//! there is one critical addition: **Page** is a first-class node type.
11
12use crate::style::Style;
13use serde::{Deserialize, Deserializer, Serialize};
14
15/// A complete document ready for rendering.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct Document {
19    /// The root nodes of the document. Typically one or more Page nodes,
20    /// but can also be content nodes that get auto-wrapped in pages.
21    pub children: Vec<Node>,
22
23    /// Document metadata (title, author, etc.)
24    #[serde(default)]
25    pub metadata: Metadata,
26
27    /// Default page configuration used when content overflows or when
28    /// nodes aren't explicitly wrapped in Page nodes.
29    #[serde(default)]
30    pub default_page: PageConfig,
31
32    /// Custom fonts to register before layout. Each entry contains
33    /// the font family name, base64-encoded font data, weight, and style.
34    #[serde(default)]
35    pub fonts: Vec<FontEntry>,
36
37    /// Default style applied to the root of the document tree.
38    /// Useful for setting a global `font_family`, `font_size`, `color`, etc.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub default_style: Option<crate::style::Style>,
41
42    /// Whether to produce a tagged (accessible) PDF with structure tree.
43    #[serde(default)]
44    pub tagged: bool,
45
46    /// PDF/A conformance level. When set, forces `tagged = true` for "2a".
47    #[serde(default)]
48    pub pdfa: Option<PdfAConformance>,
49
50    /// Optional JSON string to embed as an attached file in the PDF.
51    /// Enables round-tripping structured data through PDF files.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub embedded_data: Option<String>,
54}
55
56/// PDF/A conformance level.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum PdfAConformance {
59    /// PDF/A-2a: full accessibility (requires tagging).
60    #[serde(rename = "2a")]
61    A2a,
62    /// PDF/A-2b: basic compliance (visual appearance only).
63    #[serde(rename = "2b")]
64    A2b,
65}
66
67/// A custom font to register with the engine.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct FontEntry {
70    /// Font family name (e.g. "Inter", "Roboto").
71    pub family: String,
72    /// Base64-encoded font data, or a data URI (e.g. "data:font/ttf;base64,...").
73    pub src: String,
74    /// Font weight (100-900). Defaults to 400.
75    #[serde(default = "default_weight")]
76    pub weight: u32,
77    /// Whether this is an italic variant.
78    #[serde(default)]
79    pub italic: bool,
80}
81
82fn default_weight() -> u32 {
83    400
84}
85
86/// Document metadata embedded in the PDF.
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct Metadata {
89    pub title: Option<String>,
90    pub author: Option<String>,
91    pub subject: Option<String>,
92    pub creator: Option<String>,
93    /// Document language (BCP 47 tag, e.g. "en-US"). Emitted as /Lang in the PDF Catalog.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub lang: Option<String>,
96}
97
98/// Configuration for a page: size, margins, orientation.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PageConfig {
101    /// Page size. Defaults to A4.
102    #[serde(default = "PageSize::default")]
103    pub size: PageSize,
104
105    /// Page margins in points (1/72 inch).
106    #[serde(default)]
107    pub margin: Edges,
108
109    /// Whether this page auto-wraps content that overflows.
110    #[serde(default = "default_true")]
111    pub wrap: bool,
112}
113
114impl Default for PageConfig {
115    fn default() -> Self {
116        Self {
117            size: PageSize::A4,
118            margin: Edges::uniform(54.0), // ~0.75 inch
119            wrap: true,
120        }
121    }
122}
123
124fn default_true() -> bool {
125    true
126}
127
128/// Standard page sizes in points.
129#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
130pub enum PageSize {
131    #[default]
132    A4,
133    A3,
134    A5,
135    Letter,
136    Legal,
137    Tabloid,
138    Custom {
139        width: f64,
140        height: f64,
141    },
142}
143
144impl PageSize {
145    /// Returns (width, height) in points.
146    pub fn dimensions(&self) -> (f64, f64) {
147        match self {
148            PageSize::A4 => (595.28, 841.89),
149            PageSize::A3 => (841.89, 1190.55),
150            PageSize::A5 => (419.53, 595.28),
151            PageSize::Letter => (612.0, 792.0),
152            PageSize::Legal => (612.0, 1008.0),
153            PageSize::Tabloid => (792.0, 1224.0),
154            PageSize::Custom { width, height } => (*width, *height),
155        }
156    }
157}
158
159/// Edge values (top, right, bottom, left) used for padding and page margins.
160#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
161pub struct Edges {
162    pub top: f64,
163    pub right: f64,
164    pub bottom: f64,
165    pub left: f64,
166}
167
168/// A margin edge value — either a fixed point value or auto.
169#[derive(Debug, Clone, Copy, Serialize)]
170pub enum EdgeValue {
171    Pt(f64),
172    Auto,
173}
174
175impl Default for EdgeValue {
176    fn default() -> Self {
177        EdgeValue::Pt(0.0)
178    }
179}
180
181impl EdgeValue {
182    /// Resolve to a concrete value, treating Auto as 0.
183    pub fn resolve(&self) -> f64 {
184        match self {
185            EdgeValue::Pt(v) => *v,
186            EdgeValue::Auto => 0.0,
187        }
188    }
189
190    /// Whether this edge is auto.
191    pub fn is_auto(&self) -> bool {
192        matches!(self, EdgeValue::Auto)
193    }
194}
195
196impl<'de> Deserialize<'de> for EdgeValue {
197    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198    where
199        D: Deserializer<'de>,
200    {
201        use serde::de;
202
203        struct EdgeValueVisitor;
204
205        impl<'de> de::Visitor<'de> for EdgeValueVisitor {
206            type Value = EdgeValue;
207
208            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
209                f.write_str("a number or the string \"auto\"")
210            }
211
212            fn visit_f64<E: de::Error>(self, v: f64) -> Result<EdgeValue, E> {
213                Ok(EdgeValue::Pt(v))
214            }
215
216            fn visit_i64<E: de::Error>(self, v: i64) -> Result<EdgeValue, E> {
217                Ok(EdgeValue::Pt(v as f64))
218            }
219
220            fn visit_u64<E: de::Error>(self, v: u64) -> Result<EdgeValue, E> {
221                Ok(EdgeValue::Pt(v as f64))
222            }
223
224            fn visit_str<E: de::Error>(self, v: &str) -> Result<EdgeValue, E> {
225                if v == "auto" {
226                    Ok(EdgeValue::Auto)
227                } else {
228                    Err(de::Error::invalid_value(de::Unexpected::Str(v), &self))
229                }
230            }
231        }
232
233        deserializer.deserialize_any(EdgeValueVisitor)
234    }
235}
236
237/// Margin edges that support auto values.
238#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
239pub struct MarginEdges {
240    pub top: EdgeValue,
241    pub right: EdgeValue,
242    pub bottom: EdgeValue,
243    pub left: EdgeValue,
244}
245
246impl MarginEdges {
247    /// Sum of resolved (non-auto) horizontal margins.
248    pub fn horizontal(&self) -> f64 {
249        self.left.resolve() + self.right.resolve()
250    }
251
252    /// Sum of resolved (non-auto) vertical margins.
253    pub fn vertical(&self) -> f64 {
254        self.top.resolve() + self.bottom.resolve()
255    }
256
257    /// Whether any horizontal margin is auto.
258    pub fn has_auto_horizontal(&self) -> bool {
259        self.left.is_auto() || self.right.is_auto()
260    }
261
262    /// Whether any vertical margin is auto.
263    pub fn has_auto_vertical(&self) -> bool {
264        self.top.is_auto() || self.bottom.is_auto()
265    }
266
267    /// Convert from plain Edges (all Pt values).
268    pub fn from_edges(e: Edges) -> Self {
269        MarginEdges {
270            top: EdgeValue::Pt(e.top),
271            right: EdgeValue::Pt(e.right),
272            bottom: EdgeValue::Pt(e.bottom),
273            left: EdgeValue::Pt(e.left),
274        }
275    }
276
277    /// Convert to plain Edges, resolving auto to 0.
278    pub fn to_edges(&self) -> Edges {
279        Edges {
280            top: self.top.resolve(),
281            right: self.right.resolve(),
282            bottom: self.bottom.resolve(),
283            left: self.left.resolve(),
284        }
285    }
286}
287
288impl Edges {
289    pub fn uniform(v: f64) -> Self {
290        Self {
291            top: v,
292            right: v,
293            bottom: v,
294            left: v,
295        }
296    }
297
298    pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
299        Self {
300            top: vertical,
301            right: horizontal,
302            bottom: vertical,
303            left: horizontal,
304        }
305    }
306
307    pub fn horizontal(&self) -> f64 {
308        self.left + self.right
309    }
310
311    pub fn vertical(&self) -> f64 {
312        self.top + self.bottom
313    }
314}
315
316/// A node in the document tree.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct Node {
320    /// What kind of node this is.
321    pub kind: NodeKind,
322
323    /// Style properties for this node.
324    #[serde(default)]
325    pub style: Style,
326
327    /// Child nodes.
328    #[serde(default)]
329    pub children: Vec<Node>,
330
331    /// A unique identifier for this node (optional, useful for debugging).
332    #[serde(default)]
333    pub id: Option<String>,
334
335    /// Source code location for click-to-source in the dev inspector.
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub source_location: Option<SourceLocation>,
338
339    /// Bookmark title for this node (creates a PDF outline entry).
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub bookmark: Option<String>,
342
343    /// Optional hyperlink URL for this node (creates a PDF link annotation).
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub href: Option<String>,
346
347    /// Optional alt text for images and SVGs (accessibility).
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub alt: Option<String>,
350}
351
352/// The different kinds of nodes in the document tree.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(tag = "type")]
355pub enum NodeKind {
356    /// A page boundary. Content inside flows according to page config.
357    Page {
358        #[serde(default)]
359        config: PageConfig,
360    },
361
362    /// A generic container, analogous to a <div> or React <View>.
363    View,
364
365    /// A text node with string content.
366    Text {
367        content: String,
368        /// Optional hyperlink URL.
369        #[serde(default, skip_serializing_if = "Option::is_none")]
370        href: Option<String>,
371        /// Inline styled runs. When non-empty, `content` is ignored.
372        #[serde(default, skip_serializing_if = "Vec::is_empty")]
373        runs: Vec<TextRun>,
374    },
375
376    /// An image node.
377    Image {
378        /// Base64-encoded image data, or a file path.
379        src: String,
380        /// Image width in points (optional, will use intrinsic if not set).
381        width: Option<f64>,
382        /// Image height in points (optional, will use intrinsic if not set).
383        height: Option<f64>,
384    },
385
386    /// A table container. Children should be TableRow nodes.
387    Table {
388        /// Column width definitions. If omitted, columns distribute evenly.
389        #[serde(default)]
390        columns: Vec<ColumnDef>,
391    },
392
393    /// A row inside a Table.
394    TableRow {
395        /// If true, this row repeats at the top of each page when the table
396        /// breaks across pages. This is the killer feature.
397        #[serde(default)]
398        is_header: bool,
399    },
400
401    /// A cell inside a TableRow.
402    TableCell {
403        /// Column span.
404        #[serde(default = "default_one")]
405        col_span: u32,
406        /// Row span.
407        #[serde(default = "default_one")]
408        row_span: u32,
409    },
410
411    /// A fixed element that repeats on every page (headers, footers, page numbers).
412    Fixed {
413        /// Where to place this element on the page.
414        position: FixedPosition,
415    },
416
417    /// An explicit page break.
418    PageBreak,
419
420    /// An SVG element rendered as vector graphics.
421    Svg {
422        /// Display width in points.
423        width: f64,
424        /// Display height in points.
425        height: f64,
426        /// Optional viewBox (e.g. "0 0 100 100").
427        #[serde(default, skip_serializing_if = "Option::is_none")]
428        view_box: Option<String>,
429        /// SVG markup content (the inner XML).
430        content: String,
431    },
432
433    /// A canvas drawing primitive with arbitrary vector operations.
434    Canvas {
435        /// Display width in points.
436        width: f64,
437        /// Display height in points.
438        height: f64,
439        /// Drawing operations to execute.
440        operations: Vec<CanvasOp>,
441    },
442
443    /// A 1D barcode rendered as vector rectangles.
444    Barcode {
445        /// The data to encode.
446        data: String,
447        /// Barcode format (Code128, Code39, EAN13, EAN8, Codabar). Default: Code128.
448        #[serde(default)]
449        format: crate::barcode::BarcodeFormat,
450        /// Width in points. Defaults to available width.
451        #[serde(default, skip_serializing_if = "Option::is_none")]
452        width: Option<f64>,
453        /// Height in points. Default: 60.
454        #[serde(default = "default_barcode_height")]
455        height: f64,
456    },
457
458    /// A QR code rendered as vector rectangles.
459    QrCode {
460        /// The data to encode (URL, text, etc.).
461        data: String,
462        /// Display size in points (QR codes are always square).
463        /// Defaults to available width if omitted.
464        #[serde(default, skip_serializing_if = "Option::is_none")]
465        size: Option<f64>,
466    },
467
468    /// A bar chart rendered as native vector graphics.
469    BarChart {
470        /// Data points with labels and values.
471        data: Vec<ChartDataPoint>,
472        /// Chart width in points.
473        width: f64,
474        /// Chart height in points.
475        height: f64,
476        /// Bar color (hex string). Defaults to "#1a365d".
477        #[serde(default, skip_serializing_if = "Option::is_none")]
478        color: Option<String>,
479        /// Show X-axis labels below bars.
480        #[serde(default = "default_true")]
481        show_labels: bool,
482        /// Show value labels above bars.
483        #[serde(default)]
484        show_values: bool,
485        /// Show horizontal grid lines.
486        #[serde(default)]
487        show_grid: bool,
488        /// Optional chart title.
489        #[serde(default, skip_serializing_if = "Option::is_none")]
490        title: Option<String>,
491    },
492
493    /// A line chart rendered as native vector graphics.
494    LineChart {
495        /// Data series (each with name, data points, optional color).
496        series: Vec<ChartSeries>,
497        /// X-axis labels.
498        labels: Vec<String>,
499        /// Chart width in points.
500        width: f64,
501        /// Chart height in points.
502        height: f64,
503        /// Show dots at data points.
504        #[serde(default)]
505        show_points: bool,
506        /// Show horizontal grid lines.
507        #[serde(default)]
508        show_grid: bool,
509        /// Optional chart title.
510        #[serde(default, skip_serializing_if = "Option::is_none")]
511        title: Option<String>,
512    },
513
514    /// A pie chart rendered as native vector graphics.
515    PieChart {
516        /// Data points with labels, values, and optional colors.
517        data: Vec<ChartDataPoint>,
518        /// Chart width in points.
519        width: f64,
520        /// Chart height in points.
521        height: f64,
522        /// Whether to render as donut (hollow center).
523        #[serde(default)]
524        donut: bool,
525        /// Show legend.
526        #[serde(default)]
527        show_legend: bool,
528        /// Optional chart title.
529        #[serde(default, skip_serializing_if = "Option::is_none")]
530        title: Option<String>,
531    },
532
533    /// An area chart rendered as native vector graphics.
534    AreaChart {
535        /// Data series (each with name, data points, optional color).
536        series: Vec<ChartSeries>,
537        /// X-axis labels.
538        labels: Vec<String>,
539        /// Chart width in points.
540        width: f64,
541        /// Chart height in points.
542        height: f64,
543        /// Show horizontal grid lines.
544        #[serde(default)]
545        show_grid: bool,
546        /// Optional chart title.
547        #[serde(default, skip_serializing_if = "Option::is_none")]
548        title: Option<String>,
549    },
550
551    /// A dot plot (scatter plot) rendered as native vector graphics.
552    DotPlot {
553        /// Groups of data points.
554        groups: Vec<DotPlotGroup>,
555        /// Chart width in points.
556        width: f64,
557        /// Chart height in points.
558        height: f64,
559        /// Minimum X value. Auto-computed if not set.
560        #[serde(default, skip_serializing_if = "Option::is_none")]
561        x_min: Option<f64>,
562        /// Maximum X value. Auto-computed if not set.
563        #[serde(default, skip_serializing_if = "Option::is_none")]
564        x_max: Option<f64>,
565        /// Minimum Y value. Auto-computed if not set.
566        #[serde(default, skip_serializing_if = "Option::is_none")]
567        y_min: Option<f64>,
568        /// Maximum Y value. Auto-computed if not set.
569        #[serde(default, skip_serializing_if = "Option::is_none")]
570        y_max: Option<f64>,
571        /// X-axis label.
572        #[serde(default, skip_serializing_if = "Option::is_none")]
573        x_label: Option<String>,
574        /// Y-axis label.
575        #[serde(default, skip_serializing_if = "Option::is_none")]
576        y_label: Option<String>,
577        /// Show legend.
578        #[serde(default)]
579        show_legend: bool,
580        /// Dot radius in points.
581        #[serde(default = "default_dot_size")]
582        dot_size: f64,
583    },
584
585    /// A watermark rendered as rotated text behind page content.
586    Watermark {
587        /// The watermark text (e.g. "DRAFT", "CONFIDENTIAL").
588        text: String,
589        /// Font size in points. Default: 60.
590        #[serde(default = "default_watermark_font_size")]
591        font_size: f64,
592        /// Rotation angle in degrees (negative = counterclockwise). Default: -45.
593        #[serde(default = "default_watermark_angle")]
594        angle: f64,
595    },
596}
597
598/// A data point for bar charts and pie charts.
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct ChartDataPoint {
601    pub label: String,
602    pub value: f64,
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub color: Option<String>,
605}
606
607/// A data series for line charts and area charts.
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct ChartSeries {
610    pub name: String,
611    pub data: Vec<f64>,
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub color: Option<String>,
614}
615
616/// A group of data points for dot plots.
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct DotPlotGroup {
619    pub name: String,
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub color: Option<String>,
622    pub data: Vec<(f64, f64)>,
623}
624
625/// A canvas drawing operation.
626#[derive(Debug, Clone, Serialize, Deserialize)]
627#[serde(tag = "op")]
628pub enum CanvasOp {
629    MoveTo {
630        x: f64,
631        y: f64,
632    },
633    LineTo {
634        x: f64,
635        y: f64,
636    },
637    BezierCurveTo {
638        cp1x: f64,
639        cp1y: f64,
640        cp2x: f64,
641        cp2y: f64,
642        x: f64,
643        y: f64,
644    },
645    QuadraticCurveTo {
646        cpx: f64,
647        cpy: f64,
648        x: f64,
649        y: f64,
650    },
651    ClosePath,
652    Rect {
653        x: f64,
654        y: f64,
655        width: f64,
656        height: f64,
657    },
658    Circle {
659        cx: f64,
660        cy: f64,
661        r: f64,
662    },
663    Ellipse {
664        cx: f64,
665        cy: f64,
666        rx: f64,
667        ry: f64,
668    },
669    Arc {
670        cx: f64,
671        cy: f64,
672        r: f64,
673        start_angle: f64,
674        end_angle: f64,
675        #[serde(default)]
676        counterclockwise: bool,
677    },
678    Stroke,
679    Fill,
680    FillAndStroke,
681    SetFillColor {
682        r: f64,
683        g: f64,
684        b: f64,
685    },
686    SetStrokeColor {
687        r: f64,
688        g: f64,
689        b: f64,
690    },
691    SetLineWidth {
692        width: f64,
693    },
694    SetLineCap {
695        cap: u32,
696    },
697    SetLineJoin {
698        join: u32,
699    },
700    Save,
701    Restore,
702}
703
704/// An inline styled run within a Text node.
705#[derive(Debug, Clone, Serialize, Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct TextRun {
708    pub content: String,
709    #[serde(default)]
710    pub style: crate::style::Style,
711    #[serde(default, skip_serializing_if = "Option::is_none")]
712    pub href: Option<String>,
713}
714
715/// Positioning mode for a node.
716#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
717pub enum Position {
718    #[default]
719    Relative,
720    Absolute,
721}
722
723fn default_one() -> u32 {
724    1
725}
726
727fn default_barcode_height() -> f64 {
728    60.0
729}
730
731fn default_dot_size() -> f64 {
732    4.0
733}
734
735fn default_watermark_font_size() -> f64 {
736    60.0
737}
738
739fn default_watermark_angle() -> f64 {
740    -45.0
741}
742
743/// Column definition for tables.
744#[derive(Debug, Clone, Serialize, Deserialize)]
745pub struct ColumnDef {
746    /// Width as a fraction (0.0-1.0) of available table width, or fixed points.
747    pub width: ColumnWidth,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub enum ColumnWidth {
752    /// Fraction of available width (0.0-1.0).
753    Fraction(f64),
754    /// Fixed width in points.
755    Fixed(f64),
756    /// Distribute remaining space evenly among Auto columns.
757    Auto,
758}
759
760/// Where a fixed element is placed on the page.
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub enum FixedPosition {
763    /// Top of the content area (below margin).
764    Header,
765    /// Bottom of the content area (above margin).
766    Footer,
767}
768
769/// Source code location for click-to-source in the dev server inspector.
770#[derive(Debug, Clone, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772pub struct SourceLocation {
773    pub file: String,
774    pub line: u32,
775    pub column: u32,
776}
777
778impl Node {
779    /// Create a View node with children.
780    pub fn view(style: Style, children: Vec<Node>) -> Self {
781        Self {
782            kind: NodeKind::View,
783            style,
784            children,
785            id: None,
786            source_location: None,
787            bookmark: None,
788            href: None,
789            alt: None,
790        }
791    }
792
793    /// Create a Text node.
794    pub fn text(content: &str, style: Style) -> Self {
795        Self {
796            kind: NodeKind::Text {
797                content: content.to_string(),
798                href: None,
799                runs: vec![],
800            },
801            style,
802            children: vec![],
803            id: None,
804            source_location: None,
805            bookmark: None,
806            href: None,
807            alt: None,
808        }
809    }
810
811    /// Create a Page node.
812    pub fn page(config: PageConfig, style: Style, children: Vec<Node>) -> Self {
813        Self {
814            kind: NodeKind::Page { config },
815            style,
816            children,
817            id: None,
818            source_location: None,
819            bookmark: None,
820            href: None,
821            alt: None,
822        }
823    }
824
825    /// Is this node breakable across pages?
826    pub fn is_breakable(&self) -> bool {
827        match &self.kind {
828            NodeKind::View | NodeKind::Table { .. } | NodeKind::Text { .. } => {
829                self.style.wrap.unwrap_or(true)
830            }
831            NodeKind::TableRow { .. } => true,
832            NodeKind::Image { .. } => false,
833            NodeKind::Svg { .. } => false,
834            NodeKind::Canvas { .. } => false,
835            NodeKind::Barcode { .. } => false,
836            NodeKind::QrCode { .. } => false,
837            NodeKind::BarChart { .. } => false,
838            NodeKind::LineChart { .. } => false,
839            NodeKind::PieChart { .. } => false,
840            NodeKind::AreaChart { .. } => false,
841            NodeKind::DotPlot { .. } => false,
842            NodeKind::Watermark { .. } => false,
843            NodeKind::PageBreak => false,
844            NodeKind::Fixed { .. } => false,
845            NodeKind::Page { .. } => true,
846            NodeKind::TableCell { .. } => true,
847        }
848    }
849}