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