use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PdfALevel {
A1a,
A1b,
A2a,
A2b,
A2u,
A3a,
A3b,
A3u,
}
impl PdfALevel {
pub fn part(&self) -> PdfAPart {
match self {
PdfALevel::A1a | PdfALevel::A1b => PdfAPart::Part1,
PdfALevel::A2a | PdfALevel::A2b | PdfALevel::A2u => PdfAPart::Part2,
PdfALevel::A3a | PdfALevel::A3b | PdfALevel::A3u => PdfAPart::Part3,
}
}
pub fn conformance(&self) -> char {
match self {
PdfALevel::A1a | PdfALevel::A2a | PdfALevel::A3a => 'A',
PdfALevel::A1b | PdfALevel::A2b | PdfALevel::A3b => 'B',
PdfALevel::A2u | PdfALevel::A3u => 'U',
}
}
pub fn requires_structure(&self) -> bool {
matches!(self, PdfALevel::A1a | PdfALevel::A2a | PdfALevel::A3a)
}
pub fn requires_unicode(&self) -> bool {
matches!(
self,
PdfALevel::A1a | PdfALevel::A2a | PdfALevel::A2u | PdfALevel::A3a | PdfALevel::A3u
)
}
pub fn allows_transparency(&self) -> bool {
!matches!(self, PdfALevel::A1a | PdfALevel::A1b)
}
pub fn allows_jpeg2000(&self) -> bool {
!matches!(self, PdfALevel::A1a | PdfALevel::A1b)
}
pub fn allows_embedded_files(&self) -> bool {
matches!(self, PdfALevel::A3a | PdfALevel::A3b | PdfALevel::A3u)
}
pub fn xmp_part(&self) -> &'static str {
match self.part() {
PdfAPart::Part1 => "1",
PdfAPart::Part2 => "2",
PdfAPart::Part3 => "3",
}
}
pub fn xmp_conformance(&self) -> &'static str {
match self.conformance() {
'A' => "A",
'B' => "B",
'U' => "U",
_ => "B",
}
}
pub fn from_xmp(part: &str, conformance: &str) -> Option<Self> {
match (part, conformance.to_uppercase().as_str()) {
("1", "A") => Some(PdfALevel::A1a),
("1", "B") => Some(PdfALevel::A1b),
("2", "A") => Some(PdfALevel::A2a),
("2", "B") => Some(PdfALevel::A2b),
("2", "U") => Some(PdfALevel::A2u),
("3", "A") => Some(PdfALevel::A3a),
("3", "B") => Some(PdfALevel::A3b),
("3", "U") => Some(PdfALevel::A3u),
_ => None,
}
}
}
impl fmt::Display for PdfALevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
PdfALevel::A1a => "PDF/A-1a",
PdfALevel::A1b => "PDF/A-1b",
PdfALevel::A2a => "PDF/A-2a",
PdfALevel::A2b => "PDF/A-2b",
PdfALevel::A2u => "PDF/A-2u",
PdfALevel::A3a => "PDF/A-3a",
PdfALevel::A3b => "PDF/A-3b",
PdfALevel::A3u => "PDF/A-3u",
};
write!(f, "{}", name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PdfAPart {
Part1,
Part2,
Part3,
}
impl fmt::Display for PdfAPart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PdfAPart::Part1 => write!(f, "PDF/A-1"),
PdfAPart::Part2 => write!(f, "PDF/A-2"),
PdfAPart::Part3 => write!(f, "PDF/A-3"),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub is_compliant: bool,
pub level: PdfALevel,
pub detected_level: Option<PdfALevel>,
pub errors: Vec<ComplianceError>,
pub warnings: Vec<ComplianceWarning>,
pub stats: ValidationStats,
}
impl Default for ValidationResult {
fn default() -> Self {
Self {
is_compliant: false,
level: PdfALevel::A2b,
detected_level: None,
errors: Vec::new(),
warnings: Vec::new(),
stats: ValidationStats::default(),
}
}
}
impl ValidationResult {
pub fn new(level: PdfALevel) -> Self {
Self {
level,
..Default::default()
}
}
pub fn add_error(&mut self, error: ComplianceError) {
self.errors.push(error);
self.is_compliant = false;
}
pub fn add_warning(&mut self, warning: ComplianceWarning) {
self.warnings.push(warning);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationStats {
pub fonts_checked: usize,
pub fonts_embedded: usize,
pub images_checked: usize,
pub color_spaces_checked: usize,
pub annotations_checked: usize,
pub pages_checked: usize,
}
#[derive(Debug, Clone)]
pub struct ComplianceError {
pub code: ErrorCode,
pub message: String,
pub location: Option<String>,
pub clause: Option<String>,
}
impl ComplianceError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
location: None,
clause: None,
}
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
pub fn with_clause(mut self, clause: impl Into<String>) -> Self {
self.clause = Some(clause.into());
self
}
}
impl fmt::Display for ComplianceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.code, self.message)?;
if let Some(ref loc) = self.location {
write!(f, " (at {})", loc)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
MissingXmpMetadata,
MissingPdfaIdentification,
InvalidPdfaIdentification,
XmpMetadataMismatch,
FontNotEmbedded,
FontMissingTables,
FontInvalidEncoding,
FontMissingToUnicode,
DeviceColorWithoutIntent,
MissingOutputIntent,
InvalidIccProfile,
IccProfileVersionMismatch,
UnsupportedImageCompression,
InvalidImageColorSpace,
LzwCompressionNotAllowed,
MissingDocumentStructure,
InvalidStructureTree,
MissingLanguage,
TransparencyNotAllowed,
JavaScriptNotAllowed,
MultimediaNotAllowed,
ExternalContentNotAllowed,
EncryptionNotAllowed,
InvalidAnnotation,
MissingAppearanceStream,
InvalidAction,
LaunchActionNotAllowed,
EmbeddedFileNotAllowed,
MissingAfRelationship,
PostScriptNotAllowed,
ReferenceXObjectNotAllowed,
OptionalContentIssue,
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = match self {
ErrorCode::MissingXmpMetadata => "XMP-001",
ErrorCode::MissingPdfaIdentification => "XMP-002",
ErrorCode::InvalidPdfaIdentification => "XMP-003",
ErrorCode::XmpMetadataMismatch => "XMP-004",
ErrorCode::FontNotEmbedded => "FONT-001",
ErrorCode::FontMissingTables => "FONT-002",
ErrorCode::FontInvalidEncoding => "FONT-003",
ErrorCode::FontMissingToUnicode => "FONT-004",
ErrorCode::DeviceColorWithoutIntent => "COLOR-001",
ErrorCode::MissingOutputIntent => "COLOR-002",
ErrorCode::InvalidIccProfile => "COLOR-003",
ErrorCode::IccProfileVersionMismatch => "COLOR-004",
ErrorCode::UnsupportedImageCompression => "IMAGE-001",
ErrorCode::InvalidImageColorSpace => "IMAGE-002",
ErrorCode::LzwCompressionNotAllowed => "IMAGE-003",
ErrorCode::MissingDocumentStructure => "STRUCT-001",
ErrorCode::InvalidStructureTree => "STRUCT-002",
ErrorCode::MissingLanguage => "STRUCT-003",
ErrorCode::TransparencyNotAllowed => "CONTENT-001",
ErrorCode::JavaScriptNotAllowed => "CONTENT-002",
ErrorCode::MultimediaNotAllowed => "CONTENT-003",
ErrorCode::ExternalContentNotAllowed => "CONTENT-004",
ErrorCode::EncryptionNotAllowed => "CONTENT-005",
ErrorCode::InvalidAnnotation => "ANNOT-001",
ErrorCode::MissingAppearanceStream => "ANNOT-002",
ErrorCode::InvalidAction => "ACTION-001",
ErrorCode::LaunchActionNotAllowed => "ACTION-002",
ErrorCode::EmbeddedFileNotAllowed => "FILE-001",
ErrorCode::MissingAfRelationship => "FILE-002",
ErrorCode::PostScriptNotAllowed => "XOBJ-001",
ErrorCode::ReferenceXObjectNotAllowed => "XOBJ-002",
ErrorCode::OptionalContentIssue => "OC-001",
};
write!(f, "{}", code)
}
}
#[derive(Debug, Clone)]
pub struct ComplianceWarning {
pub code: WarningCode,
pub message: String,
pub location: Option<String>,
}
impl ComplianceWarning {
pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
location: None,
}
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
}
impl fmt::Display for ComplianceWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.code, self.message)?;
if let Some(ref loc) = self.location {
write!(f, " (at {})", loc)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningCode {
DeprecatedFeature,
LargeFileSize,
MissingRecommendedMetadata,
SmallFontSubset,
HighResolutionImage,
ComplexStructure,
PartialCheck,
}
impl fmt::Display for WarningCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = match self {
WarningCode::DeprecatedFeature => "WARN-001",
WarningCode::LargeFileSize => "WARN-002",
WarningCode::MissingRecommendedMetadata => "WARN-003",
WarningCode::SmallFontSubset => "WARN-004",
WarningCode::HighResolutionImage => "WARN-005",
WarningCode::ComplexStructure => "WARN-006",
WarningCode::PartialCheck => "WARN-007",
};
write!(f, "{}", code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pdf_a_level_properties() {
assert_eq!(PdfALevel::A1a.part(), PdfAPart::Part1);
assert_eq!(PdfALevel::A2b.part(), PdfAPart::Part2);
assert_eq!(PdfALevel::A3u.part(), PdfAPart::Part3);
assert!(PdfALevel::A1a.requires_structure());
assert!(!PdfALevel::A1b.requires_structure());
assert!(PdfALevel::A2a.requires_structure());
assert!(!PdfALevel::A1a.allows_transparency());
assert!(PdfALevel::A2b.allows_transparency());
assert!(!PdfALevel::A2b.allows_embedded_files());
assert!(PdfALevel::A3b.allows_embedded_files());
}
#[test]
fn test_pdf_a_level_xmp() {
assert_eq!(PdfALevel::A1b.xmp_part(), "1");
assert_eq!(PdfALevel::A1b.xmp_conformance(), "B");
assert_eq!(PdfALevel::A2u.xmp_conformance(), "U");
}
#[test]
fn test_pdf_a_level_from_xmp() {
assert_eq!(PdfALevel::from_xmp("1", "A"), Some(PdfALevel::A1a));
assert_eq!(PdfALevel::from_xmp("2", "b"), Some(PdfALevel::A2b));
assert_eq!(PdfALevel::from_xmp("3", "U"), Some(PdfALevel::A3u));
assert_eq!(PdfALevel::from_xmp("4", "A"), None);
}
#[test]
fn test_pdf_a_level_display() {
assert_eq!(format!("{}", PdfALevel::A1b), "PDF/A-1b");
assert_eq!(format!("{}", PdfALevel::A2u), "PDF/A-2u");
}
#[test]
fn test_validation_result() {
let mut result = ValidationResult::new(PdfALevel::A2b);
assert!(!result.is_compliant);
assert!(!result.has_errors());
result.add_error(ComplianceError::new(
ErrorCode::FontNotEmbedded,
"Font 'Arial' is not embedded",
));
assert!(result.has_errors());
assert!(!result.is_compliant);
}
#[test]
fn test_compliance_error_display() {
let error = ComplianceError::new(ErrorCode::FontNotEmbedded, "Font not embedded")
.with_location("Page 1");
let display = format!("{}", error);
assert!(display.contains("[FONT-001]"));
assert!(display.contains("Page 1"));
}
}