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