Skip to main content

oxidize_pdf/pdfa/
error.rs

1//! Error types for PDF/A validation
2
3use std::fmt;
4
5/// Errors that can occur during PDF/A validation
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ValidationError {
8    /// Document is encrypted (encryption forbidden in PDF/A)
9    EncryptionForbidden,
10
11    /// Font is not embedded in the document
12    FontNotEmbedded {
13        /// Name of the font that is not embedded
14        font_name: String,
15    },
16
17    /// Font is missing required ToUnicode CMap
18    FontMissingToUnicode {
19        /// Name of the font missing ToUnicode
20        font_name: String,
21    },
22
23    /// JavaScript is present (forbidden in PDF/A)
24    JavaScriptForbidden {
25        /// Location where JavaScript was found
26        location: String,
27    },
28
29    /// XMP metadata is missing or invalid
30    XmpMetadataMissing,
31
32    /// XMP metadata is missing PDF/A identifier
33    XmpMissingPdfAIdentifier,
34
35    /// XMP metadata has invalid PDF/A identifier
36    XmpInvalidPdfAIdentifier {
37        /// Details about the invalid identifier
38        details: String,
39    },
40
41    /// Invalid or device-dependent color space
42    InvalidColorSpace {
43        /// Name of the invalid color space
44        color_space: String,
45        /// Location where it was found
46        location: String,
47    },
48
49    /// Missing output intent for device-dependent colors
50    MissingOutputIntent,
51
52    /// Transparency is forbidden (PDF/A-1b)
53    TransparencyForbidden {
54        /// Location where transparency was found
55        location: String,
56    },
57
58    /// External reference found (forbidden in PDF/A)
59    ExternalReferenceForbidden {
60        /// Type of external reference
61        reference_type: String,
62    },
63
64    /// LZW compression is forbidden (PDF/A-1b)
65    LzwCompressionForbidden {
66        /// Object ID where LZW was found
67        object_id: String,
68    },
69
70    /// PDF version is incompatible with the requested PDF/A level
71    IncompatiblePdfVersion {
72        /// Actual PDF version
73        actual: String,
74        /// Required PDF version
75        required: String,
76    },
77
78    /// Embedded file is not allowed (PDF/A-1b, PDF/A-2b)
79    EmbeddedFileForbidden,
80
81    /// Embedded file is missing required metadata (PDF/A-3b)
82    EmbeddedFileMissingMetadata {
83        /// Name of the file
84        file_name: String,
85        /// Missing field
86        missing_field: String,
87    },
88
89    /// Actions are forbidden (certain action types)
90    ActionForbidden {
91        /// Type of action
92        action_type: String,
93    },
94}
95
96impl fmt::Display for ValidationError {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::EncryptionForbidden => {
100                write!(f, "Encryption is forbidden in PDF/A documents")
101            }
102            Self::FontNotEmbedded { font_name } => {
103                write!(f, "Font '{}' is not embedded in the document", font_name)
104            }
105            Self::FontMissingToUnicode { font_name } => {
106                write!(f, "Font '{}' is missing required ToUnicode CMap", font_name)
107            }
108            Self::JavaScriptForbidden { location } => {
109                write!(
110                    f,
111                    "JavaScript is forbidden in PDF/A (found at {})",
112                    location
113                )
114            }
115            Self::XmpMetadataMissing => {
116                write!(f, "XMP metadata is required but missing")
117            }
118            Self::XmpMissingPdfAIdentifier => {
119                write!(f, "XMP metadata is missing PDF/A identification")
120            }
121            Self::XmpInvalidPdfAIdentifier { details } => {
122                write!(f, "Invalid PDF/A identifier in XMP metadata: {}", details)
123            }
124            Self::InvalidColorSpace {
125                color_space,
126                location,
127            } => {
128                write!(
129                    f,
130                    "Invalid color space '{}' at {} (device-independent color spaces required)",
131                    color_space, location
132                )
133            }
134            Self::MissingOutputIntent => {
135                write!(
136                    f,
137                    "Output intent is required when using device-dependent color spaces"
138                )
139            }
140            Self::TransparencyForbidden { location } => {
141                write!(
142                    f,
143                    "Transparency is forbidden in PDF/A-1 (found at {})",
144                    location
145                )
146            }
147            Self::ExternalReferenceForbidden { reference_type } => {
148                write!(
149                    f,
150                    "External references are forbidden in PDF/A (type: {})",
151                    reference_type
152                )
153            }
154            Self::LzwCompressionForbidden { object_id } => {
155                write!(
156                    f,
157                    "LZW compression is forbidden in PDF/A-1 (object {})",
158                    object_id
159                )
160            }
161            Self::IncompatiblePdfVersion { actual, required } => {
162                write!(
163                    f,
164                    "PDF version {} is incompatible (required: {})",
165                    actual, required
166                )
167            }
168            Self::EmbeddedFileForbidden => {
169                write!(f, "Embedded files are forbidden in PDF/A-1 and PDF/A-2")
170            }
171            Self::EmbeddedFileMissingMetadata {
172                file_name,
173                missing_field,
174            } => {
175                write!(
176                    f,
177                    "Embedded file '{}' is missing required metadata: {}",
178                    file_name, missing_field
179                )
180            }
181            Self::ActionForbidden { action_type } => {
182                write!(f, "Action type '{}' is forbidden in PDF/A", action_type)
183            }
184        }
185    }
186}
187
188impl std::error::Error for ValidationError {}
189
190/// General errors for PDF/A operations
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum PdfAError {
193    /// Validation error
194    Validation(ValidationError),
195
196    /// XMP parsing error
197    XmpParseError(String),
198
199    /// Invalid PDF/A level string
200    InvalidLevel(String),
201
202    /// Document parsing error
203    ParseError(String),
204
205    /// IO error (as string for Clone)
206    IoError(String),
207}
208
209impl fmt::Display for PdfAError {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Validation(err) => write!(f, "PDF/A validation error: {}", err),
213            Self::XmpParseError(msg) => write!(f, "XMP parsing error: {}", msg),
214            Self::InvalidLevel(level) => write!(f, "Invalid PDF/A level: '{}'", level),
215            Self::ParseError(msg) => write!(f, "PDF parsing error: {}", msg),
216            Self::IoError(msg) => write!(f, "IO error: {}", msg),
217        }
218    }
219}
220
221impl std::error::Error for PdfAError {}
222
223impl From<ValidationError> for PdfAError {
224    fn from(err: ValidationError) -> Self {
225        PdfAError::Validation(err)
226    }
227}
228
229/// Result type for PDF/A operations
230pub type PdfAResult<T> = std::result::Result<T, PdfAError>;
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_validation_error_display_encryption() {
238        let err = ValidationError::EncryptionForbidden;
239        assert!(err.to_string().contains("Encryption"));
240        assert!(err.to_string().contains("forbidden"));
241    }
242
243    #[test]
244    fn test_validation_error_display_font_not_embedded() {
245        let err = ValidationError::FontNotEmbedded {
246            font_name: "Arial".to_string(),
247        };
248        let msg = err.to_string();
249        assert!(msg.contains("Arial"));
250        assert!(msg.contains("not embedded"));
251    }
252
253    #[test]
254    fn test_validation_error_display_javascript() {
255        let err = ValidationError::JavaScriptForbidden {
256            location: "Page 1".to_string(),
257        };
258        let msg = err.to_string();
259        assert!(msg.contains("JavaScript"));
260        assert!(msg.contains("Page 1"));
261    }
262
263    #[test]
264    fn test_validation_error_display_xmp_missing() {
265        let err = ValidationError::XmpMetadataMissing;
266        assert!(err.to_string().contains("XMP"));
267        assert!(err.to_string().contains("missing"));
268    }
269
270    #[test]
271    fn test_validation_error_display_invalid_colorspace() {
272        let err = ValidationError::InvalidColorSpace {
273            color_space: "DeviceRGB".to_string(),
274            location: "Image XObject".to_string(),
275        };
276        let msg = err.to_string();
277        assert!(msg.contains("DeviceRGB"));
278        assert!(msg.contains("Image XObject"));
279    }
280
281    #[test]
282    fn test_validation_error_display_transparency() {
283        let err = ValidationError::TransparencyForbidden {
284            location: "Page 3".to_string(),
285        };
286        let msg = err.to_string();
287        assert!(msg.contains("Transparency"));
288        assert!(msg.contains("Page 3"));
289    }
290
291    #[test]
292    fn test_validation_error_display_lzw() {
293        let err = ValidationError::LzwCompressionForbidden {
294            object_id: "15 0".to_string(),
295        };
296        let msg = err.to_string();
297        assert!(msg.contains("LZW"));
298        assert!(msg.contains("15 0"));
299    }
300
301    #[test]
302    fn test_validation_error_display_pdf_version() {
303        let err = ValidationError::IncompatiblePdfVersion {
304            actual: "1.7".to_string(),
305            required: "1.4".to_string(),
306        };
307        let msg = err.to_string();
308        assert!(msg.contains("1.7"));
309        assert!(msg.contains("1.4"));
310    }
311
312    #[test]
313    fn test_pdfa_error_from_validation_error() {
314        let validation_err = ValidationError::EncryptionForbidden;
315        let pdfa_err: PdfAError = validation_err.into();
316        assert!(matches!(pdfa_err, PdfAError::Validation(_)));
317    }
318
319    #[test]
320    fn test_pdfa_error_display() {
321        let err = PdfAError::InvalidLevel("PDF/A-4".to_string());
322        assert!(err.to_string().contains("PDF/A-4"));
323    }
324
325    #[test]
326    fn test_validation_error_is_send_sync() {
327        fn assert_send_sync<T: Send + Sync>() {}
328        assert_send_sync::<ValidationError>();
329        assert_send_sync::<PdfAError>();
330    }
331
332    #[test]
333    fn test_validation_error_clone() {
334        let err = ValidationError::FontNotEmbedded {
335            font_name: "Times".to_string(),
336        };
337        let cloned = err.clone();
338        assert_eq!(err, cloned);
339    }
340
341    #[test]
342    fn test_validation_error_debug() {
343        let err = ValidationError::XmpMetadataMissing;
344        let debug_str = format!("{:?}", err);
345        assert!(debug_str.contains("XmpMetadataMissing"));
346    }
347}