use core::fmt;
use pdfrum_object::ObjRef;
use crate::PdfaLevel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Concession {
#[default]
Refuse,
Accept,
}
impl Concession {
#[must_use]
pub const fn accepts(self) -> bool {
matches!(self, Concession::Accept)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Policy {
pub unembeddable_font: Concession,
pub unrepresentable_content: Concession,
pub forbidden_feature: Concession,
}
impl Policy {
#[must_use]
pub const fn strict() -> Self {
Self {
unembeddable_font: Concession::Refuse,
unrepresentable_content: Concession::Refuse,
forbidden_feature: Concession::Refuse,
}
}
#[must_use]
pub const fn lossy() -> Self {
Self {
unembeddable_font: Concession::Accept,
unrepresentable_content: Concession::Accept,
forbidden_feature: Concession::Accept,
}
}
#[must_use]
pub const fn unembeddable_font(mut self, concession: Concession) -> Self {
self.unembeddable_font = concession;
self
}
#[must_use]
pub const fn unrepresentable_content(mut self, concession: Concession) -> Self {
self.unrepresentable_content = concession;
self
}
#[must_use]
pub const fn forbidden_feature(mut self, concession: Concession) -> Self {
self.forbidden_feature = concession;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Compromise {
FontSubstituted {
font: ObjRef,
base_name: String,
substitute: &'static str,
},
ActionRemoved {
holder: ObjRef,
kind: String,
},
AnnotationRemoved {
annotation: ObjRef,
page: u32,
subtype: String,
},
AnnotationFlagsChanged {
annotation: ObjRef,
page: u32,
},
PageRasterized {
page: u32,
dpi: Dpi,
cause: RasterCause,
},
EmbeddedFileRemoved {
file: Option<ObjRef>,
name: String,
},
OptionalContentRemoved {
catalog: ObjRef,
},
ObjectMetadataRemoved {
object: ObjRef,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Dpi(pub u16);
impl fmt::Display for Dpi {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} dpi", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum RasterCause {
Transparency,
UnembeddableFont,
}
impl fmt::Display for RasterCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
RasterCause::Transparency => "the page uses transparency, which this level forbids",
RasterCause::UnembeddableFont => "the page draws with a font that cannot be embedded",
})
}
}
impl fmt::Display for Compromise {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Compromise::FontSubstituted {
base_name,
substitute,
..
} => write!(
f,
"substituted {substitute} for the unembedded /{base_name}"
),
Compromise::ActionRemoved { kind, .. } => write!(f, "removed a /{kind} action"),
Compromise::AnnotationRemoved { page, subtype, .. } => {
write!(f, "removed a /{subtype} annotation from page {}", page + 1)
}
Compromise::AnnotationFlagsChanged { page, .. } => {
write!(f, "corrected an annotation's flags on page {}", page + 1)
}
Compromise::PageRasterized { page, dpi, cause } => {
write!(f, "rasterized page {} at {dpi}: {cause}", page + 1)
}
Compromise::EmbeddedFileRemoved { name, .. } => {
write!(f, "removed the embedded file {name}")
}
Compromise::OptionalContentRemoved { .. } => {
f.write_str("removed the optional-content configuration")
}
Compromise::ObjectMetadataRemoved { object } => write!(
f,
"removed the XMP packet on object {} {}",
object.num, object.generation
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Refusal {
UnembeddableFont {
font: ObjRef,
base_name: String,
},
UnrepresentableContent {
page: u32,
cause: RasterCause,
},
ForbiddenFeature {
holder: ObjRef,
kind: String,
},
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Refusal::UnembeddableFont { base_name, .. } => write!(
f,
"/{base_name} is not embedded and its program was not found; \
set Policy::unembeddable_font to accept a substitute"
),
Refusal::UnrepresentableContent { page, cause } => write!(
f,
"page {}: {cause}; set Policy::unrepresentable_content to \
accept a rasterized page",
page + 1
),
Refusal::ForbiddenFeature { kind, .. } => write!(
f,
"the document carries {kind}, which PDF/A forbids; set \
Policy::forbidden_feature to accept its removal"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conversion {
pub level: PdfaLevel,
pub compromises: Vec<Compromise>,
pub refusals: Vec<Refusal>,
}
impl Conversion {
#[must_use]
pub fn converted(&self) -> bool {
self.refusals.is_empty()
}
#[must_use]
pub fn faithful(&self) -> bool {
self.converted() && self.compromises.is_empty()
}
#[must_use]
pub fn rasterized_pages(&self) -> Vec<u32> {
let mut pages: Vec<u32> = self
.compromises
.iter()
.filter_map(|c| match c {
Compromise::PageRasterized { page, .. } => Some(*page),
_ => None,
})
.collect();
pages.sort_unstable();
pages.dedup();
pages
}
}
impl fmt::Display for Conversion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.converted() {
write!(f, "{} refused: ", self.level)?;
for (i, refusal) in self.refusals.iter().enumerate() {
if i > 0 {
f.write_str("; ")?;
}
write!(f, "{refusal}")?;
}
return Ok(());
}
if self.compromises.is_empty() {
return write!(f, "{} conversion, faithful", self.level);
}
write!(
f,
"{} conversion with {} compromise(s)",
self.level,
self.compromises.len()
)
}
}