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