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