Skip to main content

cdx_core/presentation/
print.rs

1//! Print-specific presentation features.
2//!
3//! This module provides types for professional print workflows including:
4//!
5//! - Master pages/templates for reusable page layouts
6//! - Print specifications (bleed, crop marks, spot colors)
7//! - PDF/X compliance metadata
8//!
9//! # Master Pages
10//!
11//! Master pages define reusable templates that can be applied to document pages:
12//!
13//! ```
14//! use cdx_core::presentation::{MasterPage, MasterPageElement, PageSize, Margins};
15//!
16//! let master = MasterPage::new("default")
17//!     .with_page_size(PageSize::a4())
18//!     .with_margins(Margins::all("1in"))
19//!     .with_header("Company Name")
20//!     .with_footer("{pageNumber} of {totalPages}");
21//! ```
22//!
23//! # Print Specifications
24//!
25//! Print specifications define bleeding area, crop marks, and color settings:
26//!
27//! ```
28//! use cdx_core::presentation::{PrintSpecification, BleedBox, CropMarkStyle};
29//!
30//! let print_spec = PrintSpecification::default()
31//!     .with_bleed(BleedBox::all("0.125in"))
32//!     .with_crop_marks(CropMarkStyle::All);
33//! ```
34
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38use super::paginated::{Margins, Orientation, PageSize, Position};
39use super::style::Transform;
40
41// =============================================================================
42// Master Pages
43// =============================================================================
44
45/// A master page template that can be applied to document pages.
46///
47/// Master pages define reusable layouts including headers, footers,
48/// background elements, and placeholders for dynamic content.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct MasterPage {
52    /// Unique identifier for this master page.
53    pub name: String,
54
55    /// Human-readable display name.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub display_name: Option<String>,
58
59    /// Page size (overrides document default if specified).
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub page_size: Option<PageSize>,
62
63    /// Page orientation (overrides document default if specified).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub orientation: Option<Orientation>,
66
67    /// Page margins.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub margins: Option<Margins>,
70
71    /// Header region definition.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub header: Option<MasterPageRegion>,
74
75    /// Footer region definition.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub footer: Option<MasterPageRegion>,
78
79    /// Background elements (rendered behind content).
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub background_elements: Vec<MasterPageElement>,
82
83    /// Foreground elements (rendered above content).
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub foreground_elements: Vec<MasterPageElement>,
86
87    /// Placeholders for dynamic content insertion.
88    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89    pub placeholders: HashMap<String, PlaceholderDefinition>,
90
91    /// Parent master page to inherit from.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub based_on: Option<String>,
94}
95
96impl MasterPage {
97    /// Create a new master page with the given name.
98    #[must_use]
99    pub fn new(name: impl Into<String>) -> Self {
100        Self {
101            name: name.into(),
102            display_name: None,
103            page_size: None,
104            orientation: None,
105            margins: None,
106            header: None,
107            footer: None,
108            background_elements: Vec::new(),
109            foreground_elements: Vec::new(),
110            placeholders: HashMap::new(),
111            based_on: None,
112        }
113    }
114
115    /// Set the display name.
116    #[must_use]
117    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
118        self.display_name = Some(name.into());
119        self
120    }
121
122    /// Set the page size.
123    #[must_use]
124    pub fn with_page_size(mut self, size: PageSize) -> Self {
125        self.page_size = Some(size);
126        self
127    }
128
129    /// Set the page orientation.
130    #[must_use]
131    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
132        self.orientation = Some(orientation);
133        self
134    }
135
136    /// Set the margins.
137    #[must_use]
138    pub fn with_margins(mut self, margins: Margins) -> Self {
139        self.margins = Some(margins);
140        self
141    }
142
143    /// Add a simple text header.
144    #[must_use]
145    pub fn with_header(mut self, content: impl Into<String>) -> Self {
146        self.header = Some(MasterPageRegion::text(content));
147        self
148    }
149
150    /// Add a header region.
151    #[must_use]
152    pub fn with_header_region(mut self, region: MasterPageRegion) -> Self {
153        self.header = Some(region);
154        self
155    }
156
157    /// Add a simple text footer.
158    #[must_use]
159    pub fn with_footer(mut self, content: impl Into<String>) -> Self {
160        self.footer = Some(MasterPageRegion::text(content));
161        self
162    }
163
164    /// Add a footer region.
165    #[must_use]
166    pub fn with_footer_region(mut self, region: MasterPageRegion) -> Self {
167        self.footer = Some(region);
168        self
169    }
170
171    /// Add a background element.
172    #[must_use]
173    pub fn with_background_element(mut self, element: MasterPageElement) -> Self {
174        self.background_elements.push(element);
175        self
176    }
177
178    /// Add a foreground element.
179    #[must_use]
180    pub fn with_foreground_element(mut self, element: MasterPageElement) -> Self {
181        self.foreground_elements.push(element);
182        self
183    }
184
185    /// Set the parent master page.
186    #[must_use]
187    pub fn based_on(mut self, parent: impl Into<String>) -> Self {
188        self.based_on = Some(parent.into());
189        self
190    }
191
192    /// Create a standard "default" master page.
193    #[must_use]
194    pub fn default_master() -> Self {
195        Self::new("default").with_display_name("Default")
196    }
197
198    /// Create a master page for odd (right-hand) pages.
199    #[must_use]
200    pub fn odd_page() -> Self {
201        Self::new("odd").with_display_name("Odd Pages (Right)")
202    }
203
204    /// Create a master page for even (left-hand) pages.
205    #[must_use]
206    pub fn even_page() -> Self {
207        Self::new("even").with_display_name("Even Pages (Left)")
208    }
209
210    /// Create a master page for title/cover pages.
211    #[must_use]
212    pub fn title_page() -> Self {
213        Self::new("title").with_display_name("Title Page")
214    }
215}
216
217/// A header or footer region in a master page.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct MasterPageRegion {
221    /// Content template with placeholders like `{pageNumber}`, `{totalPages}`.
222    pub content: String,
223
224    /// Height of the region.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub height: Option<String>,
227
228    /// Style name to apply.
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub style: Option<String>,
231
232    /// Alignment of content within the region.
233    #[serde(default)]
234    pub alignment: RegionAlignment,
235}
236
237impl MasterPageRegion {
238    /// Create a region with text content.
239    #[must_use]
240    pub fn text(content: impl Into<String>) -> Self {
241        Self {
242            content: content.into(),
243            height: None,
244            style: None,
245            alignment: RegionAlignment::default(),
246        }
247    }
248
249    /// Create a page number footer.
250    #[must_use]
251    pub fn page_number() -> Self {
252        Self::text("{pageNumber}").with_alignment(RegionAlignment::Center)
253    }
254
255    /// Create a "page X of Y" footer.
256    #[must_use]
257    pub fn page_number_of_total() -> Self {
258        Self::text("{pageNumber} of {totalPages}").with_alignment(RegionAlignment::Center)
259    }
260
261    /// Set the height.
262    #[must_use]
263    pub fn with_height(mut self, height: impl Into<String>) -> Self {
264        self.height = Some(height.into());
265        self
266    }
267
268    /// Set the style.
269    #[must_use]
270    pub fn with_style(mut self, style: impl Into<String>) -> Self {
271        self.style = Some(style.into());
272        self
273    }
274
275    /// Set the alignment.
276    #[must_use]
277    pub fn with_alignment(mut self, alignment: RegionAlignment) -> Self {
278        self.alignment = alignment;
279        self
280    }
281}
282
283/// Alignment of content within a region.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
285#[serde(rename_all = "lowercase")]
286pub enum RegionAlignment {
287    /// Left-aligned.
288    Left,
289    /// Center-aligned.
290    #[default]
291    Center,
292    /// Right-aligned.
293    Right,
294}
295
296/// An element placed on a master page.
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct MasterPageElement {
300    /// Unique identifier for this element.
301    pub id: String,
302
303    /// Element type.
304    #[serde(rename = "type")]
305    pub element_type: MasterElementType,
306
307    /// Position on the page.
308    pub position: Position,
309
310    /// Element-specific content.
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub content: Option<String>,
313
314    /// Style name to apply.
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub style: Option<String>,
317
318    /// 2D transform.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub transform: Option<Transform>,
321
322    /// Opacity (0.0 to 1.0).
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub opacity: Option<f64>,
325}
326
327impl MasterPageElement {
328    /// Create a text element.
329    #[must_use]
330    pub fn text(id: impl Into<String>, content: impl Into<String>, position: Position) -> Self {
331        Self {
332            id: id.into(),
333            element_type: MasterElementType::Text,
334            position,
335            content: Some(content.into()),
336            style: None,
337            transform: None,
338            opacity: None,
339        }
340    }
341
342    /// Create an image element.
343    #[must_use]
344    pub fn image(id: impl Into<String>, src: impl Into<String>, position: Position) -> Self {
345        Self {
346            id: id.into(),
347            element_type: MasterElementType::Image,
348            position,
349            content: Some(src.into()),
350            style: None,
351            transform: None,
352            opacity: None,
353        }
354    }
355
356    /// Create a shape element.
357    #[must_use]
358    pub fn shape(id: impl Into<String>, shape_type: impl Into<String>, position: Position) -> Self {
359        Self {
360            id: id.into(),
361            element_type: MasterElementType::Shape,
362            position,
363            content: Some(shape_type.into()),
364            style: None,
365            transform: None,
366            opacity: None,
367        }
368    }
369
370    /// Set the style.
371    #[must_use]
372    pub fn with_style(mut self, style: impl Into<String>) -> Self {
373        self.style = Some(style.into());
374        self
375    }
376
377    /// Set the opacity.
378    #[must_use]
379    pub fn with_opacity(mut self, opacity: f64) -> Self {
380        self.opacity = Some(opacity);
381        self
382    }
383}
384
385/// Type of master page element.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(rename_all = "lowercase")]
388pub enum MasterElementType {
389    /// Text content.
390    Text,
391    /// Image reference.
392    Image,
393    /// Shape (rectangle, line, etc.).
394    Shape,
395    /// Logo placeholder.
396    Logo,
397    /// Page number field.
398    PageNumber,
399}
400
401/// Definition of a placeholder in a master page.
402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct PlaceholderDefinition {
405    /// Placeholder type.
406    #[serde(rename = "type")]
407    pub placeholder_type: PlaceholderType,
408
409    /// Position on the page.
410    pub position: Position,
411
412    /// Default content if not overridden.
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub default_content: Option<String>,
415
416    /// Style name to apply.
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub style: Option<String>,
419}
420
421/// Type of placeholder.
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub enum PlaceholderType {
425    /// Text placeholder.
426    Text,
427    /// Image placeholder.
428    Image,
429    /// Content flow area.
430    Content,
431    /// Page number.
432    PageNumber,
433    /// Total pages.
434    TotalPages,
435    /// Current date.
436    Date,
437    /// Document title.
438    Title,
439    /// Author name.
440    Author,
441}
442
443// =============================================================================
444// Print Specifications
445// =============================================================================
446
447/// Print specifications for professional output.
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct PrintSpecification {
451    /// Bleed area beyond the trim edge.
452    #[serde(default, skip_serializing_if = "Option::is_none")]
453    pub bleed: Option<BleedBox>,
454
455    /// Crop mark style.
456    #[serde(default)]
457    pub crop_marks: CropMarkStyle,
458
459    /// Registration mark settings.
460    #[serde(default)]
461    pub registration_marks: bool,
462
463    /// Color bars for press calibration.
464    #[serde(default)]
465    pub color_bars: bool,
466
467    /// Page information (file name, date, etc.).
468    #[serde(default)]
469    pub page_information: bool,
470
471    /// Trim box dimensions (final page size after cutting).
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub trim_box: Option<PageBox>,
474
475    /// Media box dimensions (physical media size).
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub media_box: Option<PageBox>,
478
479    /// Art box dimensions (intended content area).
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub art_box: Option<PageBox>,
482
483    /// Spot colors used in the document.
484    #[serde(default, skip_serializing_if = "Vec::is_empty")]
485    pub spot_colors: Vec<SpotColor>,
486
487    /// Default color space for the document.
488    #[serde(default)]
489    pub color_space: ColorSpace,
490}
491
492impl Default for PrintSpecification {
493    fn default() -> Self {
494        Self {
495            bleed: None,
496            crop_marks: CropMarkStyle::None,
497            registration_marks: false,
498            color_bars: false,
499            page_information: false,
500            trim_box: None,
501            media_box: None,
502            art_box: None,
503            spot_colors: Vec::new(),
504            color_space: ColorSpace::default(),
505        }
506    }
507}
508
509impl PrintSpecification {
510    /// Set the bleed area.
511    #[must_use]
512    pub fn with_bleed(mut self, bleed: BleedBox) -> Self {
513        self.bleed = Some(bleed);
514        self
515    }
516
517    /// Set the crop mark style.
518    #[must_use]
519    pub fn with_crop_marks(mut self, style: CropMarkStyle) -> Self {
520        self.crop_marks = style;
521        self
522    }
523
524    /// Enable registration marks.
525    #[must_use]
526    pub fn with_registration_marks(mut self) -> Self {
527        self.registration_marks = true;
528        self
529    }
530
531    /// Enable color bars.
532    #[must_use]
533    pub fn with_color_bars(mut self) -> Self {
534        self.color_bars = true;
535        self
536    }
537
538    /// Enable page information.
539    #[must_use]
540    pub fn with_page_information(mut self) -> Self {
541        self.page_information = true;
542        self
543    }
544
545    /// Set the color space.
546    #[must_use]
547    pub fn with_color_space(mut self, color_space: ColorSpace) -> Self {
548        self.color_space = color_space;
549        self
550    }
551
552    /// Add a spot color.
553    #[must_use]
554    pub fn with_spot_color(mut self, spot_color: SpotColor) -> Self {
555        self.spot_colors.push(spot_color);
556        self
557    }
558
559    /// Create a standard commercial print specification.
560    #[must_use]
561    pub fn commercial_print() -> Self {
562        Self::default()
563            .with_bleed(BleedBox::all("0.125in"))
564            .with_crop_marks(CropMarkStyle::All)
565            .with_registration_marks()
566            .with_color_bars()
567            .with_color_space(ColorSpace::Cmyk)
568    }
569}
570
571/// Bleed area beyond the trim edge.
572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
573pub struct BleedBox {
574    /// Top bleed.
575    pub top: String,
576    /// Right bleed.
577    pub right: String,
578    /// Bottom bleed.
579    pub bottom: String,
580    /// Left bleed.
581    pub left: String,
582}
583
584impl BleedBox {
585    /// Create bleed with all sides equal.
586    #[must_use]
587    pub fn all(value: impl Into<String>) -> Self {
588        let v = value.into();
589        Self {
590            top: v.clone(),
591            right: v.clone(),
592            bottom: v.clone(),
593            left: v,
594        }
595    }
596
597    /// Standard commercial print bleed (0.125 inch / 3mm).
598    #[must_use]
599    pub fn standard() -> Self {
600        Self::all("0.125in")
601    }
602
603    /// Create bleed with individual values.
604    #[must_use]
605    pub fn new(
606        top: impl Into<String>,
607        right: impl Into<String>,
608        bottom: impl Into<String>,
609        left: impl Into<String>,
610    ) -> Self {
611        Self {
612            top: top.into(),
613            right: right.into(),
614            bottom: bottom.into(),
615            left: left.into(),
616        }
617    }
618}
619
620/// Page box dimensions.
621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
622pub struct PageBox {
623    /// Box width.
624    pub width: String,
625    /// Box height.
626    pub height: String,
627}
628
629impl PageBox {
630    /// Create a page box.
631    #[must_use]
632    pub fn new(width: impl Into<String>, height: impl Into<String>) -> Self {
633        Self {
634            width: width.into(),
635            height: height.into(),
636        }
637    }
638}
639
640/// Crop mark style.
641#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub enum CropMarkStyle {
644    /// No crop marks.
645    #[default]
646    None,
647    /// Trim marks at corners only.
648    TrimMarks,
649    /// Center marks on each edge.
650    CenterMarks,
651    /// Both trim and center marks.
652    All,
653}
654
655/// Spot color definition.
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
657#[serde(rename_all = "camelCase")]
658pub struct SpotColor {
659    /// Spot color name (e.g., "PANTONE 185 C").
660    pub name: String,
661
662    /// Color type.
663    #[serde(rename = "type")]
664    pub color_type: SpotColorType,
665
666    /// Alternate color for display (typically CMYK or RGB).
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub alternate: Option<AlternateColor>,
669
670    /// Tint percentage (0-100).
671    #[serde(default = "default_tint")]
672    pub tint: f64,
673}
674
675fn default_tint() -> f64 {
676    100.0
677}
678
679impl SpotColor {
680    /// Create a Pantone spot color.
681    #[must_use]
682    pub fn pantone(name: impl Into<String>) -> Self {
683        Self {
684            name: name.into(),
685            color_type: SpotColorType::Pantone,
686            alternate: None,
687            tint: 100.0,
688        }
689    }
690
691    /// Create a custom spot color.
692    #[must_use]
693    pub fn custom(name: impl Into<String>) -> Self {
694        Self {
695            name: name.into(),
696            color_type: SpotColorType::Custom,
697            alternate: None,
698            tint: 100.0,
699        }
700    }
701
702    /// Set an alternate CMYK color.
703    #[must_use]
704    pub fn with_cmyk_alternate(mut self, c: f64, m: f64, y: f64, k: f64) -> Self {
705        self.alternate = Some(AlternateColor::Cmyk { c, m, y, k });
706        self
707    }
708
709    /// Set an alternate RGB color.
710    #[must_use]
711    pub fn with_rgb_alternate(mut self, r: u8, g: u8, b: u8) -> Self {
712        self.alternate = Some(AlternateColor::Rgb { r, g, b });
713        self
714    }
715
716    /// Set the tint percentage.
717    #[must_use]
718    pub fn with_tint(mut self, tint: f64) -> Self {
719        self.tint = tint;
720        self
721    }
722}
723
724/// Type of spot color.
725#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "lowercase")]
727pub enum SpotColorType {
728    /// Pantone color.
729    Pantone,
730    /// Custom spot color.
731    Custom,
732    /// Metallic color.
733    Metallic,
734    /// Fluorescent color.
735    Fluorescent,
736}
737
738/// Alternate color representation.
739#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
740#[serde(rename_all = "lowercase", tag = "type")]
741pub enum AlternateColor {
742    /// CMYK color (0-100 for each component).
743    Cmyk {
744        /// Cyan component (0-100).
745        c: f64,
746        /// Magenta component (0-100).
747        m: f64,
748        /// Yellow component (0-100).
749        y: f64,
750        /// Black (Key) component (0-100).
751        k: f64,
752    },
753    /// RGB color (0-255 for each component).
754    Rgb {
755        /// Red component (0-255).
756        r: u8,
757        /// Green component (0-255).
758        g: u8,
759        /// Blue component (0-255).
760        b: u8,
761    },
762    /// Lab color.
763    Lab {
764        /// Lightness component (0-100).
765        l: f64,
766        /// a* component (green-red axis).
767        a: f64,
768        /// b* component (blue-yellow axis).
769        b: f64,
770    },
771}
772
773/// Color space for the document.
774#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
775#[serde(rename_all = "lowercase")]
776pub enum ColorSpace {
777    /// RGB color space (screen display).
778    #[default]
779    Rgb,
780    /// CMYK color space (commercial print).
781    Cmyk,
782    /// Grayscale.
783    Grayscale,
784    /// Device-independent Lab color space.
785    Lab,
786}
787
788// =============================================================================
789// PDF/X Compliance
790// =============================================================================
791
792/// PDF/X compliance metadata.
793#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795pub struct PdfXCompliance {
796    /// PDF/X conformance level.
797    pub level: PdfXLevel,
798
799    /// Output intent specification.
800    pub output_intent: OutputIntent,
801
802    /// Whether all fonts are embedded.
803    #[serde(default = "default_true")]
804    pub fonts_embedded: bool,
805
806    /// Whether transparency is flattened.
807    #[serde(default)]
808    pub transparency_flattened: bool,
809
810    /// ICC color profile reference.
811    #[serde(default, skip_serializing_if = "Option::is_none")]
812    pub icc_profile: Option<String>,
813
814    /// Additional compliance notes.
815    #[serde(default, skip_serializing_if = "Option::is_none")]
816    pub notes: Option<String>,
817}
818
819fn default_true() -> bool {
820    true
821}
822
823impl PdfXCompliance {
824    /// Create PDF/X-1a:2001 compliance specification.
825    #[must_use]
826    pub fn x1a_2001() -> Self {
827        Self {
828            level: PdfXLevel::X1a2001,
829            output_intent: OutputIntent::swop(),
830            fonts_embedded: true,
831            transparency_flattened: true,
832            icc_profile: None,
833            notes: None,
834        }
835    }
836
837    /// Create PDF/X-3:2002 compliance specification.
838    #[must_use]
839    pub fn x3_2002() -> Self {
840        Self {
841            level: PdfXLevel::X32002,
842            output_intent: OutputIntent::default(),
843            fonts_embedded: true,
844            transparency_flattened: false,
845            icc_profile: None,
846            notes: None,
847        }
848    }
849
850    /// Create PDF/X-4 compliance specification.
851    #[must_use]
852    pub fn x4() -> Self {
853        Self {
854            level: PdfXLevel::X4,
855            output_intent: OutputIntent::default(),
856            fonts_embedded: true,
857            transparency_flattened: false,
858            icc_profile: None,
859            notes: None,
860        }
861    }
862
863    /// Set the ICC profile reference.
864    #[must_use]
865    pub fn with_icc_profile(mut self, profile: impl Into<String>) -> Self {
866        self.icc_profile = Some(profile.into());
867        self
868    }
869
870    /// Set the output intent.
871    #[must_use]
872    pub fn with_output_intent(mut self, intent: OutputIntent) -> Self {
873        self.output_intent = intent;
874        self
875    }
876}
877
878/// PDF/X conformance level.
879#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
880pub enum PdfXLevel {
881    /// PDF/X-1a:2001 - CMYK/spot only, no transparency.
882    #[serde(rename = "PDF/X-1a:2001")]
883    #[strum(serialize = "PDF/X-1a:2001")]
884    X1a2001,
885
886    /// PDF/X-1a:2003 - Updated PDF/X-1a.
887    #[serde(rename = "PDF/X-1a:2003")]
888    #[strum(serialize = "PDF/X-1a:2003")]
889    X1a2003,
890
891    /// PDF/X-3:2002 - Allows RGB and device-independent color.
892    #[serde(rename = "PDF/X-3:2002")]
893    #[strum(serialize = "PDF/X-3:2002")]
894    X32002,
895
896    /// PDF/X-3:2003 - Updated PDF/X-3.
897    #[serde(rename = "PDF/X-3:2003")]
898    #[strum(serialize = "PDF/X-3:2003")]
899    X32003,
900
901    /// PDF/X-4 - Supports transparency, layers, and OpenType fonts.
902    #[serde(rename = "PDF/X-4")]
903    #[strum(serialize = "PDF/X-4")]
904    X4,
905
906    /// PDF/X-4p - PDF/X-4 with external ICC profile reference.
907    #[serde(rename = "PDF/X-4p")]
908    #[strum(serialize = "PDF/X-4p")]
909    X4p,
910
911    /// PDF/X-5g - For multi-file workflows with external graphics.
912    #[serde(rename = "PDF/X-5g")]
913    #[strum(serialize = "PDF/X-5g")]
914    X5g,
915
916    /// PDF/X-5pg - Combines X-4p and X-5g features.
917    #[serde(rename = "PDF/X-5pg")]
918    #[strum(serialize = "PDF/X-5pg")]
919    X5pg,
920
921    /// PDF/X-6 - Latest standard with expanded features.
922    #[serde(rename = "PDF/X-6")]
923    #[strum(serialize = "PDF/X-6")]
924    X6,
925}
926
927/// Output intent specification for PDF/X.
928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
929#[serde(rename_all = "camelCase")]
930pub struct OutputIntent {
931    /// Output condition identifier (e.g., "FOGRA39").
932    pub output_condition_identifier: String,
933
934    /// Human-readable output condition.
935    #[serde(default, skip_serializing_if = "Option::is_none")]
936    pub output_condition: Option<String>,
937
938    /// Registry name (e.g., `http://www.color.org`).
939    #[serde(default, skip_serializing_if = "Option::is_none")]
940    pub registry_name: Option<String>,
941
942    /// Additional information.
943    #[serde(default, skip_serializing_if = "Option::is_none")]
944    pub info: Option<String>,
945}
946
947impl Default for OutputIntent {
948    fn default() -> Self {
949        Self {
950            output_condition_identifier: "sRGB".to_string(),
951            output_condition: Some("sRGB IEC61966-2.1".to_string()),
952            registry_name: Some("http://www.color.org".to_string()),
953            info: None,
954        }
955    }
956}
957
958impl OutputIntent {
959    /// Create a SWOP output intent (US web coated).
960    #[must_use]
961    pub fn swop() -> Self {
962        Self {
963            output_condition_identifier: "CGATS TR 001".to_string(),
964            output_condition: Some("SWOP (Publication) Grade 1 Paper".to_string()),
965            registry_name: Some("http://www.color.org".to_string()),
966            info: None,
967        }
968    }
969
970    /// Create a FOGRA39 output intent (European coated offset).
971    #[must_use]
972    pub fn fogra39() -> Self {
973        Self {
974            output_condition_identifier: "FOGRA39".to_string(),
975            output_condition: Some("Coated FOGRA39 (ISO 12647-2:2004)".to_string()),
976            registry_name: Some("http://www.color.org".to_string()),
977            info: None,
978        }
979    }
980
981    /// Create a `GRACoL` output intent (US commercial printing).
982    #[must_use]
983    pub fn gracol() -> Self {
984        Self {
985            output_condition_identifier: "CGATS TR 006".to_string(),
986            output_condition: Some("GRACoL 2006 (Coated #1)".to_string()),
987            registry_name: Some("http://www.color.org".to_string()),
988            info: None,
989        }
990    }
991
992    /// Create a custom output intent.
993    #[must_use]
994    pub fn custom(identifier: impl Into<String>) -> Self {
995        Self {
996            output_condition_identifier: identifier.into(),
997            output_condition: None,
998            registry_name: None,
999            info: None,
1000        }
1001    }
1002
1003    /// Set the output condition description.
1004    #[must_use]
1005    pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
1006        self.output_condition = Some(condition.into());
1007        self
1008    }
1009
1010    /// Set the registry name.
1011    #[must_use]
1012    pub fn with_registry(mut self, registry: impl Into<String>) -> Self {
1013        self.registry_name = Some(registry.into());
1014        self
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021
1022    #[test]
1023    fn test_master_page_creation() {
1024        let master = MasterPage::new("default")
1025            .with_display_name("Default Page")
1026            .with_page_size(PageSize::a4())
1027            .with_margins(Margins::all("1in"))
1028            .with_header("Document Title")
1029            .with_footer("{pageNumber} of {totalPages}");
1030
1031        assert_eq!(master.name, "default");
1032        assert_eq!(master.display_name, Some("Default Page".to_string()));
1033        assert!(master.header.is_some());
1034        assert!(master.footer.is_some());
1035    }
1036
1037    #[test]
1038    fn test_master_page_serialization() {
1039        let master = MasterPage::new("test")
1040            .with_header("Header")
1041            .with_footer("Footer");
1042
1043        let json = serde_json::to_string_pretty(&master).unwrap();
1044        assert!(json.contains("\"name\": \"test\""));
1045
1046        let deserialized: MasterPage = serde_json::from_str(&json).unwrap();
1047        assert_eq!(deserialized.name, "test");
1048    }
1049
1050    #[test]
1051    fn test_print_specification() {
1052        let spec = PrintSpecification::commercial_print();
1053
1054        assert!(spec.bleed.is_some());
1055        assert_eq!(spec.crop_marks, CropMarkStyle::All);
1056        assert!(spec.registration_marks);
1057        assert!(spec.color_bars);
1058        assert_eq!(spec.color_space, ColorSpace::Cmyk);
1059    }
1060
1061    #[test]
1062    fn test_bleed_box() {
1063        let bleed = BleedBox::standard();
1064        assert_eq!(bleed.top, "0.125in");
1065        assert_eq!(bleed.right, "0.125in");
1066        assert_eq!(bleed.bottom, "0.125in");
1067        assert_eq!(bleed.left, "0.125in");
1068    }
1069
1070    #[test]
1071    fn test_spot_color() {
1072        let color = SpotColor::pantone("PANTONE 185 C")
1073            .with_cmyk_alternate(0.0, 91.0, 76.0, 0.0)
1074            .with_tint(100.0);
1075
1076        assert_eq!(color.name, "PANTONE 185 C");
1077        assert_eq!(color.color_type, SpotColorType::Pantone);
1078        assert!(color.alternate.is_some());
1079    }
1080
1081    #[test]
1082    fn test_pdfx_compliance() {
1083        let compliance = PdfXCompliance::x4()
1084            .with_icc_profile("sRGB IEC61966-2.1")
1085            .with_output_intent(OutputIntent::fogra39());
1086
1087        assert_eq!(compliance.level, PdfXLevel::X4);
1088        assert!(compliance.fonts_embedded);
1089        assert!(!compliance.transparency_flattened);
1090        assert_eq!(
1091            compliance.output_intent.output_condition_identifier,
1092            "FOGRA39"
1093        );
1094    }
1095
1096    #[test]
1097    fn test_pdfx_level_display() {
1098        assert_eq!(PdfXLevel::X1a2001.to_string(), "PDF/X-1a:2001");
1099        assert_eq!(PdfXLevel::X4.to_string(), "PDF/X-4");
1100    }
1101
1102    #[test]
1103    fn test_output_intent_presets() {
1104        let swop = OutputIntent::swop();
1105        assert_eq!(swop.output_condition_identifier, "CGATS TR 001");
1106
1107        let fogra = OutputIntent::fogra39();
1108        assert_eq!(fogra.output_condition_identifier, "FOGRA39");
1109
1110        let gracol = OutputIntent::gracol();
1111        assert_eq!(gracol.output_condition_identifier, "CGATS TR 006");
1112    }
1113
1114    #[test]
1115    fn test_master_page_presets() {
1116        let default = MasterPage::default_master();
1117        assert_eq!(default.name, "default");
1118
1119        let odd = MasterPage::odd_page();
1120        assert_eq!(odd.name, "odd");
1121
1122        let even = MasterPage::even_page();
1123        assert_eq!(even.name, "even");
1124
1125        let title = MasterPage::title_page();
1126        assert_eq!(title.name, "title");
1127    }
1128
1129    #[test]
1130    fn test_region_alignment() {
1131        let region = MasterPageRegion::page_number_of_total();
1132        assert_eq!(region.alignment, RegionAlignment::Center);
1133        assert_eq!(region.content, "{pageNumber} of {totalPages}");
1134    }
1135
1136    #[test]
1137    fn test_print_spec_serialization() {
1138        let spec = PrintSpecification::commercial_print();
1139        let json = serde_json::to_string_pretty(&spec).unwrap();
1140
1141        assert!(json.contains("\"cropMarks\": \"all\""));
1142        assert!(json.contains("\"colorSpace\": \"cmyk\""));
1143
1144        let deserialized: PrintSpecification = serde_json::from_str(&json).unwrap();
1145        assert_eq!(deserialized.crop_marks, CropMarkStyle::All);
1146    }
1147}