use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::paginated::{Margins, Orientation, PageSize, Position};
use super::style::Transform;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MasterPage {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<PageSize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub orientation: Option<Orientation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margins: Option<Margins>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub header: Option<MasterPageRegion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footer: Option<MasterPageRegion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub background_elements: Vec<MasterPageElement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub foreground_elements: Vec<MasterPageElement>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub placeholders: HashMap<String, PlaceholderDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub based_on: Option<String>,
}
impl MasterPage {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
display_name: None,
page_size: None,
orientation: None,
margins: None,
header: None,
footer: None,
background_elements: Vec::new(),
foreground_elements: Vec::new(),
placeholders: HashMap::new(),
based_on: None,
}
}
#[must_use]
pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
self.display_name = Some(name.into());
self
}
#[must_use]
pub fn with_page_size(mut self, size: PageSize) -> Self {
self.page_size = Some(size);
self
}
#[must_use]
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = Some(orientation);
self
}
#[must_use]
pub fn with_margins(mut self, margins: Margins) -> Self {
self.margins = Some(margins);
self
}
#[must_use]
pub fn with_header(mut self, content: impl Into<String>) -> Self {
self.header = Some(MasterPageRegion::text(content));
self
}
#[must_use]
pub fn with_header_region(mut self, region: MasterPageRegion) -> Self {
self.header = Some(region);
self
}
#[must_use]
pub fn with_footer(mut self, content: impl Into<String>) -> Self {
self.footer = Some(MasterPageRegion::text(content));
self
}
#[must_use]
pub fn with_footer_region(mut self, region: MasterPageRegion) -> Self {
self.footer = Some(region);
self
}
#[must_use]
pub fn with_background_element(mut self, element: MasterPageElement) -> Self {
self.background_elements.push(element);
self
}
#[must_use]
pub fn with_foreground_element(mut self, element: MasterPageElement) -> Self {
self.foreground_elements.push(element);
self
}
#[must_use]
pub fn based_on(mut self, parent: impl Into<String>) -> Self {
self.based_on = Some(parent.into());
self
}
#[must_use]
pub fn default_master() -> Self {
Self::new("default").with_display_name("Default")
}
#[must_use]
pub fn odd_page() -> Self {
Self::new("odd").with_display_name("Odd Pages (Right)")
}
#[must_use]
pub fn even_page() -> Self {
Self::new("even").with_display_name("Even Pages (Left)")
}
#[must_use]
pub fn title_page() -> Self {
Self::new("title").with_display_name("Title Page")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MasterPageRegion {
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
#[serde(default)]
pub alignment: RegionAlignment,
}
impl MasterPageRegion {
#[must_use]
pub fn text(content: impl Into<String>) -> Self {
Self {
content: content.into(),
height: None,
style: None,
alignment: RegionAlignment::default(),
}
}
#[must_use]
pub fn page_number() -> Self {
Self::text("{pageNumber}").with_alignment(RegionAlignment::Center)
}
#[must_use]
pub fn page_number_of_total() -> Self {
Self::text("{pageNumber} of {totalPages}").with_alignment(RegionAlignment::Center)
}
#[must_use]
pub fn with_height(mut self, height: impl Into<String>) -> Self {
self.height = Some(height.into());
self
}
#[must_use]
pub fn with_style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
#[must_use]
pub fn with_alignment(mut self, alignment: RegionAlignment) -> Self {
self.alignment = alignment;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RegionAlignment {
Left,
#[default]
Center,
Right,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MasterPageElement {
pub id: String,
#[serde(rename = "type")]
pub element_type: MasterElementType,
pub position: Position,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transform: Option<Transform>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opacity: Option<f64>,
}
impl MasterPageElement {
#[must_use]
pub fn text(id: impl Into<String>, content: impl Into<String>, position: Position) -> Self {
Self {
id: id.into(),
element_type: MasterElementType::Text,
position,
content: Some(content.into()),
style: None,
transform: None,
opacity: None,
}
}
#[must_use]
pub fn image(id: impl Into<String>, src: impl Into<String>, position: Position) -> Self {
Self {
id: id.into(),
element_type: MasterElementType::Image,
position,
content: Some(src.into()),
style: None,
transform: None,
opacity: None,
}
}
#[must_use]
pub fn shape(id: impl Into<String>, shape_type: impl Into<String>, position: Position) -> Self {
Self {
id: id.into(),
element_type: MasterElementType::Shape,
position,
content: Some(shape_type.into()),
style: None,
transform: None,
opacity: None,
}
}
#[must_use]
pub fn with_style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
#[must_use]
pub fn with_opacity(mut self, opacity: f64) -> Self {
self.opacity = Some(opacity);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MasterElementType {
Text,
Image,
Shape,
Logo,
PageNumber,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaceholderDefinition {
#[serde(rename = "type")]
pub placeholder_type: PlaceholderType,
pub position: Position,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PlaceholderType {
Text,
Image,
Content,
PageNumber,
TotalPages,
Date,
Title,
Author,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrintSpecification {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bleed: Option<BleedBox>,
#[serde(default)]
pub crop_marks: CropMarkStyle,
#[serde(default)]
pub registration_marks: bool,
#[serde(default)]
pub color_bars: bool,
#[serde(default)]
pub page_information: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trim_box: Option<PageBox>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_box: Option<PageBox>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub art_box: Option<PageBox>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spot_colors: Vec<SpotColor>,
#[serde(default)]
pub color_space: ColorSpace,
}
impl Default for PrintSpecification {
fn default() -> Self {
Self {
bleed: None,
crop_marks: CropMarkStyle::None,
registration_marks: false,
color_bars: false,
page_information: false,
trim_box: None,
media_box: None,
art_box: None,
spot_colors: Vec::new(),
color_space: ColorSpace::default(),
}
}
}
impl PrintSpecification {
#[must_use]
pub fn with_bleed(mut self, bleed: BleedBox) -> Self {
self.bleed = Some(bleed);
self
}
#[must_use]
pub fn with_crop_marks(mut self, style: CropMarkStyle) -> Self {
self.crop_marks = style;
self
}
#[must_use]
pub fn with_registration_marks(mut self) -> Self {
self.registration_marks = true;
self
}
#[must_use]
pub fn with_color_bars(mut self) -> Self {
self.color_bars = true;
self
}
#[must_use]
pub fn with_page_information(mut self) -> Self {
self.page_information = true;
self
}
#[must_use]
pub fn with_color_space(mut self, color_space: ColorSpace) -> Self {
self.color_space = color_space;
self
}
#[must_use]
pub fn with_spot_color(mut self, spot_color: SpotColor) -> Self {
self.spot_colors.push(spot_color);
self
}
#[must_use]
pub fn commercial_print() -> Self {
Self::default()
.with_bleed(BleedBox::all("0.125in"))
.with_crop_marks(CropMarkStyle::All)
.with_registration_marks()
.with_color_bars()
.with_color_space(ColorSpace::Cmyk)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BleedBox {
pub top: String,
pub right: String,
pub bottom: String,
pub left: String,
}
impl BleedBox {
#[must_use]
pub fn all(value: impl Into<String>) -> Self {
let v = value.into();
Self {
top: v.clone(),
right: v.clone(),
bottom: v.clone(),
left: v,
}
}
#[must_use]
pub fn standard() -> Self {
Self::all("0.125in")
}
#[must_use]
pub fn new(
top: impl Into<String>,
right: impl Into<String>,
bottom: impl Into<String>,
left: impl Into<String>,
) -> Self {
Self {
top: top.into(),
right: right.into(),
bottom: bottom.into(),
left: left.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PageBox {
pub width: String,
pub height: String,
}
impl PageBox {
#[must_use]
pub fn new(width: impl Into<String>, height: impl Into<String>) -> Self {
Self {
width: width.into(),
height: height.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CropMarkStyle {
#[default]
None,
TrimMarks,
CenterMarks,
All,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotColor {
pub name: String,
#[serde(rename = "type")]
pub color_type: SpotColorType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alternate: Option<AlternateColor>,
#[serde(default = "default_tint")]
pub tint: f64,
}
fn default_tint() -> f64 {
100.0
}
impl SpotColor {
#[must_use]
pub fn pantone(name: impl Into<String>) -> Self {
Self {
name: name.into(),
color_type: SpotColorType::Pantone,
alternate: None,
tint: 100.0,
}
}
#[must_use]
pub fn custom(name: impl Into<String>) -> Self {
Self {
name: name.into(),
color_type: SpotColorType::Custom,
alternate: None,
tint: 100.0,
}
}
#[must_use]
pub fn with_cmyk_alternate(mut self, c: f64, m: f64, y: f64, k: f64) -> Self {
self.alternate = Some(AlternateColor::Cmyk { c, m, y, k });
self
}
#[must_use]
pub fn with_rgb_alternate(mut self, r: u8, g: u8, b: u8) -> Self {
self.alternate = Some(AlternateColor::Rgb { r, g, b });
self
}
#[must_use]
pub fn with_tint(mut self, tint: f64) -> Self {
self.tint = tint;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SpotColorType {
Pantone,
Custom,
Metallic,
Fluorescent,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", tag = "type")]
pub enum AlternateColor {
Cmyk {
c: f64,
m: f64,
y: f64,
k: f64,
},
Rgb {
r: u8,
g: u8,
b: u8,
},
Lab {
l: f64,
a: f64,
b: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColorSpace {
#[default]
Rgb,
Cmyk,
Grayscale,
Lab,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfXCompliance {
pub level: PdfXLevel,
pub output_intent: OutputIntent,
#[serde(default = "default_true")]
pub fonts_embedded: bool,
#[serde(default)]
pub transparency_flattened: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icc_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
fn default_true() -> bool {
true
}
impl PdfXCompliance {
#[must_use]
pub fn x1a_2001() -> Self {
Self {
level: PdfXLevel::X1a2001,
output_intent: OutputIntent::swop(),
fonts_embedded: true,
transparency_flattened: true,
icc_profile: None,
notes: None,
}
}
#[must_use]
pub fn x3_2002() -> Self {
Self {
level: PdfXLevel::X32002,
output_intent: OutputIntent::default(),
fonts_embedded: true,
transparency_flattened: false,
icc_profile: None,
notes: None,
}
}
#[must_use]
pub fn x4() -> Self {
Self {
level: PdfXLevel::X4,
output_intent: OutputIntent::default(),
fonts_embedded: true,
transparency_flattened: false,
icc_profile: None,
notes: None,
}
}
#[must_use]
pub fn with_icc_profile(mut self, profile: impl Into<String>) -> Self {
self.icc_profile = Some(profile.into());
self
}
#[must_use]
pub fn with_output_intent(mut self, intent: OutputIntent) -> Self {
self.output_intent = intent;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
pub enum PdfXLevel {
#[serde(rename = "PDF/X-1a:2001")]
#[strum(serialize = "PDF/X-1a:2001")]
X1a2001,
#[serde(rename = "PDF/X-1a:2003")]
#[strum(serialize = "PDF/X-1a:2003")]
X1a2003,
#[serde(rename = "PDF/X-3:2002")]
#[strum(serialize = "PDF/X-3:2002")]
X32002,
#[serde(rename = "PDF/X-3:2003")]
#[strum(serialize = "PDF/X-3:2003")]
X32003,
#[serde(rename = "PDF/X-4")]
#[strum(serialize = "PDF/X-4")]
X4,
#[serde(rename = "PDF/X-4p")]
#[strum(serialize = "PDF/X-4p")]
X4p,
#[serde(rename = "PDF/X-5g")]
#[strum(serialize = "PDF/X-5g")]
X5g,
#[serde(rename = "PDF/X-5pg")]
#[strum(serialize = "PDF/X-5pg")]
X5pg,
#[serde(rename = "PDF/X-6")]
#[strum(serialize = "PDF/X-6")]
X6,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputIntent {
pub output_condition_identifier: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_condition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info: Option<String>,
}
impl Default for OutputIntent {
fn default() -> Self {
Self {
output_condition_identifier: "sRGB".to_string(),
output_condition: Some("sRGB IEC61966-2.1".to_string()),
registry_name: Some("http://www.color.org".to_string()),
info: None,
}
}
}
impl OutputIntent {
#[must_use]
pub fn swop() -> Self {
Self {
output_condition_identifier: "CGATS TR 001".to_string(),
output_condition: Some("SWOP (Publication) Grade 1 Paper".to_string()),
registry_name: Some("http://www.color.org".to_string()),
info: None,
}
}
#[must_use]
pub fn fogra39() -> Self {
Self {
output_condition_identifier: "FOGRA39".to_string(),
output_condition: Some("Coated FOGRA39 (ISO 12647-2:2004)".to_string()),
registry_name: Some("http://www.color.org".to_string()),
info: None,
}
}
#[must_use]
pub fn gracol() -> Self {
Self {
output_condition_identifier: "CGATS TR 006".to_string(),
output_condition: Some("GRACoL 2006 (Coated #1)".to_string()),
registry_name: Some("http://www.color.org".to_string()),
info: None,
}
}
#[must_use]
pub fn custom(identifier: impl Into<String>) -> Self {
Self {
output_condition_identifier: identifier.into(),
output_condition: None,
registry_name: None,
info: None,
}
}
#[must_use]
pub fn with_condition(mut self, condition: impl Into<String>) -> Self {
self.output_condition = Some(condition.into());
self
}
#[must_use]
pub fn with_registry(mut self, registry: impl Into<String>) -> Self {
self.registry_name = Some(registry.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_master_page_creation() {
let master = MasterPage::new("default")
.with_display_name("Default Page")
.with_page_size(PageSize::a4())
.with_margins(Margins::all("1in"))
.with_header("Document Title")
.with_footer("{pageNumber} of {totalPages}");
assert_eq!(master.name, "default");
assert_eq!(master.display_name, Some("Default Page".to_string()));
assert!(master.header.is_some());
assert!(master.footer.is_some());
}
#[test]
fn test_master_page_serialization() {
let master = MasterPage::new("test")
.with_header("Header")
.with_footer("Footer");
let json = serde_json::to_string_pretty(&master).unwrap();
assert!(json.contains("\"name\": \"test\""));
let deserialized: MasterPage = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "test");
}
#[test]
fn test_print_specification() {
let spec = PrintSpecification::commercial_print();
assert!(spec.bleed.is_some());
assert_eq!(spec.crop_marks, CropMarkStyle::All);
assert!(spec.registration_marks);
assert!(spec.color_bars);
assert_eq!(spec.color_space, ColorSpace::Cmyk);
}
#[test]
fn test_bleed_box() {
let bleed = BleedBox::standard();
assert_eq!(bleed.top, "0.125in");
assert_eq!(bleed.right, "0.125in");
assert_eq!(bleed.bottom, "0.125in");
assert_eq!(bleed.left, "0.125in");
}
#[test]
fn test_spot_color() {
let color = SpotColor::pantone("PANTONE 185 C")
.with_cmyk_alternate(0.0, 91.0, 76.0, 0.0)
.with_tint(100.0);
assert_eq!(color.name, "PANTONE 185 C");
assert_eq!(color.color_type, SpotColorType::Pantone);
assert!(color.alternate.is_some());
}
#[test]
fn test_pdfx_compliance() {
let compliance = PdfXCompliance::x4()
.with_icc_profile("sRGB IEC61966-2.1")
.with_output_intent(OutputIntent::fogra39());
assert_eq!(compliance.level, PdfXLevel::X4);
assert!(compliance.fonts_embedded);
assert!(!compliance.transparency_flattened);
assert_eq!(
compliance.output_intent.output_condition_identifier,
"FOGRA39"
);
}
#[test]
fn test_pdfx_level_display() {
assert_eq!(PdfXLevel::X1a2001.to_string(), "PDF/X-1a:2001");
assert_eq!(PdfXLevel::X4.to_string(), "PDF/X-4");
}
#[test]
fn test_output_intent_presets() {
let swop = OutputIntent::swop();
assert_eq!(swop.output_condition_identifier, "CGATS TR 001");
let fogra = OutputIntent::fogra39();
assert_eq!(fogra.output_condition_identifier, "FOGRA39");
let gracol = OutputIntent::gracol();
assert_eq!(gracol.output_condition_identifier, "CGATS TR 006");
}
#[test]
fn test_master_page_presets() {
let default = MasterPage::default_master();
assert_eq!(default.name, "default");
let odd = MasterPage::odd_page();
assert_eq!(odd.name, "odd");
let even = MasterPage::even_page();
assert_eq!(even.name, "even");
let title = MasterPage::title_page();
assert_eq!(title.name, "title");
}
#[test]
fn test_region_alignment() {
let region = MasterPageRegion::page_number_of_total();
assert_eq!(region.alignment, RegionAlignment::Center);
assert_eq!(region.content, "{pageNumber} of {totalPages}");
}
#[test]
fn test_print_spec_serialization() {
let spec = PrintSpecification::commercial_print();
let json = serde_json::to_string_pretty(&spec).unwrap();
assert!(json.contains("\"cropMarks\": \"all\""));
assert!(json.contains("\"colorSpace\": \"cmyk\""));
let deserialized: PrintSpecification = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.crop_marks, CropMarkStyle::All);
}
}