Skip to main content

oxidize_pdf/pdfa/
types.rs

1//! Core types for PDF/A compliance
2
3use super::error::ValidationError;
4use std::fmt;
5use std::str::FromStr;
6
7/// PDF/A conformance level
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum PdfAConformance {
10    /// Level B - Basic conformance (visual appearance)
11    B,
12    /// Level U - Unicode conformance (text can be reliably extracted)
13    U,
14    /// Level A - Accessible conformance (tagged PDF, full Unicode mapping)
15    A,
16}
17
18impl fmt::Display for PdfAConformance {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::B => write!(f, "B"),
22            Self::U => write!(f, "U"),
23            Self::A => write!(f, "A"),
24        }
25    }
26}
27
28impl FromStr for PdfAConformance {
29    type Err = String;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        match s.to_uppercase().as_str() {
33            "B" => Ok(Self::B),
34            "U" => Ok(Self::U),
35            "A" => Ok(Self::A),
36            _ => Err(format!("Invalid PDF/A conformance level: {}", s)),
37        }
38    }
39}
40
41/// PDF/A level (part + conformance)
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum PdfALevel {
44    /// PDF/A-1a (ISO 19005-1:2005, Level A - Accessible)
45    A1a,
46    /// PDF/A-1b (ISO 19005-1:2005, Level B - Basic)
47    A1b,
48    /// PDF/A-2a (ISO 19005-2:2011, Level A - Accessible)
49    A2a,
50    /// PDF/A-2b (ISO 19005-2:2011, Level B - Basic)
51    A2b,
52    /// PDF/A-2u (ISO 19005-2:2011, Level U - Unicode)
53    A2u,
54    /// PDF/A-3a (ISO 19005-3:2012, Level A - Accessible)
55    A3a,
56    /// PDF/A-3b (ISO 19005-3:2012, Level B - Basic)
57    A3b,
58    /// PDF/A-3u (ISO 19005-3:2012, Level U - Unicode)
59    A3u,
60}
61
62impl PdfALevel {
63    /// Get the PDF/A part number (1, 2, or 3)
64    pub fn part(&self) -> u8 {
65        match self {
66            Self::A1a | Self::A1b => 1,
67            Self::A2a | Self::A2b | Self::A2u => 2,
68            Self::A3a | Self::A3b | Self::A3u => 3,
69        }
70    }
71
72    /// Get the conformance level
73    pub fn conformance(&self) -> PdfAConformance {
74        match self {
75            Self::A1a | Self::A2a | Self::A3a => PdfAConformance::A,
76            Self::A1b | Self::A2b | Self::A3b => PdfAConformance::B,
77            Self::A2u | Self::A3u => PdfAConformance::U,
78        }
79    }
80
81    /// Get the required PDF version for this level
82    pub fn required_pdf_version(&self) -> &'static str {
83        match self.part() {
84            1 => "1.4",
85            2 | 3 => "1.7",
86            _ => "1.7",
87        }
88    }
89
90    /// Check if transparency is allowed
91    pub fn allows_transparency(&self) -> bool {
92        // Transparency is forbidden in PDF/A-1, allowed in PDF/A-2 and PDF/A-3
93        self.part() >= 2
94    }
95
96    /// Check if LZW compression is allowed
97    pub fn allows_lzw(&self) -> bool {
98        // LZW is forbidden in PDF/A-1, allowed in PDF/A-2 and PDF/A-3
99        self.part() >= 2
100    }
101
102    /// Check if embedded files are allowed
103    pub fn allows_embedded_files(&self) -> bool {
104        // Embedded files are forbidden in PDF/A-1 and PDF/A-2, allowed in PDF/A-3
105        self.part() == 3
106    }
107
108    /// Get the ISO standard reference
109    pub fn iso_reference(&self) -> &'static str {
110        match self.part() {
111            1 => "ISO 19005-1:2005",
112            2 => "ISO 19005-2:2011",
113            3 => "ISO 19005-3:2012",
114            _ => "Unknown",
115        }
116    }
117}
118
119impl fmt::Display for PdfALevel {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "PDF/A-{}{}", self.part(), self.conformance())
122    }
123}
124
125impl FromStr for PdfALevel {
126    type Err = String;
127
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        let s = s.to_uppercase().replace("PDF/A-", "").replace("PDFA", "");
130        match s.as_str() {
131            "1A" => Ok(Self::A1a),
132            "1B" => Ok(Self::A1b),
133            "2A" => Ok(Self::A2a),
134            "2B" => Ok(Self::A2b),
135            "2U" => Ok(Self::A2u),
136            "3A" => Ok(Self::A3a),
137            "3B" => Ok(Self::A3b),
138            "3U" => Ok(Self::A3u),
139            _ => Err(format!("Invalid PDF/A level: {}", s)),
140        }
141    }
142}
143
144/// Warnings during PDF/A validation (informational, don't affect compliance)
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum ValidationWarning {
147    /// Font is subset but may cause issues
148    FontSubsetWarning {
149        /// Font name
150        font_name: String,
151        /// Warning details
152        details: String,
153    },
154    /// Optional metadata field is missing
155    OptionalMetadataMissing {
156        /// Field name
157        field: String,
158    },
159    /// Color profile warning
160    ColorProfileWarning {
161        /// Warning details
162        details: String,
163    },
164    /// File size warning
165    LargeFileWarning {
166        /// File size in bytes
167        size_bytes: u64,
168    },
169}
170
171impl fmt::Display for ValidationWarning {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::FontSubsetWarning { font_name, details } => {
175                write!(f, "Font '{}' subset warning: {}", font_name, details)
176            }
177            Self::OptionalMetadataMissing { field } => {
178                write!(f, "Optional metadata field '{}' is missing", field)
179            }
180            Self::ColorProfileWarning { details } => {
181                write!(f, "Color profile warning: {}", details)
182            }
183            Self::LargeFileWarning { size_bytes } => {
184                write!(
185                    f,
186                    "Large file ({:.2} MB) may cause performance issues",
187                    *size_bytes as f64 / 1_048_576.0
188                )
189            }
190        }
191    }
192}
193
194/// Result of PDF/A validation
195#[derive(Debug, Clone)]
196pub struct ValidationResult {
197    /// The PDF/A level that was validated against
198    level: PdfALevel,
199    /// List of validation errors (empty if compliant)
200    errors: Vec<ValidationError>,
201    /// List of warnings (informational, don't affect compliance)
202    warnings: Vec<ValidationWarning>,
203}
204
205impl ValidationResult {
206    /// Creates a new validation result
207    pub fn new(level: PdfALevel) -> Self {
208        Self {
209            level,
210            errors: Vec::new(),
211            warnings: Vec::new(),
212        }
213    }
214
215    /// Creates a validation result with errors
216    pub fn with_errors(level: PdfALevel, errors: Vec<ValidationError>) -> Self {
217        Self {
218            level,
219            errors,
220            warnings: Vec::new(),
221        }
222    }
223
224    /// Creates a validation result with errors and warnings
225    pub fn with_errors_and_warnings(
226        level: PdfALevel,
227        errors: Vec<ValidationError>,
228        warnings: Vec<ValidationWarning>,
229    ) -> Self {
230        Self {
231            level,
232            errors,
233            warnings,
234        }
235    }
236
237    /// Returns true if the document is compliant (no errors)
238    pub fn is_valid(&self) -> bool {
239        self.errors.is_empty()
240    }
241
242    /// Returns the PDF/A level that was validated against
243    pub fn level(&self) -> PdfALevel {
244        self.level
245    }
246
247    /// Returns the list of validation errors
248    pub fn errors(&self) -> &[ValidationError] {
249        &self.errors
250    }
251
252    /// Returns the list of warnings
253    pub fn warnings(&self) -> &[ValidationWarning] {
254        &self.warnings
255    }
256
257    /// Returns the number of errors
258    pub fn error_count(&self) -> usize {
259        self.errors.len()
260    }
261
262    /// Returns the number of warnings
263    pub fn warning_count(&self) -> usize {
264        self.warnings.len()
265    }
266
267    /// Adds an error to the result
268    pub fn add_error(&mut self, error: ValidationError) {
269        self.errors.push(error);
270    }
271
272    /// Adds a warning to the result
273    pub fn add_warning(&mut self, warning: ValidationWarning) {
274        self.warnings.push(warning);
275    }
276}
277
278impl fmt::Display for ValidationResult {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        if self.is_valid() {
281            write!(f, "{} compliant", self.level)?;
282        } else {
283            write!(
284                f,
285                "{} validation failed: {} error(s)",
286                self.level,
287                self.errors.len()
288            )?;
289        }
290        if !self.warnings.is_empty() {
291            write!(f, ", {} warning(s)", self.warnings.len())?;
292        }
293        Ok(())
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_pdfa_level_part() {
303        assert_eq!(PdfALevel::A1a.part(), 1);
304        assert_eq!(PdfALevel::A1b.part(), 1);
305        assert_eq!(PdfALevel::A2a.part(), 2);
306        assert_eq!(PdfALevel::A2b.part(), 2);
307        assert_eq!(PdfALevel::A2u.part(), 2);
308        assert_eq!(PdfALevel::A3a.part(), 3);
309        assert_eq!(PdfALevel::A3b.part(), 3);
310        assert_eq!(PdfALevel::A3u.part(), 3);
311    }
312
313    #[test]
314    fn test_pdfa_level_conformance() {
315        assert_eq!(PdfALevel::A1a.conformance(), PdfAConformance::A);
316        assert_eq!(PdfALevel::A1b.conformance(), PdfAConformance::B);
317        assert_eq!(PdfALevel::A2a.conformance(), PdfAConformance::A);
318        assert_eq!(PdfALevel::A2b.conformance(), PdfAConformance::B);
319        assert_eq!(PdfALevel::A2u.conformance(), PdfAConformance::U);
320        assert_eq!(PdfALevel::A3a.conformance(), PdfAConformance::A);
321        assert_eq!(PdfALevel::A3b.conformance(), PdfAConformance::B);
322        assert_eq!(PdfALevel::A3u.conformance(), PdfAConformance::U);
323    }
324
325    #[test]
326    fn test_pdfa_level_required_version() {
327        assert_eq!(PdfALevel::A1b.required_pdf_version(), "1.4");
328        assert_eq!(PdfALevel::A2b.required_pdf_version(), "1.7");
329        assert_eq!(PdfALevel::A3b.required_pdf_version(), "1.7");
330    }
331
332    #[test]
333    fn test_pdfa_level_transparency() {
334        assert!(!PdfALevel::A1b.allows_transparency());
335        assert!(PdfALevel::A2b.allows_transparency());
336        assert!(PdfALevel::A3b.allows_transparency());
337    }
338
339    #[test]
340    fn test_pdfa_level_lzw() {
341        assert!(!PdfALevel::A1b.allows_lzw());
342        assert!(PdfALevel::A2b.allows_lzw());
343        assert!(PdfALevel::A3b.allows_lzw());
344    }
345
346    #[test]
347    fn test_pdfa_level_embedded_files() {
348        assert!(!PdfALevel::A1b.allows_embedded_files());
349        assert!(!PdfALevel::A2b.allows_embedded_files());
350        assert!(PdfALevel::A3b.allows_embedded_files());
351    }
352
353    #[test]
354    fn test_pdfa_level_display() {
355        assert_eq!(PdfALevel::A1b.to_string(), "PDF/A-1B");
356        assert_eq!(PdfALevel::A2u.to_string(), "PDF/A-2U");
357        assert_eq!(PdfALevel::A3a.to_string(), "PDF/A-3A");
358    }
359
360    #[test]
361    fn test_pdfa_level_from_str() {
362        assert_eq!("1B".parse::<PdfALevel>().unwrap(), PdfALevel::A1b);
363        assert_eq!("PDF/A-2U".parse::<PdfALevel>().unwrap(), PdfALevel::A2u);
364        assert_eq!("3a".parse::<PdfALevel>().unwrap(), PdfALevel::A3a);
365    }
366
367    #[test]
368    fn test_pdfa_level_from_str_invalid() {
369        assert!("4B".parse::<PdfALevel>().is_err());
370        assert!("invalid".parse::<PdfALevel>().is_err());
371    }
372
373    #[test]
374    fn test_pdfa_conformance_display() {
375        assert_eq!(PdfAConformance::A.to_string(), "A");
376        assert_eq!(PdfAConformance::B.to_string(), "B");
377        assert_eq!(PdfAConformance::U.to_string(), "U");
378    }
379
380    #[test]
381    fn test_pdfa_conformance_from_str() {
382        assert_eq!("A".parse::<PdfAConformance>().unwrap(), PdfAConformance::A);
383        assert_eq!("b".parse::<PdfAConformance>().unwrap(), PdfAConformance::B);
384        assert_eq!("U".parse::<PdfAConformance>().unwrap(), PdfAConformance::U);
385    }
386
387    #[test]
388    fn test_validation_result_new() {
389        let result = ValidationResult::new(PdfALevel::A1b);
390        assert!(result.is_valid());
391        assert_eq!(result.level(), PdfALevel::A1b);
392        assert_eq!(result.error_count(), 0);
393        assert_eq!(result.warning_count(), 0);
394    }
395
396    #[test]
397    fn test_validation_result_with_errors() {
398        let errors = vec![ValidationError::EncryptionForbidden];
399        let result = ValidationResult::with_errors(PdfALevel::A2b, errors);
400        assert!(!result.is_valid());
401        assert_eq!(result.error_count(), 1);
402    }
403
404    #[test]
405    fn test_validation_result_add_error() {
406        let mut result = ValidationResult::new(PdfALevel::A1b);
407        assert!(result.is_valid());
408        result.add_error(ValidationError::XmpMetadataMissing);
409        assert!(!result.is_valid());
410        assert_eq!(result.error_count(), 1);
411    }
412
413    #[test]
414    fn test_validation_result_add_warning() {
415        let mut result = ValidationResult::new(PdfALevel::A1b);
416        result.add_warning(ValidationWarning::OptionalMetadataMissing {
417            field: "Title".to_string(),
418        });
419        assert!(result.is_valid()); // Warnings don't affect validity
420        assert_eq!(result.warning_count(), 1);
421    }
422
423    #[test]
424    fn test_validation_result_display_valid() {
425        let result = ValidationResult::new(PdfALevel::A1b);
426        assert!(result.to_string().contains("compliant"));
427    }
428
429    #[test]
430    fn test_validation_result_display_invalid() {
431        let errors = vec![ValidationError::EncryptionForbidden];
432        let result = ValidationResult::with_errors(PdfALevel::A2b, errors);
433        assert!(result.to_string().contains("failed"));
434        assert!(result.to_string().contains("1 error"));
435    }
436
437    #[test]
438    fn test_pdfa_level_iso_reference() {
439        assert_eq!(PdfALevel::A1b.iso_reference(), "ISO 19005-1:2005");
440        assert_eq!(PdfALevel::A2b.iso_reference(), "ISO 19005-2:2011");
441        assert_eq!(PdfALevel::A3b.iso_reference(), "ISO 19005-3:2012");
442    }
443
444    #[test]
445    fn test_validation_warning_display() {
446        let warning = ValidationWarning::LargeFileWarning {
447            size_bytes: 10_485_760,
448        };
449        assert!(warning.to_string().contains("10.00 MB"));
450    }
451
452    #[test]
453    fn test_pdfa_level_clone_eq() {
454        let level1 = PdfALevel::A1b;
455        let level2 = level1;
456        assert_eq!(level1, level2);
457    }
458
459    #[test]
460    fn test_pdfa_conformance_clone_eq() {
461        let conf1 = PdfAConformance::A;
462        let conf2 = conf1;
463        assert_eq!(conf1, conf2);
464    }
465}