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    /// When true, the PDF claims PDF/UA-1 conformance. Forces `tagged = true`.
51    #[serde(default)]
52    pub pdf_ua: bool,
53
54    /// Optional JSON string to embed as an attached file in the PDF.
55    /// Enables round-tripping structured data through PDF files.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub embedded_data: Option<String>,
58
59    /// When true, form field values are rendered as static content and no
60    /// interactive AcroForm widgets are emitted. The resulting PDF has no
61    /// fillable fields.
62    #[serde(default)]
63    pub flatten_forms: bool,
64
65    /// Digital signature configuration. When set, the rendered PDF is signed
66    /// with the specified X.509 certificate and RSA private key.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub signature: Option<SignatureConfig>,
69}
70
71/// PDF/A conformance level.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum PdfAConformance {
74    /// PDF/A-2a: full accessibility (requires tagging).
75    #[serde(rename = "2a")]
76    A2a,
77    /// PDF/A-2b: basic compliance (visual appearance only).
78    #[serde(rename = "2b")]
79    A2b,
80}
81
82/// Configuration for digitally signing a PDF with an X.509 certificate.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct SignatureConfig {
86    /// PEM-encoded X.509 certificate.
87    pub certificate_pem: String,
88    /// PEM-encoded RSA private key (PKCS#8).
89    pub private_key_pem: String,
90    /// Reason for signing (e.g. "Approved").
91    #[serde(default)]
92    pub reason: Option<String>,
93    /// Location of signing (e.g. "New York, NY").
94    #[serde(default)]
95    pub location: Option<String>,
96    /// Contact info for the signer.
97    #[serde(default)]
98    pub contact: Option<String>,
99    /// Whether to show a visible signature annotation on the page.
100    #[serde(default)]
101    pub visible: bool,
102    /// X coordinate in points for visible signature.
103    #[serde(default)]
104    pub x: Option<f64>,
105    /// Y coordinate in points for visible signature.
106    #[serde(default)]
107    pub y: Option<f64>,
108    /// Width in points for visible signature.
109    #[serde(default)]
110    pub width: Option<f64>,
111    /// Height in points for visible signature.
112    #[serde(default)]
113    pub height: Option<f64>,
114}
115
116/// A custom font to register with the engine.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct FontEntry {
119    /// Font family name (e.g. "Inter", "Roboto").
120    pub family: String,
121    /// Base64-encoded font data, or a data URI (e.g. "data:font/ttf;base64,...").
122    pub src: String,
123    /// Font weight (100-900). Defaults to 400.
124    #[serde(default = "default_weight")]
125    pub weight: u32,
126    /// Whether this is an italic variant.
127    #[serde(default)]
128    pub italic: bool,
129}
130
131fn default_weight() -> u32 {
132    400
133}
134
135/// Document metadata embedded in the PDF.
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct Metadata {
138    pub title: Option<String>,
139    pub author: Option<String>,
140    pub subject: Option<String>,
141    pub creator: Option<String>,
142    /// Document language (BCP 47 tag, e.g. "en-US"). Emitted as /Lang in the PDF Catalog.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub lang: Option<String>,
145}
146
147/// Configuration for a page: size, margins, orientation.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct PageConfig {
150    /// Page size. Defaults to A4.
151    #[serde(default = "PageSize::default")]
152    pub size: PageSize,
153
154    /// Page margins in points (1/72 inch).
155    #[serde(default)]
156    pub margin: Edges,
157
158    /// Whether this page auto-wraps content that overflows.
159    #[serde(default = "default_true")]
160    pub wrap: bool,
161}
162
163impl Default for PageConfig {
164    fn default() -> Self {
165        Self {
166            size: PageSize::A4,
167            margin: Edges::uniform(54.0), // ~0.75 inch
168            wrap: true,
169        }
170    }
171}
172
173fn default_true() -> bool {
174    true
175}
176
177/// Standard page sizes in points.
178#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
179pub enum PageSize {
180    #[default]
181    A4,
182    A3,
183    A5,
184    Letter,
185    Legal,
186    Tabloid,
187    Custom {
188        width: f64,
189        height: f64,
190    },
191}
192
193impl PageSize {
194    /// Returns (width, height) in points.
195    pub fn dimensions(&self) -> (f64, f64) {
196        match self {
197            PageSize::A4 => (595.28, 841.89),
198            PageSize::A3 => (841.89, 1190.55),
199            PageSize::A5 => (419.53, 595.28),
200            PageSize::Letter => (612.0, 792.0),
201            PageSize::Legal => (612.0, 1008.0),
202            PageSize::Tabloid => (792.0, 1224.0),
203            PageSize::Custom { width, height } => (*width, *height),
204        }
205    }
206}
207
208/// Edge values (top, right, bottom, left) used for padding and page margins.
209#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
210pub struct Edges {
211    pub top: f64,
212    pub right: f64,
213    pub bottom: f64,
214    pub left: f64,
215}
216
217/// A margin edge value — either a fixed point value or auto.
218#[derive(Debug, Clone, Copy, Serialize)]
219pub enum EdgeValue {
220    Pt(f64),
221    Auto,
222}
223
224impl Default for EdgeValue {
225    fn default() -> Self {
226        EdgeValue::Pt(0.0)
227    }
228}
229
230impl EdgeValue {
231    /// Resolve to a concrete value, treating Auto as 0.
232    pub fn resolve(&self) -> f64 {
233        match self {
234            EdgeValue::Pt(v) => *v,
235            EdgeValue::Auto => 0.0,
236        }
237    }
238
239    /// Whether this edge is auto.
240    pub fn is_auto(&self) -> bool {
241        matches!(self, EdgeValue::Auto)
242    }
243}
244
245impl<'de> Deserialize<'de> for EdgeValue {
246    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247    where
248        D: Deserializer<'de>,
249    {
250        use serde::de;
251
252        struct EdgeValueVisitor;
253
254        impl<'de> de::Visitor<'de> for EdgeValueVisitor {
255            type Value = EdgeValue;
256
257            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
258                f.write_str("a number or the string \"auto\"")
259            }
260
261            fn visit_f64<E: de::Error>(self, v: f64) -> Result<EdgeValue, E> {
262                Ok(EdgeValue::Pt(v))
263            }
264
265            fn visit_i64<E: de::Error>(self, v: i64) -> Result<EdgeValue, E> {
266                Ok(EdgeValue::Pt(v as f64))
267            }
268
269            fn visit_u64<E: de::Error>(self, v: u64) -> Result<EdgeValue, E> {
270                Ok(EdgeValue::Pt(v as f64))
271            }
272
273            fn visit_str<E: de::Error>(self, v: &str) -> Result<EdgeValue, E> {
274                if v == "auto" {
275                    Ok(EdgeValue::Auto)
276                } else {
277                    Err(de::Error::invalid_value(de::Unexpected::Str(v), &self))
278                }
279            }
280        }
281
282        deserializer.deserialize_any(EdgeValueVisitor)
283    }
284}
285
286/// Margin edges that support auto values.
287#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
288pub struct MarginEdges {
289    pub top: EdgeValue,
290    pub right: EdgeValue,
291    pub bottom: EdgeValue,
292    pub left: EdgeValue,
293}
294
295impl MarginEdges {
296    /// Sum of resolved (non-auto) horizontal margins.
297    pub fn horizontal(&self) -> f64 {
298        self.left.resolve() + self.right.resolve()
299    }
300
301    /// Sum of resolved (non-auto) vertical margins.
302    pub fn vertical(&self) -> f64 {
303        self.top.resolve() + self.bottom.resolve()
304    }
305
306    /// Whether any horizontal margin is auto.
307    pub fn has_auto_horizontal(&self) -> bool {
308        self.left.is_auto() || self.right.is_auto()
309    }
310
311    /// Whether any vertical margin is auto.
312    pub fn has_auto_vertical(&self) -> bool {
313        self.top.is_auto() || self.bottom.is_auto()
314    }
315
316    /// Convert from plain Edges (all Pt values).
317    pub fn from_edges(e: Edges) -> Self {
318        MarginEdges {
319            top: EdgeValue::Pt(e.top),
320            right: EdgeValue::Pt(e.right),
321            bottom: EdgeValue::Pt(e.bottom),
322            left: EdgeValue::Pt(e.left),
323        }
324    }
325
326    /// Convert to plain Edges, resolving auto to 0.
327    pub fn to_edges(&self) -> Edges {
328        Edges {
329            top: self.top.resolve(),
330            right: self.right.resolve(),
331            bottom: self.bottom.resolve(),
332            left: self.left.resolve(),
333        }
334    }
335}
336
337impl Edges {
338    pub fn uniform(v: f64) -> Self {
339        Self {
340            top: v,
341            right: v,
342            bottom: v,
343            left: v,
344        }
345    }
346
347    pub fn symmetric(vertical: f64, horizontal: f64) -> Self {
348        Self {
349            top: vertical,
350            right: horizontal,
351            bottom: vertical,
352            left: horizontal,
353        }
354    }
355
356    pub fn horizontal(&self) -> f64 {
357        self.left + self.right
358    }
359
360    pub fn vertical(&self) -> f64 {
361        self.top + self.bottom
362    }
363}
364
365/// A node in the document tree.
366#[derive(Debug, Clone, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct Node {
369    /// What kind of node this is.
370    pub kind: NodeKind,
371
372    /// Style properties for this node.
373    #[serde(default)]
374    pub style: Style,
375
376    /// Child nodes.
377    #[serde(default)]
378    pub children: Vec<Node>,
379
380    /// A unique identifier for this node (optional, useful for debugging).
381    #[serde(default)]
382    pub id: Option<String>,
383
384    /// Source code location for click-to-source in the dev inspector.
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub source_location: Option<SourceLocation>,
387
388    /// Bookmark title for this node (creates a PDF outline entry).
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub bookmark: Option<String>,
391
392    /// Optional hyperlink URL for this node (creates a PDF link annotation).
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub href: Option<String>,
395
396    /// Optional alt text for images and SVGs (accessibility).
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub alt: Option<String>,
399}
400
401/// The different kinds of nodes in the document tree.
402#[derive(Debug, Clone, Serialize, Deserialize)]
403#[serde(tag = "type")]
404pub enum NodeKind {
405    /// A page boundary. Content inside flows according to page config.
406    Page {
407        #[serde(default)]
408        config: PageConfig,
409    },
410
411    /// A generic container, analogous to a <div> or React <View>.
412    View,
413
414    /// A text node with string content.
415    Text {
416        content: String,
417        /// Optional hyperlink URL.
418        #[serde(default, skip_serializing_if = "Option::is_none")]
419        href: Option<String>,
420        /// Inline styled runs. When non-empty, `content` is ignored.
421        #[serde(default, skip_serializing_if = "Vec::is_empty")]
422        runs: Vec<TextRun>,
423    },
424
425    /// An image node.
426    Image {
427        /// Base64-encoded image data, or a file path.
428        src: String,
429        /// Image width in points (optional, will use intrinsic if not set).
430        width: Option<f64>,
431        /// Image height in points (optional, will use intrinsic if not set).
432        height: Option<f64>,
433    },
434
435    /// A table container. Children should be TableRow nodes.
436    Table {
437        /// Column width definitions. If omitted, columns distribute evenly.
438        #[serde(default)]
439        columns: Vec<ColumnDef>,
440    },
441
442    /// A row inside a Table.
443    TableRow {
444        /// If true, this row repeats at the top of each page when the table
445        /// breaks across pages. This is the killer feature.
446        #[serde(default)]
447        is_header: bool,
448    },
449
450    /// A cell inside a TableRow.
451    TableCell {
452        /// Column span.
453        #[serde(default = "default_one")]
454        col_span: u32,
455        /// Row span.
456        #[serde(default = "default_one")]
457        row_span: u32,
458    },
459
460    /// A fixed element that repeats on every page (headers, footers, page numbers).
461    Fixed {
462        /// Where to place this element on the page.
463        position: FixedPosition,
464    },
465
466    /// An explicit page break.
467    PageBreak,
468
469    /// An SVG element rendered as vector graphics.
470    Svg {
471        /// Display width in points.
472        width: f64,
473        /// Display height in points.
474        height: f64,
475        /// Optional viewBox (e.g. "0 0 100 100").
476        #[serde(default, skip_serializing_if = "Option::is_none")]
477        view_box: Option<String>,
478        /// SVG markup content (the inner XML).
479        content: String,
480    },
481
482    /// A canvas drawing primitive with arbitrary vector operations.
483    Canvas {
484        /// Display width in points.
485        width: f64,
486        /// Display height in points.
487        height: f64,
488        /// Drawing operations to execute.
489        operations: Vec<CanvasOp>,
490    },
491
492    /// A 1D barcode rendered as vector rectangles.
493    Barcode {
494        /// The data to encode.
495        data: String,
496        /// Barcode format (Code128, Code39, EAN13, EAN8, Codabar). Default: Code128.
497        #[serde(default)]
498        format: crate::barcode::BarcodeFormat,
499        /// Width in points. Defaults to available width.
500        #[serde(default, skip_serializing_if = "Option::is_none")]
501        width: Option<f64>,
502        /// Height in points. Default: 60.
503        #[serde(default = "default_barcode_height")]
504        height: f64,
505    },
506
507    /// A QR code rendered as vector rectangles.
508    QrCode {
509        /// The data to encode (URL, text, etc.).
510        data: String,
511        /// Display size in points (QR codes are always square).
512        /// Defaults to available width if omitted.
513        #[serde(default, skip_serializing_if = "Option::is_none")]
514        size: Option<f64>,
515    },
516
517    /// A bar chart rendered as native vector graphics.
518    BarChart {
519        /// Data points with labels and values.
520        data: Vec<ChartDataPoint>,
521        /// Chart width in points.
522        width: f64,
523        /// Chart height in points.
524        height: f64,
525        /// Bar color (hex string). Defaults to "#1a365d".
526        #[serde(default, skip_serializing_if = "Option::is_none")]
527        color: Option<String>,
528        /// Show X-axis labels below bars.
529        #[serde(default = "default_true")]
530        show_labels: bool,
531        /// Show value labels above bars.
532        #[serde(default)]
533        show_values: bool,
534        /// Show horizontal grid lines.
535        #[serde(default)]
536        show_grid: bool,
537        /// Optional chart title.
538        #[serde(default, skip_serializing_if = "Option::is_none")]
539        title: Option<String>,
540    },
541
542    /// A line chart rendered as native vector graphics.
543    LineChart {
544        /// Data series (each with name, data points, optional color).
545        series: Vec<ChartSeries>,
546        /// X-axis labels.
547        labels: Vec<String>,
548        /// Chart width in points.
549        width: f64,
550        /// Chart height in points.
551        height: f64,
552        /// Show dots at data points.
553        #[serde(default)]
554        show_points: bool,
555        /// Show horizontal grid lines.
556        #[serde(default)]
557        show_grid: bool,
558        /// Optional chart title.
559        #[serde(default, skip_serializing_if = "Option::is_none")]
560        title: Option<String>,
561    },
562
563    /// A pie chart rendered as native vector graphics.
564    PieChart {
565        /// Data points with labels, values, and optional colors.
566        data: Vec<ChartDataPoint>,
567        /// Chart width in points.
568        width: f64,
569        /// Chart height in points.
570        height: f64,
571        /// Whether to render as donut (hollow center).
572        #[serde(default)]
573        donut: bool,
574        /// Show legend.
575        #[serde(default)]
576        show_legend: bool,
577        /// Optional chart title.
578        #[serde(default, skip_serializing_if = "Option::is_none")]
579        title: Option<String>,
580    },
581
582    /// An area chart rendered as native vector graphics.
583    AreaChart {
584        /// Data series (each with name, data points, optional color).
585        series: Vec<ChartSeries>,
586        /// X-axis labels.
587        labels: Vec<String>,
588        /// Chart width in points.
589        width: f64,
590        /// Chart height in points.
591        height: f64,
592        /// Show horizontal grid lines.
593        #[serde(default)]
594        show_grid: bool,
595        /// Optional chart title.
596        #[serde(default, skip_serializing_if = "Option::is_none")]
597        title: Option<String>,
598    },
599
600    /// A dot plot (scatter plot) rendered as native vector graphics.
601    DotPlot {
602        /// Groups of data points.
603        groups: Vec<DotPlotGroup>,
604        /// Chart width in points.
605        width: f64,
606        /// Chart height in points.
607        height: f64,
608        /// Minimum X value. Auto-computed if not set.
609        #[serde(default, skip_serializing_if = "Option::is_none")]
610        x_min: Option<f64>,
611        /// Maximum X value. Auto-computed if not set.
612        #[serde(default, skip_serializing_if = "Option::is_none")]
613        x_max: Option<f64>,
614        /// Minimum Y value. Auto-computed if not set.
615        #[serde(default, skip_serializing_if = "Option::is_none")]
616        y_min: Option<f64>,
617        /// Maximum Y value. Auto-computed if not set.
618        #[serde(default, skip_serializing_if = "Option::is_none")]
619        y_max: Option<f64>,
620        /// X-axis label.
621        #[serde(default, skip_serializing_if = "Option::is_none")]
622        x_label: Option<String>,
623        /// Y-axis label.
624        #[serde(default, skip_serializing_if = "Option::is_none")]
625        y_label: Option<String>,
626        /// Show legend.
627        #[serde(default)]
628        show_legend: bool,
629        /// Dot radius in points.
630        #[serde(default = "default_dot_size")]
631        dot_size: f64,
632    },
633
634    /// A watermark rendered as rotated text behind page content.
635    Watermark {
636        /// The watermark text (e.g. "DRAFT", "CONFIDENTIAL").
637        text: String,
638        /// Font size in points. Default: 60.
639        #[serde(default = "default_watermark_font_size")]
640        font_size: f64,
641        /// Rotation angle in degrees (negative = counterclockwise). Default: -45.
642        #[serde(default = "default_watermark_angle")]
643        angle: f64,
644    },
645
646    /// An interactive text input field (PDF AcroForm widget).
647    TextField {
648        /// Field name, used for data extraction.
649        name: String,
650        /// Default/current value.
651        #[serde(default, skip_serializing_if = "Option::is_none")]
652        value: Option<String>,
653        /// Placeholder text displayed when empty.
654        #[serde(default, skip_serializing_if = "Option::is_none")]
655        placeholder: Option<String>,
656        /// Field width in points.
657        width: f64,
658        /// Field height in points. Default: 24.
659        #[serde(default = "default_form_field_height")]
660        height: f64,
661        /// Allow multiple lines of input.
662        #[serde(default)]
663        multiline: bool,
664        /// Mask input as password dots.
665        #[serde(default)]
666        password: bool,
667        /// Prevent editing.
668        #[serde(default)]
669        read_only: bool,
670        /// Maximum number of characters.
671        #[serde(default, skip_serializing_if = "Option::is_none")]
672        max_length: Option<u32>,
673        /// Font size in points. Default: 12.
674        #[serde(default = "default_form_font_size")]
675        font_size: f64,
676    },
677
678    /// An interactive checkbox (PDF AcroForm widget).
679    Checkbox {
680        /// Field name, used for data extraction.
681        name: String,
682        /// Default checked state.
683        #[serde(default)]
684        checked: bool,
685        /// Checkbox width in points. Default: 14.
686        #[serde(default = "default_checkbox_size")]
687        width: f64,
688        /// Checkbox height in points. Default: 14.
689        #[serde(default = "default_checkbox_size")]
690        height: f64,
691        /// Prevent editing.
692        #[serde(default)]
693        read_only: bool,
694    },
695
696    /// An interactive dropdown/combo box (PDF AcroForm widget).
697    Dropdown {
698        /// Field name, used for data extraction.
699        name: String,
700        /// Available options.
701        options: Vec<String>,
702        /// Default selected value.
703        #[serde(default, skip_serializing_if = "Option::is_none")]
704        value: Option<String>,
705        /// Field width in points.
706        width: f64,
707        /// Field height in points. Default: 24.
708        #[serde(default = "default_form_field_height")]
709        height: f64,
710        /// Prevent editing.
711        #[serde(default)]
712        read_only: bool,
713        /// Font size in points. Default: 12.
714        #[serde(default = "default_form_font_size")]
715        font_size: f64,
716    },
717
718    /// An interactive radio button (PDF AcroForm widget).
719    /// Multiple RadioButtons with the same `name` form a mutually exclusive group.
720    RadioButton {
721        /// Group name shared by all buttons in the group.
722        name: String,
723        /// This button's export value.
724        value: String,
725        /// Default selected state.
726        #[serde(default)]
727        checked: bool,
728        /// Button width in points. Default: 14.
729        #[serde(default = "default_checkbox_size")]
730        width: f64,
731        /// Button height in points. Default: 14.
732        #[serde(default = "default_checkbox_size")]
733        height: f64,
734        /// Prevent editing.
735        #[serde(default)]
736        read_only: bool,
737    },
738}
739
740/// A data point for bar charts and pie charts.
741#[derive(Debug, Clone, Serialize, Deserialize)]
742pub struct ChartDataPoint {
743    pub label: String,
744    pub value: f64,
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub color: Option<String>,
747}
748
749/// A data series for line charts and area charts.
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct ChartSeries {
752    pub name: String,
753    pub data: Vec<f64>,
754    #[serde(default, skip_serializing_if = "Option::is_none")]
755    pub color: Option<String>,
756}
757
758/// A group of data points for dot plots.
759#[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct DotPlotGroup {
761    pub name: String,
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub color: Option<String>,
764    pub data: Vec<(f64, f64)>,
765}
766
767/// A canvas drawing operation.
768#[derive(Debug, Clone, Serialize, Deserialize)]
769#[serde(tag = "op")]
770pub enum CanvasOp {
771    MoveTo {
772        x: f64,
773        y: f64,
774    },
775    LineTo {
776        x: f64,
777        y: f64,
778    },
779    BezierCurveTo {
780        cp1x: f64,
781        cp1y: f64,
782        cp2x: f64,
783        cp2y: f64,
784        x: f64,
785        y: f64,
786    },
787    QuadraticCurveTo {
788        cpx: f64,
789        cpy: f64,
790        x: f64,
791        y: f64,
792    },
793    ClosePath,
794    Rect {
795        x: f64,
796        y: f64,
797        width: f64,
798        height: f64,
799    },
800    Circle {
801        cx: f64,
802        cy: f64,
803        r: f64,
804    },
805    Ellipse {
806        cx: f64,
807        cy: f64,
808        rx: f64,
809        ry: f64,
810    },
811    Arc {
812        cx: f64,
813        cy: f64,
814        r: f64,
815        start_angle: f64,
816        end_angle: f64,
817        #[serde(default)]
818        counterclockwise: bool,
819    },
820    Stroke,
821    Fill,
822    FillAndStroke,
823    SetFillColor {
824        r: f64,
825        g: f64,
826        b: f64,
827    },
828    SetStrokeColor {
829        r: f64,
830        g: f64,
831        b: f64,
832    },
833    SetLineWidth {
834        width: f64,
835    },
836    SetLineCap {
837        cap: u32,
838    },
839    SetLineJoin {
840        join: u32,
841    },
842    Save,
843    Restore,
844}
845
846/// An inline styled run within a Text node.
847#[derive(Debug, Clone, Serialize, Deserialize)]
848#[serde(rename_all = "camelCase")]
849pub struct TextRun {
850    pub content: String,
851    #[serde(default)]
852    pub style: crate::style::Style,
853    #[serde(default, skip_serializing_if = "Option::is_none")]
854    pub href: Option<String>,
855}
856
857/// Positioning mode for a node.
858#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
859pub enum Position {
860    #[default]
861    Relative,
862    Absolute,
863}
864
865fn default_one() -> u32 {
866    1
867}
868
869fn default_barcode_height() -> f64 {
870    60.0
871}
872
873fn default_dot_size() -> f64 {
874    4.0
875}
876
877fn default_watermark_font_size() -> f64 {
878    60.0
879}
880
881fn default_watermark_angle() -> f64 {
882    -45.0
883}
884
885fn default_form_field_height() -> f64 {
886    24.0
887}
888
889fn default_form_font_size() -> f64 {
890    12.0
891}
892
893fn default_checkbox_size() -> f64 {
894    14.0
895}
896
897/// Column definition for tables.
898#[derive(Debug, Clone, Serialize, Deserialize)]
899pub struct ColumnDef {
900    /// Width as a fraction (0.0-1.0) of available table width, or fixed points.
901    pub width: ColumnWidth,
902}
903
904#[derive(Debug, Clone, Serialize, Deserialize)]
905pub enum ColumnWidth {
906    /// Fraction of available width (0.0-1.0).
907    Fraction(f64),
908    /// Fixed width in points.
909    Fixed(f64),
910    /// Distribute remaining space evenly among Auto columns.
911    Auto,
912}
913
914/// Where a fixed element is placed on the page.
915#[derive(Debug, Clone, Serialize, Deserialize)]
916pub enum FixedPosition {
917    /// Top of the content area (below margin).
918    Header,
919    /// Bottom of the content area (above margin).
920    Footer,
921}
922
923/// Source code location for click-to-source in the dev server inspector.
924#[derive(Debug, Clone, Serialize, Deserialize)]
925#[serde(rename_all = "camelCase")]
926pub struct SourceLocation {
927    pub file: String,
928    pub line: u32,
929    pub column: u32,
930}
931
932impl Node {
933    /// Create a View node with children.
934    pub fn view(style: Style, children: Vec<Node>) -> Self {
935        Self {
936            kind: NodeKind::View,
937            style,
938            children,
939            id: None,
940            source_location: None,
941            bookmark: None,
942            href: None,
943            alt: None,
944        }
945    }
946
947    /// Create a Text node.
948    pub fn text(content: &str, style: Style) -> Self {
949        Self {
950            kind: NodeKind::Text {
951                content: content.to_string(),
952                href: None,
953                runs: vec![],
954            },
955            style,
956            children: vec![],
957            id: None,
958            source_location: None,
959            bookmark: None,
960            href: None,
961            alt: None,
962        }
963    }
964
965    /// Create a Page node.
966    pub fn page(config: PageConfig, style: Style, children: Vec<Node>) -> Self {
967        Self {
968            kind: NodeKind::Page { config },
969            style,
970            children,
971            id: None,
972            source_location: None,
973            bookmark: None,
974            href: None,
975            alt: None,
976        }
977    }
978
979    /// Is this node breakable across pages?
980    pub fn is_breakable(&self) -> bool {
981        match &self.kind {
982            NodeKind::View | NodeKind::Table { .. } | NodeKind::Text { .. } => {
983                self.style.wrap.unwrap_or(true)
984            }
985            NodeKind::TableRow { .. } => true,
986            NodeKind::Image { .. } => false,
987            NodeKind::Svg { .. } => false,
988            NodeKind::Canvas { .. } => false,
989            NodeKind::Barcode { .. } => false,
990            NodeKind::QrCode { .. } => false,
991            NodeKind::BarChart { .. } => false,
992            NodeKind::LineChart { .. } => false,
993            NodeKind::PieChart { .. } => false,
994            NodeKind::AreaChart { .. } => false,
995            NodeKind::DotPlot { .. } => false,
996            NodeKind::Watermark { .. } => false,
997            NodeKind::TextField { .. } => false,
998            NodeKind::Checkbox { .. } => false,
999            NodeKind::Dropdown { .. } => false,
1000            NodeKind::RadioButton { .. } => false,
1001            NodeKind::PageBreak => false,
1002            NodeKind::Fixed { .. } => false,
1003            NodeKind::Page { .. } => true,
1004            NodeKind::TableCell { .. } => true,
1005        }
1006    }
1007}