printwell-pdf 0.1.5

PDF manipulation features (forms, signing) for Printwell
Documentation
//! PDF/UA (Universal Accessibility) compliance support.
//!
//! This module provides validation and conversion capabilities for PDF/UA,
//! the ISO standard for accessible PDF documents (ISO 14289).
//!
//! **Note:** This feature requires a commercial license.
//! Purchase at: <https://printwell.dev/pricing>

use crate::{PdfUAError, Result};
use typed_builder::TypedBuilder;

/// PDF/UA conformance identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfUALevel {
    /// PDF/UA-1 (ISO 14289-1:2014)
    PdfUA1,
    /// PDF/UA-2 (ISO 14289-2:2024)
    PdfUA2,
}

impl PdfUALevel {
    /// Get the ISO standard identifier.
    #[must_use]
    pub const fn iso_identifier(&self) -> &'static str {
        match self {
            Self::PdfUA1 => "ISO 14289-1",
            Self::PdfUA2 => "ISO 14289-2",
        }
    }

    /// Get the PDF/UA part number.
    #[must_use]
    pub const fn part(&self) -> i32 {
        match self {
            Self::PdfUA1 => 1,
            Self::PdfUA2 => 2,
        }
    }
}

impl std::fmt::Display for PdfUALevel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "PDF/UA-{}", self.part())
    }
}

/// Error returned when parsing a `PdfUALevel` from a string fails.
#[derive(Debug, Clone)]
pub struct ParsePdfUALevelError(String);

impl std::fmt::Display for ParsePdfUALevelError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for ParsePdfUALevelError {}

impl std::str::FromStr for PdfUALevel {
    type Err = ParsePdfUALevelError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "1" | "ua1" | "pdfua1" | "pdf/ua-1" => Ok(Self::PdfUA1),
            "2" | "ua2" | "pdfua2" | "pdf/ua-2" => Ok(Self::PdfUA2),
            _ => Err(ParsePdfUALevelError(format!(
                "Unknown PDF/UA level: {s}. Use '1' or '2'."
            ))),
        }
    }
}

/// Severity of a PDF/UA accessibility issue.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueSeverity {
    /// Critical: Must be fixed
    Error,
    /// Warning: Should be fixed
    Warning,
    /// Info: Informational note
    Info,
}

impl std::fmt::Display for IssueSeverity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Error => write!(f, "error"),
            Self::Warning => write!(f, "warning"),
            Self::Info => write!(f, "info"),
        }
    }
}

/// Category of a PDF/UA accessibility issue.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueCategory {
    /// Document structure issues
    Structure,
    /// Alt text issues
    AltText,
    /// Language issues
    Language,
    /// Heading hierarchy issues
    Headings,
    /// Table structure issues
    Tables,
    /// List structure issues
    Lists,
    /// Link issues
    Links,
    /// Form field issues
    Forms,
    /// Color contrast issues
    Color,
    /// Reading order issues
    ReadingOrder,
}

impl std::fmt::Display for IssueCategory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Structure => write!(f, "Structure"),
            Self::AltText => write!(f, "Alt Text"),
            Self::Language => write!(f, "Language"),
            Self::Headings => write!(f, "Headings"),
            Self::Tables => write!(f, "Tables"),
            Self::Lists => write!(f, "Lists"),
            Self::Links => write!(f, "Links"),
            Self::Forms => write!(f, "Forms"),
            Self::Color => write!(f, "Color"),
            Self::ReadingOrder => write!(f, "Reading Order"),
        }
    }
}

/// An accessibility issue found during validation.
#[derive(Debug, Clone)]
pub struct PdfUAIssue {
    /// Issue severity
    pub severity: IssueSeverity,
    /// Issue category
    pub category: IssueCategory,
    /// Human-readable description
    pub description: String,
    /// PDF/UA clause reference
    pub clause: Option<String>,
    /// Suggestion for fixing the issue
    pub suggestion: Option<String>,
    /// WCAG guideline reference
    pub wcag_ref: Option<String>,
    /// Page number where issue occurs (0 = document-level)
    pub page: u32,
    /// Element path or identifier
    pub element: Option<String>,
}

/// Result of PDF/UA validation.
#[derive(Debug, Clone)]
pub struct ValidationResult {
    /// PDF/UA level validated against
    pub level: PdfUALevel,
    /// Whether the document is compliant
    pub is_compliant: bool,
    /// List of issues found
    pub issues: Vec<PdfUAIssue>,
    /// Number of errors
    pub error_count: usize,
    /// Number of warnings
    pub warning_count: usize,
    /// Number of pages checked
    pub pages_checked: usize,
    /// Number of tagged elements found
    pub tagged_elements: usize,
}

impl ValidationResult {
    /// Check if validation passed (no errors)
    #[must_use]
    pub const fn passed(&self) -> bool {
        self.error_count == 0
    }
}

/// Validate a PDF against PDF/UA requirements.
///
/// **Note:** This feature requires a commercial license.
///
/// # Errors
///
/// Always returns an error as this feature requires a commercial license.
pub fn validate_pdfua(_pdf_data: &[u8], _level: PdfUALevel) -> Result<ValidationResult> {
    Err(PdfUAError::RequiresLicense.into())
}

/// Generate XMP metadata for PDF/UA identification.
///
/// **Note:** This feature requires a commercial license.
#[must_use]
pub const fn generate_pdfua_xmp(
    _level: PdfUALevel,
    _language: &str,
    _title: Option<&str>,
    _author: Option<&str>,
) -> String {
    String::new()
}

/// Add PDF/UA metadata to a PDF.
///
/// **Note:** This feature requires a commercial license.
///
/// # Errors
///
/// Always returns an error as this feature requires a commercial license.
pub fn add_pdfua_metadata(
    _pdf_data: &[u8],
    _level: PdfUALevel,
    _options: &AccessibilityOptions,
) -> Result<Vec<u8>> {
    Err(PdfUAError::RequiresLicense.into())
}

/// Options for accessibility enhancements.
#[derive(Debug, Clone, Default, TypedBuilder)]
#[builder(field_defaults(default, setter(into)))]
pub struct AccessibilityOptions {
    /// Document language (e.g., "en-US")
    #[builder(default)]
    pub language: String,
    /// Document title
    #[builder(default)]
    pub title: String,
    /// PDF/UA level to target
    #[builder(default)]
    pub level: Option<PdfUALevel>,
}