1use super::error::ValidationError;
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum PdfAConformance {
10 B,
12 U,
14 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum PdfALevel {
44 A1a,
46 A1b,
48 A2a,
50 A2b,
52 A2u,
54 A3a,
56 A3b,
58 A3u,
60}
61
62impl PdfALevel {
63 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 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 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 pub fn allows_transparency(&self) -> bool {
92 self.part() >= 2
94 }
95
96 pub fn allows_lzw(&self) -> bool {
98 self.part() >= 2
100 }
101
102 pub fn allows_embedded_files(&self) -> bool {
104 self.part() == 3
106 }
107
108 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#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum ValidationWarning {
147 FontSubsetWarning {
149 font_name: String,
151 details: String,
153 },
154 OptionalMetadataMissing {
156 field: String,
158 },
159 ColorProfileWarning {
161 details: String,
163 },
164 LargeFileWarning {
166 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#[derive(Debug, Clone)]
196pub struct ValidationResult {
197 level: PdfALevel,
199 errors: Vec<ValidationError>,
201 warnings: Vec<ValidationWarning>,
203}
204
205impl ValidationResult {
206 pub fn new(level: PdfALevel) -> Self {
208 Self {
209 level,
210 errors: Vec::new(),
211 warnings: Vec::new(),
212 }
213 }
214
215 pub fn with_errors(level: PdfALevel, errors: Vec<ValidationError>) -> Self {
217 Self {
218 level,
219 errors,
220 warnings: Vec::new(),
221 }
222 }
223
224 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 pub fn is_valid(&self) -> bool {
239 self.errors.is_empty()
240 }
241
242 pub fn level(&self) -> PdfALevel {
244 self.level
245 }
246
247 pub fn errors(&self) -> &[ValidationError] {
249 &self.errors
250 }
251
252 pub fn warnings(&self) -> &[ValidationWarning] {
254 &self.warnings
255 }
256
257 pub fn error_count(&self) -> usize {
259 self.errors.len()
260 }
261
262 pub fn warning_count(&self) -> usize {
264 self.warnings.len()
265 }
266
267 pub fn add_error(&mut self, error: ValidationError) {
269 self.errors.push(error);
270 }
271
272 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()); 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}