use pdf_writer::{Finish, Pdf, Ref, TextStr};
use std::cell::LazyCell;
use xmp_writer::{LangId, Timezone, XmpWriter};
use crate::configure::{Configuration, PdfVersion, ValidationError};
use crate::serialize::SerializeContext;
#[derive(Default, Clone, Debug)]
pub struct Metadata {
pub(crate) title: Option<String>,
pub(crate) description: Option<String>,
pub(crate) creator: Option<String>,
pub(crate) producer: Option<String>,
pub(crate) keywords: Option<Vec<String>>,
pub(crate) authors: Option<Vec<String>>,
pub(crate) document_id: Option<String>,
pub(crate) language: Option<String>,
pub(crate) creation_date: Option<DateTime>,
pub(crate) text_direction: Option<TextDirection>,
pub(crate) page_layout: Option<PageLayout>,
}
impl Metadata {
pub fn new() -> Self {
Self {
..Default::default()
}
}
pub fn title(mut self, title: String) -> Self {
if !title.is_empty() {
self.title = Some(title);
}
self
}
pub fn description(mut self, description: String) -> Self {
if !description.is_empty() {
self.description = Some(description);
}
self
}
pub fn keywords(mut self, keywords: Vec<String>) -> Self {
if !keywords.is_empty() {
self.keywords = Some(keywords);
}
self
}
pub fn language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
pub fn creator(mut self, creator: String) -> Self {
if !creator.is_empty() {
self.creator = Some(creator);
}
self
}
pub fn producer(mut self, producer: String) -> Self {
if !producer.is_empty() {
self.producer = Some(producer);
}
self
}
pub fn authors(mut self, authors: Vec<String>) -> Self {
if !authors.is_empty() {
self.authors = Some(authors);
}
self
}
pub fn creation_date(mut self, creation_date: DateTime) -> Self {
self.creation_date = Some(creation_date);
self
}
pub fn document_id(mut self, document_id: String) -> Self {
self.document_id = Some(document_id);
self
}
pub fn text_direction(mut self, text_direction: TextDirection) -> Self {
self.text_direction = Some(text_direction);
self
}
pub fn page_layout(mut self, page_layout: PageLayout) -> Self {
self.page_layout = Some(page_layout);
self
}
pub(crate) fn has_document_info(&self) -> bool {
self.title.is_some()
|| self.producer.is_some()
|| self.keywords.is_some()
|| self.authors.is_some()
|| self.creator.is_some()
|| self.creation_date.is_some()
|| self.description.is_some()
}
pub(crate) fn serialize_xmp_metadata(
&self,
xmp: &mut XmpWriter,
sc: &mut SerializeContext,
instance_id: &str,
) {
if let Some(title) = &self.title {
xmp.title([(None, title.as_str())]);
}
if let Some(description) = &self.description {
xmp.description([(None, description.as_str())]);
}
if let Some(keywords) = &self.keywords {
let joined = keywords.join(", ");
xmp.pdf_keywords(joined.as_str());
}
match &self.authors {
Some(authors) if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf20 => {
xmp.creator(authors.iter().map(String::as_str));
}
Some(authors) => {
let joined = authors.join(", ");
xmp.creator([joined.as_str()]);
}
None => {}
}
if let Some(creator) = &self.creator {
xmp.creator_tool(creator);
}
if let Some(producer) = &self.producer {
xmp.producer(producer);
}
if let Some(lang) = &self.language {
xmp.language([LangId(lang)]);
}
if let Some(date) = self.creation_date.map(xmp_date) {
xmp.modify_date(date);
xmp.create_date(date);
if sc
.serialize_settings()
.validator()
.requires_file_provenance_information()
{
let mut history = xmp.history();
let mut saved = history.add_event();
saved
.action(xmp_writer::ResourceEventAction::Saved)
.when(date);
if !sc
.serialize_settings()
.validator()
.prohibits_instance_id_in_xmp_metadata()
{
saved.instance_id(&format!("{instance_id}_source"));
}
saved.finish();
let mut converted = history.add_event();
converted
.action(xmp_writer::ResourceEventAction::Converted)
.when(date);
if let Some(creator) = &self.creator {
converted.software_agent(creator);
}
if !sc
.serialize_settings()
.validator()
.prohibits_instance_id_in_xmp_metadata()
{
converted.instance_id(&format!("{instance_id}_source"));
}
}
} else {
sc.register_validation_error(ValidationError::MissingDocumentDate);
}
}
pub(crate) fn serialize_document_info(
&self,
ref_: &mut Ref,
pdf: &mut Pdf,
config: Configuration,
) {
if !config.validator().allows_info_dict() {
return;
}
if self.has_document_info() {
let ref_ = ref_.bump();
let mut document_info = LazyCell::new(|| pdf.document_info(ref_));
if config.version() < PdfVersion::Pdf20 {
if let Some(title) = &self.title {
document_info.title(TextStr(title));
}
if let Some(description) = &self.description {
document_info.subject(TextStr(description));
}
if let Some(keywords) = &self.keywords {
let joined = keywords.join(", ");
document_info.keywords(TextStr(&joined));
}
if let Some(authors) = &self.authors {
let joined = authors.join(", ");
document_info.author(TextStr(&joined));
}
if let Some(creator) = &self.creator {
document_info.creator(TextStr(creator));
}
if let Some(producer) = &self.producer {
document_info.producer(TextStr(producer));
}
}
if let Some(date_time) = self.creation_date {
document_info.modified_date(pdf_date(date_time));
document_info.creation_date(pdf_date(date_time));
}
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct DateTime {
pub(crate) year: u16,
pub(crate) month: Option<u8>,
pub(crate) day: Option<u8>,
pub(crate) hour: Option<u8>,
pub(crate) minute: Option<u8>,
pub(crate) second: Option<u8>,
pub(crate) utc_offset_hour: Option<i8>,
pub(crate) utc_offset_minute: u8,
}
impl DateTime {
#[inline]
pub fn new(year: u16) -> Self {
Self {
year: year.min(9999),
month: None,
day: None,
hour: None,
minute: None,
second: None,
utc_offset_hour: None,
utc_offset_minute: 0,
}
}
#[inline]
pub fn month(mut self, month: u8) -> Self {
self.month = Some(month.clamp(1, 12));
self
}
#[inline]
pub fn day(mut self, day: u8) -> Self {
self.day = Some(day.clamp(1, 31));
self
}
#[inline]
pub fn hour(mut self, hour: u8) -> Self {
self.hour = Some(hour.min(23));
self
}
#[inline]
pub fn minute(mut self, minute: u8) -> Self {
self.minute = Some(minute.min(59));
self
}
#[inline]
pub fn second(mut self, second: u8) -> Self {
self.second = Some(second.min(59));
self
}
#[inline]
pub fn utc_offset_hour(mut self, hour: i8) -> Self {
self.utc_offset_hour = Some(hour.clamp(-23, 23));
self
}
#[inline]
pub fn utc_offset_minute(mut self, minute: u8) -> Self {
self.utc_offset_minute = minute.min(59);
self
}
}
pub(crate) fn pdf_date(date_time: DateTime) -> pdf_writer::Date {
pdf_writer::Date::new(date_time.year)
.month(date_time.month.unwrap_or(1))
.day(date_time.day.unwrap_or(1))
.hour(date_time.hour.unwrap_or(0))
.minute(date_time.minute.unwrap_or(0))
.second(date_time.second.unwrap_or(0))
.utc_offset_hour(date_time.utc_offset_hour.unwrap_or(0))
.utc_offset_minute(date_time.utc_offset_minute)
}
fn xmp_date(datetime: DateTime) -> xmp_writer::DateTime {
let timezone = match (datetime.utc_offset_hour, datetime.utc_offset_minute) {
(Some(h), m) => Some(Timezone::Local {
hour: h,
minute: m as i8,
}),
_ => Some(Timezone::Utc),
};
xmp_writer::DateTime {
year: datetime.year,
month: Some(datetime.month.unwrap_or(1)),
day: Some(datetime.day.unwrap_or(1)),
hour: Some(datetime.hour.unwrap_or(0)),
minute: Some(datetime.minute.unwrap_or(0)),
second: Some(datetime.second.unwrap_or(0)),
timezone,
}
}
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
}
impl TextDirection {
pub(crate) fn to_pdf(self) -> pdf_writer::types::Direction {
match self {
TextDirection::LeftToRight => pdf_writer::types::Direction::L2R,
TextDirection::RightToLeft => pdf_writer::types::Direction::R2L,
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum PageLayout {
SinglePage,
OneColumn,
TwoColumnLeft,
TwoColumnRight,
TwoPageLeft,
TwoPageRight,
}
impl PageLayout {
pub(crate) fn to_pdf(self) -> pdf_writer::types::PageLayout {
match self {
PageLayout::SinglePage => pdf_writer::types::PageLayout::SinglePage,
PageLayout::OneColumn => pdf_writer::types::PageLayout::OneColumn,
PageLayout::TwoColumnLeft => pdf_writer::types::PageLayout::TwoColumnLeft,
PageLayout::TwoColumnRight => pdf_writer::types::PageLayout::TwoColumnRight,
PageLayout::TwoPageLeft => pdf_writer::types::PageLayout::TwoPageLeft,
PageLayout::TwoPageRight => pdf_writer::types::PageLayout::TwoPageRight,
}
}
}