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