1use crate::element::Element;
2use std::rc::Rc;
3
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
7#[derive(Clone, Copy, PartialEq, Debug)]
8pub enum PageFormat {
9 A3,
10 A4,
11 A5,
12 Letter,
13 Legal,
14 Custom(f32, f32),
15}
16
17impl PageFormat {
18 pub fn size(&self) -> (f32, f32) {
20 match self {
21 PageFormat::A3 => (841.8898, 1190.5512),
22 PageFormat::A4 => (595.2756, 841.8898),
23 PageFormat::A5 => (419.5276, 595.2756),
24 PageFormat::Letter => (612.0, 792.0),
25 PageFormat::Legal => (612.0, 1008.0),
26 PageFormat::Custom(w, h) => (*w, *h),
27 }
28 }
29}
30
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]
32#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
33#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
34pub enum Orientation {
35 #[default]
36 Portrait,
37 Landscape,
38}
39
40#[cfg_attr(
41 feature = "serde",
42 derive(serde::Serialize, serde::Deserialize),
43 serde(deny_unknown_fields, default)
44)]
45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
46#[derive(Clone, Debug, Default)]
47pub struct DocumentMetadata {
48 pub title: Option<String>,
49 pub author: Option<String>,
50 pub subject: Option<String>,
51 pub keywords: Option<String>,
52 pub creator: Option<String>,
53 pub creation_date: Option<PdfDate>,
54 pub mod_date: Option<PdfDate>,
55}
56
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63#[derive(Clone, Copy, PartialEq, Eq, Debug)]
64pub struct PdfDate {
65 pub year: u16,
66 pub month: u8,
67 pub day: u8,
68 pub hour: u8,
69 pub minute: u8,
70 pub second: u8,
71}
72
73impl PdfDate {
74 pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
75 PdfDate {
76 year,
77 month,
78 day,
79 hour,
80 minute,
81 second,
82 }
83 }
84
85 pub fn to_pdf_string(self) -> String {
88 format!(
89 "D:{:04}{:02}{:02}{:02}{:02}{:02}Z",
90 self.year, self.month, self.day, self.hour, self.minute, self.second
91 )
92 }
93
94 pub fn to_xmp_string(self) -> String {
98 format!(
99 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
100 self.year, self.month, self.day, self.hour, self.minute, self.second
101 )
102 }
103}
104
105#[cfg_attr(
106 feature = "serde",
107 derive(serde::Serialize, serde::Deserialize),
108 serde(deny_unknown_fields, default)
109)]
110#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
111#[derive(Clone, Copy, PartialEq, Debug, Default)]
112pub struct Margin {
113 pub top: f32,
114 pub right: f32,
115 pub bottom: f32,
116 pub left: f32,
117}
118
119impl Margin {
120 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
121 Margin {
122 top: vertical,
123 right: horizontal,
124 bottom: vertical,
125 left: horizontal,
126 }
127 }
128
129 pub fn all(value: f32) -> Self {
130 Margin {
131 top: value,
132 right: value,
133 bottom: value,
134 left: value,
135 }
136 }
137}
138
139#[derive(Clone, Copy, Debug)]
143pub struct PageContext {
144 pub page: usize,
145 pub total_pages: usize,
146}
147
148type HeaderFooterFn = Rc<dyn Fn(&PageContext) -> Element>;
149
150#[derive(Clone)]
153pub struct Header {
154 pub height: f32,
155 pub content: HeaderFooterFn,
156}
157
158impl Header {
159 pub fn new(height: f32, content: impl Fn(&PageContext) -> Element + 'static) -> Self {
160 Header {
161 height,
162 content: Rc::new(content),
163 }
164 }
165}
166
167#[derive(Clone)]
168pub struct Footer {
169 pub height: f32,
170 pub content: HeaderFooterFn,
171}
172
173impl Footer {
174 pub fn new(height: f32, content: impl Fn(&PageContext) -> Element + 'static) -> Self {
175 Footer {
176 height,
177 content: Rc::new(content),
178 }
179 }
180}
181
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
183#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
184#[derive(Clone)]
185pub struct Document {
186 pub page_format: PageFormat,
187 #[cfg_attr(feature = "serde", serde(default))]
188 pub orientation: Orientation,
189 #[cfg_attr(feature = "serde", serde(default))]
190 pub margin: Margin,
191 #[cfg_attr(feature = "serde", serde(skip))]
196 pub header: Option<Header>,
197 #[cfg_attr(feature = "serde", serde(skip))]
198 pub footer: Option<Footer>,
199 #[cfg_attr(feature = "serde", serde(skip, default = "default_visible_from"))]
200 pub header_visible_from: usize,
201 #[cfg_attr(feature = "serde", serde(skip, default = "default_visible_from"))]
202 pub footer_visible_from: usize,
203 #[cfg_attr(feature = "serde", serde(default))]
204 pub watermark: Option<crate::watermark::Watermark>,
205 #[cfg_attr(feature = "serde", serde(default))]
206 pub metadata: DocumentMetadata,
207 #[cfg_attr(feature = "serde", serde(default))]
211 pub theme: Option<crate::theme::Theme>,
212 #[cfg_attr(feature = "serde", serde(default))]
221 pub pdf_a3b: bool,
222 #[cfg_attr(feature = "serde", serde(skip))]
230 pub zugferd_xml: Option<Vec<u8>>,
231 #[cfg_attr(feature = "serde", serde(default))]
241 pub pdf_ua: bool,
242 #[cfg_attr(feature = "serde", serde(default))]
246 pub lang: Option<String>,
247 #[cfg_attr(feature = "serde", serde(default))]
248 pub children: Vec<Element>,
249}
250
251#[cfg(feature = "serde")]
252fn default_visible_from() -> usize {
253 1
254}
255
256impl Document {
257 pub fn new(page_format: PageFormat) -> Self {
258 Document {
259 page_format,
260 orientation: Orientation::default(),
261 margin: Margin::default(),
262 header: None,
263 footer: None,
264 header_visible_from: 1,
265 footer_visible_from: 1,
266 watermark: None,
267 metadata: DocumentMetadata::default(),
268 theme: None,
269 pdf_a3b: false,
270 zugferd_xml: None,
271 pdf_ua: false,
272 lang: None,
273 children: Vec::new(),
274 }
275 }
276
277 pub fn theme(mut self, theme: crate::theme::Theme) -> Self {
278 self.theme = Some(theme);
279 self
280 }
281
282 pub fn pdf_a3b(mut self) -> Self {
288 self.pdf_a3b = true;
289 self
290 }
291
292 pub fn zugferd_xml(mut self, xml: impl Into<Vec<u8>>) -> Self {
300 self.pdf_a3b = true;
301 self.zugferd_xml = Some(xml.into());
302 self
303 }
304
305 pub fn pdf_ua(mut self) -> Self {
315 self.pdf_a3b = true;
316 self.pdf_ua = true;
317 self
318 }
319
320 pub fn lang(mut self, lang: impl Into<String>) -> Self {
322 self.lang = Some(lang.into());
323 self
324 }
325
326 pub fn page_size(&self) -> (f32, f32) {
328 let (w, h) = self.page_format.size();
329 match self.orientation {
330 Orientation::Portrait => (w, h),
331 Orientation::Landscape => (h, w),
332 }
333 }
334
335 pub fn orientation(mut self, orientation: Orientation) -> Self {
336 self.orientation = orientation;
337 self
338 }
339
340 pub fn landscape(mut self) -> Self {
341 self.orientation = Orientation::Landscape;
342 self
343 }
344
345 pub fn portrait(mut self) -> Self {
346 self.orientation = Orientation::Portrait;
347 self
348 }
349
350 pub fn title(mut self, title: impl Into<String>) -> Self {
351 self.metadata.title = Some(title.into());
352 self
353 }
354
355 pub fn author(mut self, author: impl Into<String>) -> Self {
356 self.metadata.author = Some(author.into());
357 self
358 }
359
360 pub fn subject(mut self, subject: impl Into<String>) -> Self {
361 self.metadata.subject = Some(subject.into());
362 self
363 }
364
365 pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
366 self.metadata.keywords = Some(keywords.into());
367 self
368 }
369
370 pub fn creator(mut self, creator: impl Into<String>) -> Self {
371 self.metadata.creator = Some(creator.into());
372 self
373 }
374
375 pub fn creation_date(mut self, date: PdfDate) -> Self {
376 self.metadata.creation_date = Some(date);
377 self
378 }
379
380 pub fn mod_date(mut self, date: PdfDate) -> Self {
381 self.metadata.mod_date = Some(date);
382 self
383 }
384
385 pub fn margin(mut self, margin: Margin) -> Self {
386 self.margin = margin;
387 self
388 }
389
390 pub fn header(mut self, header: Header) -> Self {
391 self.header = Some(header);
392 self
393 }
394
395 pub fn footer(mut self, footer: Footer) -> Self {
396 self.footer = Some(footer);
397 self
398 }
399
400 pub fn header_visible_from(mut self, page: usize) -> Self {
404 self.header_visible_from = page;
405 self
406 }
407
408 pub fn footer_visible_from(mut self, page: usize) -> Self {
409 self.footer_visible_from = page;
410 self
411 }
412
413 pub fn watermark(mut self, watermark: crate::watermark::Watermark) -> Self {
416 self.watermark = Some(watermark);
417 self
418 }
419
420 pub fn add(&mut self, element: impl Into<Element>) -> &mut Self {
421 let mut element = element.into();
422 if let Some(theme) = &self.theme {
423 crate::theme::apply_theme(&mut element, theme);
424 }
425 self.children.push(element);
426 self
427 }
428}
429
430#[cfg(feature = "serde")]
438pub const CURRENT_SCHEMA_VERSION: u32 = 1;
439
440#[cfg(feature = "serde")]
447#[derive(serde::Serialize, serde::Deserialize)]
448#[serde(deny_unknown_fields)]
449#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
450pub struct DocumentSchema {
451 pub schema_version: u32,
452 pub document: Document,
453}
454
455#[cfg(feature = "serde")]
456#[derive(Debug)]
457pub enum DocumentJsonError {
458 UnsupportedSchemaVersion(u32),
460 HeaderOrFooterNotSupported,
464 ZugferdXmlNotSupported,
467 Json(serde_json::Error),
468 Template(crate::template::TemplateError),
472}
473
474#[cfg(feature = "serde")]
475impl std::fmt::Display for DocumentJsonError {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 match self {
478 DocumentJsonError::UnsupportedSchemaVersion(v) => {
479 write!(
480 f,
481 "unsupported schema_version {v} (this crate understands {CURRENT_SCHEMA_VERSION})"
482 )
483 }
484 DocumentJsonError::HeaderOrFooterNotSupported => {
485 write!(
486 f,
487 "Document::to_json: header/footer aren't representable in the JSON schema (issue #17 V1 scope)"
488 )
489 }
490 DocumentJsonError::ZugferdXmlNotSupported => {
491 write!(
492 f,
493 "Document::to_json: zugferd_xml isn't representable in the JSON schema (issue #26)"
494 )
495 }
496 DocumentJsonError::Json(e) => write!(f, "{e}"),
497 DocumentJsonError::Template(e) => write!(f, "{e}"),
498 }
499 }
500}
501
502#[cfg(feature = "serde")]
503impl std::error::Error for DocumentJsonError {
504 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
505 match self {
506 DocumentJsonError::Json(e) => Some(e),
507 DocumentJsonError::Template(e) => Some(e),
508 _ => None,
509 }
510 }
511}
512
513#[cfg(feature = "serde")]
514impl Document {
515 pub fn from_json(json: &str) -> Result<Document, DocumentJsonError> {
518 let schema: DocumentSchema = serde_json::from_str(json).map_err(DocumentJsonError::Json)?;
519 if schema.schema_version != CURRENT_SCHEMA_VERSION {
520 return Err(DocumentJsonError::UnsupportedSchemaVersion(schema.schema_version));
521 }
522 Ok(schema.document)
523 }
524
525 pub fn from_template(
530 template_json: &str,
531 data_json: &str,
532 on_missing: crate::template::MissingPlaceholder,
533 ) -> Result<Document, DocumentJsonError> {
534 let resolved = crate::template::render_template(template_json, data_json, on_missing).map_err(DocumentJsonError::Template)?;
535 Document::from_json(&resolved)
536 }
537
538 pub fn to_json(&self) -> Result<String, DocumentJsonError> {
541 if self.header.is_some() || self.footer.is_some() {
542 return Err(DocumentJsonError::HeaderOrFooterNotSupported);
543 }
544 if self.zugferd_xml.is_some() {
545 return Err(DocumentJsonError::ZugferdXmlNotSupported);
546 }
547 let schema = DocumentSchema {
548 schema_version: CURRENT_SCHEMA_VERSION,
549 document: self.clone(),
550 };
551 serde_json::to_string(&schema).map_err(DocumentJsonError::Json)
552 }
553}
554
555#[cfg(all(test, feature = "serde"))]
556mod json_tests {
557 use super::*;
558 use crate::element::Text;
559 use crate::style::{Align, Color};
560
561 fn sample_document() -> Document {
562 let mut doc = Document::new(PageFormat::A4).margin(Margin::all(30.0)).title("Rechnung");
563 doc.add(Text::new("Hello").size(18.0).color(Color::rgb(200, 0, 0)).align(Align::Center));
564 doc
565 }
566
567 #[test]
568 fn round_trip_preserves_page_format_and_children() {
569 let json = sample_document().to_json().expect("to_json should succeed");
570 assert!(
571 json.contains("\"schema_version\":1"),
572 "expected a versioned root field, got: {json}"
573 );
574 let doc = Document::from_json(&json).expect("from_json should succeed");
575 assert_eq!(doc.page_format, PageFormat::A4);
576 assert_eq!(doc.metadata.title.as_deref(), Some("Rechnung"));
577 assert_eq!(doc.children.len(), 1);
578 let Element::Text(t) = &doc.children[0] else {
579 panic!("expected a Text child");
580 };
581 assert_eq!(t.content, "Hello");
582 assert_eq!(t.style.size, 18.0);
583 assert_eq!(t.style.color, Color::rgb(200, 0, 0));
584 assert_eq!(t.style.align, Align::Center);
585 }
586
587 #[test]
588 fn unknown_field_is_a_clear_error_not_silent_loss() {
589 let json = r#"{"schema_version":1,"document":{"page_format":"A4","typo_field":true}}"#;
590 let Err(err) = Document::from_json(json) else {
591 panic!("an unknown field must be rejected");
592 };
593 let message = err.to_string();
594 assert!(
595 message.contains("typo_field") || message.contains("unknown field"),
596 "expected the error to mention the unknown field, got: {message}"
597 );
598 }
599
600 #[test]
601 fn to_json_refuses_a_document_with_a_header() {
602 let mut doc = sample_document();
603 doc = doc.header(Header::new(20.0, |_| Element::Text(Text::new("Header"))));
604 assert!(matches!(doc.to_json(), Err(DocumentJsonError::HeaderOrFooterNotSupported)));
605 }
606
607 #[test]
608 fn unsupported_schema_version_is_rejected() {
609 let json = r#"{"schema_version":99,"document":{"page_format":"A4"}}"#;
610 assert!(matches!(
611 Document::from_json(json),
612 Err(DocumentJsonError::UnsupportedSchemaVersion(99))
613 ));
614 }
615}