1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ValidationError {
8 EncryptionForbidden,
10
11 FontNotEmbedded {
13 font_name: String,
15 },
16
17 FontMissingToUnicode {
19 font_name: String,
21 },
22
23 JavaScriptForbidden {
25 location: String,
27 },
28
29 XmpMetadataMissing,
31
32 XmpMissingPdfAIdentifier,
34
35 XmpInvalidPdfAIdentifier {
37 details: String,
39 },
40
41 InvalidColorSpace {
43 color_space: String,
45 location: String,
47 },
48
49 MissingOutputIntent,
51
52 TransparencyForbidden {
54 location: String,
56 },
57
58 ExternalReferenceForbidden {
60 reference_type: String,
62 },
63
64 LzwCompressionForbidden {
66 object_id: String,
68 },
69
70 IncompatiblePdfVersion {
72 actual: String,
74 required: String,
76 },
77
78 EmbeddedFileForbidden,
80
81 EmbeddedFileMissingMetadata {
83 file_name: String,
85 missing_field: String,
87 },
88
89 ActionForbidden {
91 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#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum PdfAError {
193 Validation(ValidationError),
195
196 XmpParseError(String),
198
199 InvalidLevel(String),
201
202 ParseError(String),
204
205 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
229pub 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}