1use super::error::{PdfAError, ValidationError};
7use super::types::{PdfAConformance, PdfALevel, ValidationResult, ValidationWarning};
8use super::xmp::XmpMetadata;
9use crate::parser::{ParseOptions, PdfReader};
10use std::io::{Read, Seek};
11
12struct CatalogData {
14 metadata_ref: Option<(u32, u16)>,
15 names_ref: Option<(u32, u16)>,
16 names_inline: Option<crate::parser::objects::PdfDictionary>,
17 open_action_ref: Option<(u32, u16)>,
18 open_action_inline: Option<crate::parser::objects::PdfDictionary>,
19 aa_ref: Option<(u32, u16)>,
20 aa_inline: Option<crate::parser::objects::PdfDictionary>,
21}
22
23#[derive(Debug, Clone)]
46pub struct PdfAValidator {
47 level: PdfALevel,
49 collect_all_errors: bool,
51}
52
53impl PdfAValidator {
54 pub fn new(level: PdfALevel) -> Self {
56 Self {
57 level,
58 collect_all_errors: true,
59 }
60 }
61
62 pub fn collect_all_errors(mut self, collect: bool) -> Self {
64 self.collect_all_errors = collect;
65 self
66 }
67
68 pub fn level(&self) -> PdfALevel {
70 self.level
71 }
72
73 pub fn validate<R: Read + Seek>(
77 &self,
78 reader: &mut PdfReader<R>,
79 ) -> Result<ValidationResult, PdfAError> {
80 let mut errors = Vec::new();
81 let mut warnings = Vec::new();
82
83 self.check_encryption(reader, &mut errors);
85
86 self.check_pdf_version(reader, &mut errors)?;
88
89 let catalog_data = self.extract_catalog_data(reader)?;
91
92 self.check_metadata_from_data(reader, &catalog_data, &mut errors, &mut warnings)?;
94
95 self.check_javascript_from_data(reader, &catalog_data, &mut errors)?;
97
98 self.check_external_references_from_data(reader, &catalog_data, &mut errors)?;
100
101 if !self.level.allows_transparency() {
103 self.check_transparency(reader, &mut errors)?;
104 }
105
106 if !self.level.allows_lzw() {
108 self.check_lzw_compression(reader, &mut errors)?;
109 }
110
111 self.check_embedded_files(reader, &catalog_data, &mut errors)?;
113
114 self.check_fonts(reader, &mut errors)?;
116
117 self.check_color_spaces(reader, &mut errors)?;
119
120 Ok(ValidationResult::with_errors_and_warnings(
121 self.level, errors, warnings,
122 ))
123 }
124
125 fn check_encryption<R: Read + Seek>(
127 &self,
128 reader: &PdfReader<R>,
129 errors: &mut Vec<ValidationError>,
130 ) {
131 if reader.is_encrypted() {
132 errors.push(ValidationError::EncryptionForbidden);
133 }
134 }
135
136 fn check_pdf_version<R: Read + Seek>(
138 &self,
139 reader: &PdfReader<R>,
140 errors: &mut Vec<ValidationError>,
141 ) -> Result<(), PdfAError> {
142 let version = reader.version();
143 let version_str = version.to_string();
144
145 let required = self.level.required_pdf_version();
146
147 let actual_parts: Vec<u8> = version_str
149 .split('.')
150 .filter_map(|s| s.parse().ok())
151 .collect();
152
153 let (actual_major, actual_minor) = (
155 actual_parts.first().copied().unwrap_or(1),
156 actual_parts.get(1).copied().unwrap_or(0),
157 );
158
159 let is_compatible = match self.level.part() {
162 1 => actual_major == 1 && actual_minor == 4,
163 2 | 3 => actual_major == 1 && actual_minor >= 4 && actual_minor <= 7,
164 _ => false,
165 };
166
167 if !is_compatible {
168 errors.push(ValidationError::IncompatiblePdfVersion {
169 actual: version_str,
170 required: required.to_string(),
171 });
172 }
173
174 Ok(())
175 }
176
177 fn extract_catalog_data<R: Read + Seek>(
179 &self,
180 reader: &mut PdfReader<R>,
181 ) -> Result<CatalogData, PdfAError> {
182 let catalog = reader
183 .catalog()
184 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
185
186 let metadata_ref = catalog
187 .get("Metadata")
188 .and_then(|obj| obj.as_reference())
189 .map(|(n, g)| (n, g));
190
191 let names_ref = catalog
192 .get("Names")
193 .and_then(|obj| obj.as_reference())
194 .map(|(n, g)| (n, g));
195
196 let names_inline = catalog.get("Names").and_then(|obj| obj.as_dict()).cloned();
197
198 let open_action_ref = catalog
199 .get("OpenAction")
200 .and_then(|obj| obj.as_reference())
201 .map(|(n, g)| (n, g));
202
203 let open_action_inline = catalog
204 .get("OpenAction")
205 .and_then(|obj| obj.as_dict())
206 .cloned();
207
208 let aa_ref = catalog
209 .get("AA")
210 .and_then(|obj| obj.as_reference())
211 .map(|(n, g)| (n, g));
212
213 let aa_inline = catalog.get("AA").and_then(|obj| obj.as_dict()).cloned();
214
215 Ok(CatalogData {
216 metadata_ref,
217 names_ref,
218 names_inline,
219 open_action_ref,
220 open_action_inline,
221 aa_ref,
222 aa_inline,
223 })
224 }
225
226 fn check_metadata_from_data<R: Read + Seek>(
228 &self,
229 reader: &mut PdfReader<R>,
230 catalog_data: &CatalogData,
231 errors: &mut Vec<ValidationError>,
232 _warnings: &mut Vec<ValidationWarning>,
233 ) -> Result<(), PdfAError> {
234 let metadata_ref = match catalog_data.metadata_ref {
236 Some(r) => r,
237 None => {
238 errors.push(ValidationError::XmpMetadataMissing);
239 return Ok(());
240 }
241 };
242
243 let obj = reader
245 .get_object(metadata_ref.0, metadata_ref.1)
246 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
247
248 let stream = match obj.as_stream() {
250 Some(s) => s,
251 None => {
252 errors.push(ValidationError::XmpMetadataMissing);
253 return Ok(());
254 }
255 };
256
257 let decoded = match stream.decode(&ParseOptions::default()) {
264 Ok(data) => data,
265 Err(_) => {
266 errors.push(ValidationError::XmpMetadataMissing);
269 return Ok(());
270 }
271 };
272
273 let xmp_data = String::from_utf8_lossy(&decoded);
275 let xmp = match XmpMetadata::parse(&xmp_data) {
276 Ok(x) => x,
277 Err(_) => {
278 errors.push(ValidationError::XmpMetadataMissing);
279 return Ok(());
280 }
281 };
282
283 match &xmp.pdfa_id {
285 None => {
286 errors.push(ValidationError::XmpMissingPdfAIdentifier);
287 }
288 Some(pdfa_id) => {
289 let expected_part = self.level.part();
291 let expected_conformance = self.level.conformance();
292
293 if pdfa_id.part != expected_part {
294 errors.push(ValidationError::XmpInvalidPdfAIdentifier {
295 details: format!(
296 "Part mismatch: expected {}, found {}",
297 expected_part, pdfa_id.part
298 ),
299 });
300 } else if pdfa_id.conformance != expected_conformance {
301 errors.push(ValidationError::XmpInvalidPdfAIdentifier {
302 details: format!(
303 "Conformance mismatch: expected {:?}, found {:?}",
304 expected_conformance, pdfa_id.conformance
305 ),
306 });
307 }
308 }
309 }
310
311 Ok(())
312 }
313
314 fn check_javascript_from_data<R: Read + Seek>(
316 &self,
317 reader: &mut PdfReader<R>,
318 catalog_data: &CatalogData,
319 errors: &mut Vec<ValidationError>,
320 ) -> Result<(), PdfAError> {
321 if let Some((obj_num, gen_num)) = catalog_data.names_ref {
323 let names_obj = reader
324 .get_object(obj_num, gen_num)
325 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
326
327 if let Some(names_dict) = names_obj.as_dict() {
328 if names_dict.get("JavaScript").is_some() {
329 errors.push(ValidationError::JavaScriptForbidden {
330 location: "Names/JavaScript".to_string(),
331 });
332 }
333 }
334 } else if let Some(ref names_dict) = catalog_data.names_inline {
335 if names_dict.get("JavaScript").is_some() {
336 errors.push(ValidationError::JavaScriptForbidden {
337 location: "Names/JavaScript".to_string(),
338 });
339 }
340 }
341
342 if let Some((obj_num, gen_num)) = catalog_data.open_action_ref {
344 let action_obj = reader
345 .get_object(obj_num, gen_num)
346 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
347
348 if let Some(action_dict) = action_obj.as_dict() {
349 if self.is_javascript_action(action_dict) {
350 errors.push(ValidationError::JavaScriptForbidden {
351 location: "OpenAction".to_string(),
352 });
353 }
354 }
355 } else if let Some(ref action_dict) = catalog_data.open_action_inline {
356 if self.is_javascript_action(action_dict) {
357 errors.push(ValidationError::JavaScriptForbidden {
358 location: "OpenAction".to_string(),
359 });
360 }
361 }
362
363 if let Some((obj_num, gen_num)) = catalog_data.aa_ref {
365 let aa_obj = reader
366 .get_object(obj_num, gen_num)
367 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
368
369 if let Some(aa_dict) = aa_obj.as_dict().cloned() {
370 if self.check_aa_dict_for_javascript(reader, &aa_dict)? {
371 errors.push(ValidationError::JavaScriptForbidden {
372 location: "Catalog/AA".to_string(),
373 });
374 }
375 }
376 } else if let Some(ref aa_dict) = catalog_data.aa_inline {
377 if self.check_aa_dict_for_javascript(reader, aa_dict)? {
378 errors.push(ValidationError::JavaScriptForbidden {
379 location: "Catalog/AA".to_string(),
380 });
381 }
382 }
383
384 Ok(())
385 }
386
387 fn is_javascript_action(&self, dict: &crate::parser::objects::PdfDictionary) -> bool {
389 if let Some(action_type) = dict.get("S") {
390 if let Some(name) = action_type.as_name() {
391 return name.0 == "JavaScript";
392 }
393 }
394 false
395 }
396
397 fn check_aa_dict_for_javascript<R: Read + Seek>(
399 &self,
400 reader: &mut PdfReader<R>,
401 aa_dict: &crate::parser::objects::PdfDictionary,
402 ) -> Result<bool, PdfAError> {
403 for (_key, value) in aa_dict.0.iter() {
405 let action_dict = if let Some((obj_num, gen_num)) = value.as_reference() {
406 let obj = reader
407 .get_object(obj_num, gen_num)
408 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
409 obj.as_dict().cloned()
410 } else {
411 value.as_dict().cloned()
412 };
413
414 if let Some(dict) = action_dict {
415 if self.is_javascript_action(&dict) {
416 return Ok(true);
417 }
418 }
419 }
420
421 Ok(false)
422 }
423
424 fn check_external_references_from_data<R: Read + Seek>(
426 &self,
427 reader: &mut PdfReader<R>,
428 catalog_data: &CatalogData,
429 errors: &mut Vec<ValidationError>,
430 ) -> Result<(), PdfAError> {
431 if let Some((obj_num, gen_num)) = catalog_data.open_action_ref {
433 let action_obj = reader
434 .get_object(obj_num, gen_num)
435 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
436
437 if let Some(action_dict) = action_obj.as_dict() {
438 self.check_for_external_action(action_dict, errors);
439 }
440 } else if let Some(ref action_dict) = catalog_data.open_action_inline {
441 self.check_for_external_action(action_dict, errors);
442 }
443
444 Ok(())
445 }
446
447 fn check_for_external_action(
449 &self,
450 dict: &crate::parser::objects::PdfDictionary,
451 errors: &mut Vec<ValidationError>,
452 ) {
453 if let Some(action_type) = dict.get("S") {
454 if let Some(name) = action_type.as_name() {
455 if name.0 == "GoToR" || name.0 == "GoToE" || name.0 == "Launch" {
456 errors.push(ValidationError::ExternalReferenceForbidden {
457 reference_type: name.0.clone(),
458 });
459 }
460 }
461 }
462 }
463
464 fn check_transparency<R: Read + Seek>(
466 &self,
467 reader: &mut PdfReader<R>,
468 errors: &mut Vec<ValidationError>,
469 ) -> Result<(), PdfAError> {
470 let page_count = reader
471 .page_count()
472 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
473
474 for page_idx in 0..page_count {
475 let page_dict = self.get_page_dict(reader, page_idx)?;
477
478 if let Some(resources) = self.get_resources_dict(reader, &page_dict)? {
480 if let Some(ext_gstate) = resources.get("ExtGState") {
482 self.check_ext_gstate_transparency(reader, ext_gstate, page_idx, errors)?;
483 }
484
485 if let Some(xobjects) = resources.get("XObject") {
487 self.check_xobject_transparency(reader, xobjects, page_idx, errors)?;
488 }
489 }
490 }
491
492 Ok(())
493 }
494
495 fn get_page_dict<R: Read + Seek>(
497 &self,
498 reader: &mut PdfReader<R>,
499 page_idx: u32,
500 ) -> Result<crate::parser::objects::PdfDictionary, PdfAError> {
501 let pages_dict = reader
503 .pages()
504 .map_err(|e| PdfAError::ParseError(e.to_string()))?
505 .clone();
506
507 let kids = pages_dict
509 .get("Kids")
510 .and_then(|k| k.as_array())
511 .ok_or_else(|| PdfAError::ParseError("Pages missing Kids array".to_string()))?;
512
513 let page_ref = kids
515 .0
516 .get(page_idx as usize)
517 .ok_or_else(|| PdfAError::ParseError(format!("Page {} not found", page_idx)))?;
518
519 if let Some((obj_num, gen_num)) = page_ref.as_reference() {
521 let page_obj = reader
522 .get_object(obj_num, gen_num)
523 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
524 page_obj
525 .as_dict()
526 .cloned()
527 .ok_or_else(|| PdfAError::ParseError("Page is not a dictionary".to_string()))
528 } else if let Some(dict) = page_ref.as_dict() {
529 Ok(dict.clone())
530 } else {
531 Err(PdfAError::ParseError("Invalid page reference".to_string()))
532 }
533 }
534
535 fn get_resources_dict<R: Read + Seek>(
537 &self,
538 reader: &mut PdfReader<R>,
539 page_dict: &crate::parser::objects::PdfDictionary,
540 ) -> Result<Option<crate::parser::objects::PdfDictionary>, PdfAError> {
541 let resources_obj = match page_dict.get("Resources") {
542 Some(obj) => obj,
543 None => return Ok(None),
544 };
545
546 if let Some((obj_num, gen_num)) = resources_obj.as_reference() {
547 let resolved = reader
548 .get_object(obj_num, gen_num)
549 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
550 Ok(resolved.as_dict().cloned())
551 } else {
552 Ok(resources_obj.as_dict().cloned())
553 }
554 }
555
556 fn check_ext_gstate_transparency<R: Read + Seek>(
558 &self,
559 reader: &mut PdfReader<R>,
560 ext_gstate_obj: &crate::parser::objects::PdfObject,
561 page_idx: u32,
562 errors: &mut Vec<ValidationError>,
563 ) -> Result<(), PdfAError> {
564 let ext_gstate_dict = if let Some((obj_num, gen_num)) = ext_gstate_obj.as_reference() {
565 let obj = reader
566 .get_object(obj_num, gen_num)
567 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
568 obj.as_dict().cloned()
569 } else {
570 ext_gstate_obj.as_dict().cloned()
571 };
572
573 let ext_gstate_dict = match ext_gstate_dict {
574 Some(d) => d,
575 None => return Ok(()),
576 };
577
578 for (gs_name, gs_value) in ext_gstate_dict.0.iter() {
580 let gs_dict = if let Some((obj_num, gen_num)) = gs_value.as_reference() {
581 let obj = reader
582 .get_object(obj_num, gen_num)
583 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
584 obj.as_dict().cloned()
585 } else {
586 gs_value.as_dict().cloned()
587 };
588
589 if let Some(gs_dict) = gs_dict {
590 if let Some(ca) = gs_dict.get("CA") {
592 let val = ca.as_real().or_else(|| ca.as_integer().map(|i| i as f64));
593 if let Some(val) = val {
594 if (val - 1.0).abs() > f64::EPSILON {
595 errors.push(ValidationError::TransparencyForbidden {
596 location: format!(
597 "Page {}, ExtGState/{}/CA",
598 page_idx + 1,
599 &gs_name.0
600 ),
601 });
602 }
603 }
604 }
605
606 if let Some(ca) = gs_dict.get("ca") {
608 let val = ca.as_real().or_else(|| ca.as_integer().map(|i| i as f64));
609 if let Some(val) = val {
610 if (val - 1.0).abs() > f64::EPSILON {
611 errors.push(ValidationError::TransparencyForbidden {
612 location: format!(
613 "Page {}, ExtGState/{}/ca",
614 page_idx + 1,
615 &gs_name.0
616 ),
617 });
618 }
619 }
620 }
621
622 if let Some(smask) = gs_dict.get("SMask") {
624 let is_none = smask.as_name().map(|n| n.0 == "None").unwrap_or(false);
626 if !is_none {
627 errors.push(ValidationError::TransparencyForbidden {
628 location: format!(
629 "Page {}, ExtGState/{}/SMask",
630 page_idx + 1,
631 &gs_name.0
632 ),
633 });
634 }
635 }
636
637 if let Some(bm) = gs_dict.get("BM") {
639 if let Some(name) = bm.as_name() {
640 if name.0 != "Normal" && name.0 != "Compatible" {
641 errors.push(ValidationError::TransparencyForbidden {
642 location: format!(
643 "Page {}, ExtGState/{}/BM={}",
644 page_idx + 1,
645 &gs_name.0,
646 &name.0
647 ),
648 });
649 }
650 }
651 }
652 }
653 }
654
655 Ok(())
656 }
657
658 fn check_xobject_transparency<R: Read + Seek>(
660 &self,
661 reader: &mut PdfReader<R>,
662 xobject_obj: &crate::parser::objects::PdfObject,
663 page_idx: u32,
664 errors: &mut Vec<ValidationError>,
665 ) -> Result<(), PdfAError> {
666 let xobject_dict = if let Some((obj_num, gen_num)) = xobject_obj.as_reference() {
667 let obj = reader
668 .get_object(obj_num, gen_num)
669 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
670 obj.as_dict().cloned()
671 } else {
672 xobject_obj.as_dict().cloned()
673 };
674
675 let xobject_dict = match xobject_dict {
676 Some(d) => d,
677 None => return Ok(()),
678 };
679
680 for (xo_name, xo_value) in xobject_dict.0.iter() {
682 let xo_stream_dict = if let Some((obj_num, gen_num)) = xo_value.as_reference() {
683 let obj = reader
684 .get_object(obj_num, gen_num)
685 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
686 if let Some(stream) = obj.as_stream() {
687 Some(stream.dict.clone())
688 } else {
689 obj.as_dict().cloned()
690 }
691 } else if let Some(stream) = xo_value.as_stream() {
692 Some(stream.dict.clone())
693 } else {
694 xo_value.as_dict().cloned()
695 };
696
697 if let Some(xo_dict) = xo_stream_dict {
698 if let Some(group) = xo_dict.get("Group") {
700 let group_dict = if let Some((obj_num, gen_num)) = group.as_reference() {
701 let obj = reader
702 .get_object(obj_num, gen_num)
703 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
704 obj.as_dict().cloned()
705 } else {
706 group.as_dict().cloned()
707 };
708
709 if let Some(group_dict) = group_dict {
710 if let Some(s) = group_dict.get("S") {
711 if let Some(name) = s.as_name() {
712 if name.0 == "Transparency" {
713 errors.push(ValidationError::TransparencyForbidden {
714 location: format!(
715 "Page {}, XObject/{} has transparency group",
716 page_idx + 1,
717 &xo_name.0
718 ),
719 });
720 }
721 }
722 }
723 }
724 }
725
726 if let Some(subtype) = xo_dict.get("Subtype") {
728 if let Some(name) = subtype.as_name() {
729 if name.0 == "Image" {
730 if xo_dict.get("SMask").is_some() {
731 errors.push(ValidationError::TransparencyForbidden {
732 location: format!(
733 "Page {}, Image XObject/{} has SMask",
734 page_idx + 1,
735 &xo_name.0
736 ),
737 });
738 }
739 }
740 }
741 }
742 }
743 }
744
745 Ok(())
746 }
747
748 fn check_lzw_compression<R: Read + Seek>(
753 &self,
754 reader: &mut PdfReader<R>,
755 errors: &mut Vec<ValidationError>,
756 ) -> Result<(), PdfAError> {
757 let page_count = reader
758 .page_count()
759 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
760
761 for page_idx in 0..page_count {
762 let page_dict = self.get_page_dict(reader, page_idx)?;
763
764 if let Some(resources) = self.get_resources_dict(reader, &page_dict)? {
766 if let Some(xobjects) = resources.get("XObject") {
767 self.check_xobjects_for_lzw(reader, xobjects, page_idx, errors)?;
768 }
769 }
770
771 if let Some(contents) = page_dict.get("Contents") {
773 self.check_contents_for_lzw(reader, contents, page_idx, errors)?;
774 }
775 }
776
777 Ok(())
778 }
779
780 fn check_xobjects_for_lzw<R: Read + Seek>(
782 &self,
783 reader: &mut PdfReader<R>,
784 xobject_obj: &crate::parser::objects::PdfObject,
785 page_idx: u32,
786 errors: &mut Vec<ValidationError>,
787 ) -> Result<(), PdfAError> {
788 let xobject_dict = if let Some((obj_num, gen_num)) = xobject_obj.as_reference() {
789 let obj = reader
790 .get_object(obj_num, gen_num)
791 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
792 obj.as_dict().cloned()
793 } else {
794 xobject_obj.as_dict().cloned()
795 };
796
797 let xobject_dict = match xobject_dict {
798 Some(d) => d,
799 None => return Ok(()),
800 };
801
802 for (_xo_name, xo_value) in xobject_dict.0.iter() {
803 if let Some((obj_num, gen_num)) = xo_value.as_reference() {
804 if let Ok(obj) = reader.get_object(obj_num, gen_num) {
805 if let Some(stream) = obj.as_stream() {
806 self.check_stream_for_lzw(&stream.dict, obj_num, page_idx, errors);
807 }
808 }
809 }
810 }
811
812 Ok(())
813 }
814
815 fn check_contents_for_lzw<R: Read + Seek>(
817 &self,
818 reader: &mut PdfReader<R>,
819 contents: &crate::parser::objects::PdfObject,
820 page_idx: u32,
821 errors: &mut Vec<ValidationError>,
822 ) -> Result<(), PdfAError> {
823 if let Some((obj_num, gen_num)) = contents.as_reference() {
825 if let Ok(obj) = reader.get_object(obj_num, gen_num) {
826 if let Some(stream) = obj.as_stream() {
827 self.check_stream_for_lzw(&stream.dict, obj_num, page_idx, errors);
828 }
829 }
830 } else if let Some(arr) = contents.as_array() {
831 for item in &arr.0 {
832 if let Some((obj_num, gen_num)) = item.as_reference() {
833 if let Ok(obj) = reader.get_object(obj_num, gen_num) {
834 if let Some(stream) = obj.as_stream() {
835 self.check_stream_for_lzw(&stream.dict, obj_num, page_idx, errors);
836 }
837 }
838 }
839 }
840 }
841
842 Ok(())
843 }
844
845 fn check_stream_for_lzw(
847 &self,
848 dict: &crate::parser::objects::PdfDictionary,
849 obj_num: u32,
850 page_idx: u32,
851 errors: &mut Vec<ValidationError>,
852 ) {
853 if let Some(filter) = dict.get("Filter") {
854 if let Some(name) = filter.as_name() {
856 if name.0 == "LZWDecode" {
857 errors.push(ValidationError::LzwCompressionForbidden {
858 object_id: format!("page {}, object {} 0", page_idx + 1, obj_num),
859 });
860 }
861 } else if let Some(arr) = filter.as_array() {
862 for (idx, f) in arr.0.iter().enumerate() {
863 if let Some(name) = f.as_name() {
864 if name.0 == "LZWDecode" {
865 errors.push(ValidationError::LzwCompressionForbidden {
866 object_id: format!(
867 "page {}, object {} 0 (filter {})",
868 page_idx + 1,
869 obj_num,
870 idx
871 ),
872 });
873 }
874 }
875 }
876 }
877 }
878 }
879
880 fn check_embedded_files<R: Read + Seek>(
882 &self,
883 reader: &mut PdfReader<R>,
884 catalog_data: &CatalogData,
885 errors: &mut Vec<ValidationError>,
886 ) -> Result<(), PdfAError> {
887 if self.level.allows_embedded_files() {
888 return Ok(());
891 }
892
893 if let Some((obj_num, gen_num)) = catalog_data.names_ref {
895 let names_obj = reader
896 .get_object(obj_num, gen_num)
897 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
898
899 if let Some(names_dict) = names_obj.as_dict() {
900 if names_dict.get("EmbeddedFiles").is_some() {
901 errors.push(ValidationError::EmbeddedFileForbidden);
902 }
903 }
904 } else if let Some(ref names_dict) = catalog_data.names_inline {
905 if names_dict.get("EmbeddedFiles").is_some() {
906 errors.push(ValidationError::EmbeddedFileForbidden);
907 }
908 }
909
910 Ok(())
911 }
912
913 fn check_fonts<R: Read + Seek>(
915 &self,
916 reader: &mut PdfReader<R>,
917 errors: &mut Vec<ValidationError>,
918 ) -> Result<(), PdfAError> {
919 let page_count = reader
920 .page_count()
921 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
922 let requires_tounicode = self.level.conformance() == PdfAConformance::A;
923
924 for page_idx in 0..page_count {
925 let page_dict = self.get_page_dict(reader, page_idx)?;
926
927 if let Some(resources) = self.get_resources_dict(reader, &page_dict)? {
928 if let Some(fonts_obj) = resources.get("Font") {
929 self.check_font_resources(reader, fonts_obj, requires_tounicode, errors)?;
930 }
931 }
932 }
933
934 Ok(())
935 }
936
937 fn check_font_resources<R: Read + Seek>(
939 &self,
940 reader: &mut PdfReader<R>,
941 fonts_obj: &crate::parser::objects::PdfObject,
942 requires_tounicode: bool,
943 errors: &mut Vec<ValidationError>,
944 ) -> Result<(), PdfAError> {
945 let fonts_dict = if let Some((obj_num, gen_num)) = fonts_obj.as_reference() {
946 let obj = reader
947 .get_object(obj_num, gen_num)
948 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
949 obj.as_dict().cloned()
950 } else {
951 fonts_obj.as_dict().cloned()
952 };
953
954 let fonts_dict = match fonts_dict {
955 Some(d) => d,
956 None => return Ok(()),
957 };
958
959 for (font_name, font_ref) in fonts_dict.0.iter() {
961 let font_dict = if let Some((obj_num, gen_num)) = font_ref.as_reference() {
962 let obj = reader
963 .get_object(obj_num, gen_num)
964 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
965 obj.as_dict().cloned()
966 } else {
967 font_ref.as_dict().cloned()
968 };
969
970 if let Some(font_dict) = font_dict {
971 self.check_single_font(
972 reader,
973 &font_name.0,
974 &font_dict,
975 requires_tounicode,
976 errors,
977 )?;
978 }
979 }
980
981 Ok(())
982 }
983
984 fn check_single_font<R: Read + Seek>(
986 &self,
987 reader: &mut PdfReader<R>,
988 font_name: &str,
989 font_dict: &crate::parser::objects::PdfDictionary,
990 requires_tounicode: bool,
991 errors: &mut Vec<ValidationError>,
992 ) -> Result<(), PdfAError> {
993 let font_type = font_dict
995 .get("Subtype")
996 .and_then(|s| s.as_name())
997 .map(|n| n.0.clone())
998 .unwrap_or_default();
999
1000 if font_type == "Type3" {
1002 if requires_tounicode && font_dict.get("ToUnicode").is_none() {
1005 errors.push(ValidationError::FontMissingToUnicode {
1006 font_name: font_name.to_string(),
1007 });
1008 }
1009 return Ok(());
1010 }
1011
1012 if font_type == "Type0" {
1014 return self.check_type0_font(reader, font_name, font_dict, requires_tounicode, errors);
1015 }
1016
1017 let font_descriptor = self.get_font_descriptor(reader, font_dict)?;
1019
1020 if let Some(desc) = font_descriptor {
1021 let has_fontfile = desc.get("FontFile").is_some()
1023 || desc.get("FontFile2").is_some()
1024 || desc.get("FontFile3").is_some();
1025
1026 if !has_fontfile {
1027 errors.push(ValidationError::FontNotEmbedded {
1028 font_name: font_name.to_string(),
1029 });
1030 }
1031 } else {
1032 errors.push(ValidationError::FontNotEmbedded {
1036 font_name: font_name.to_string(),
1037 });
1038 }
1039
1040 if requires_tounicode && font_dict.get("ToUnicode").is_none() {
1042 let has_encoding = font_dict.get("Encoding").is_some();
1044 if !has_encoding {
1045 errors.push(ValidationError::FontMissingToUnicode {
1046 font_name: font_name.to_string(),
1047 });
1048 }
1049 }
1050
1051 Ok(())
1052 }
1053
1054 fn check_type0_font<R: Read + Seek>(
1056 &self,
1057 reader: &mut PdfReader<R>,
1058 font_name: &str,
1059 font_dict: &crate::parser::objects::PdfDictionary,
1060 requires_tounicode: bool,
1061 errors: &mut Vec<ValidationError>,
1062 ) -> Result<(), PdfAError> {
1063 let descendants = match font_dict.get("DescendantFonts") {
1065 Some(d) => d,
1066 None => {
1067 errors.push(ValidationError::FontNotEmbedded {
1068 font_name: font_name.to_string(),
1069 });
1070 return Ok(());
1071 }
1072 };
1073
1074 let desc_array = if let Some((obj_num, gen_num)) = descendants.as_reference() {
1075 let obj = reader
1076 .get_object(obj_num, gen_num)
1077 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1078 obj.as_array().cloned()
1079 } else {
1080 descendants.as_array().cloned()
1081 };
1082
1083 let desc_array = match desc_array {
1084 Some(a) => a,
1085 None => return Ok(()),
1086 };
1087
1088 if let Some(cid_font_ref) = desc_array.0.first() {
1090 let cid_font_dict = if let Some((obj_num, gen_num)) = cid_font_ref.as_reference() {
1091 let obj = reader
1092 .get_object(obj_num, gen_num)
1093 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1094 obj.as_dict().cloned()
1095 } else {
1096 cid_font_ref.as_dict().cloned()
1097 };
1098
1099 if let Some(cid_dict) = cid_font_dict {
1100 let font_descriptor = self.get_font_descriptor(reader, &cid_dict)?;
1102
1103 if let Some(desc) = font_descriptor {
1104 let has_fontfile = desc.get("FontFile").is_some()
1105 || desc.get("FontFile2").is_some()
1106 || desc.get("FontFile3").is_some();
1107
1108 if !has_fontfile {
1109 errors.push(ValidationError::FontNotEmbedded {
1110 font_name: font_name.to_string(),
1111 });
1112 }
1113 } else {
1114 errors.push(ValidationError::FontNotEmbedded {
1115 font_name: font_name.to_string(),
1116 });
1117 }
1118 }
1119 }
1120
1121 if requires_tounicode && font_dict.get("ToUnicode").is_none() {
1123 let encoding = font_dict.get("Encoding").and_then(|e| e.as_name());
1126 let is_identity = encoding
1127 .map(|n| n.0 == "Identity-H" || n.0 == "Identity-V")
1128 .unwrap_or(false);
1129
1130 if !is_identity {
1131 errors.push(ValidationError::FontMissingToUnicode {
1132 font_name: font_name.to_string(),
1133 });
1134 }
1135 }
1136
1137 Ok(())
1138 }
1139
1140 fn get_font_descriptor<R: Read + Seek>(
1142 &self,
1143 reader: &mut PdfReader<R>,
1144 font_dict: &crate::parser::objects::PdfDictionary,
1145 ) -> Result<Option<crate::parser::objects::PdfDictionary>, PdfAError> {
1146 let desc_ref = match font_dict.get("FontDescriptor") {
1147 Some(d) => d,
1148 None => return Ok(None),
1149 };
1150
1151 if let Some((obj_num, gen_num)) = desc_ref.as_reference() {
1152 let obj = reader
1153 .get_object(obj_num, gen_num)
1154 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1155 Ok(obj.as_dict().cloned())
1156 } else {
1157 Ok(desc_ref.as_dict().cloned())
1158 }
1159 }
1160
1161 fn check_color_spaces<R: Read + Seek>(
1167 &self,
1168 reader: &mut PdfReader<R>,
1169 errors: &mut Vec<ValidationError>,
1170 ) -> Result<(), PdfAError> {
1171 let has_output_intent = self.has_output_intent(reader)?;
1173
1174 let page_count = reader
1175 .page_count()
1176 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1177
1178 for page_idx in 0..page_count {
1179 let page_dict = self.get_page_dict(reader, page_idx)?;
1180
1181 if let Some(resources) = self.get_resources_dict(reader, &page_dict)? {
1182 if let Some(cs_obj) = resources.get("ColorSpace") {
1184 self.check_colorspace_dict(
1185 reader,
1186 cs_obj,
1187 page_idx,
1188 has_output_intent,
1189 errors,
1190 )?;
1191 }
1192
1193 if let Some(xobjects) = resources.get("XObject") {
1195 self.check_xobject_colorspaces(
1196 reader,
1197 xobjects,
1198 page_idx,
1199 has_output_intent,
1200 errors,
1201 )?;
1202 }
1203 }
1204 }
1205
1206 Ok(())
1207 }
1208
1209 fn has_output_intent<R: Read + Seek>(
1211 &self,
1212 reader: &mut PdfReader<R>,
1213 ) -> Result<bool, PdfAError> {
1214 let catalog = reader
1215 .catalog()
1216 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1217
1218 if let Some(output_intents) = catalog.get("OutputIntents") {
1219 let arr = if let Some((obj_num, gen_num)) = output_intents.as_reference() {
1220 let obj = reader
1221 .get_object(obj_num, gen_num)
1222 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1223 obj.as_array().cloned()
1224 } else {
1225 output_intents.as_array().cloned()
1226 };
1227
1228 if let Some(arr) = arr {
1229 for item in &arr.0 {
1231 let intent_dict = if let Some((obj_num, gen_num)) = item.as_reference() {
1232 let obj = reader
1233 .get_object(obj_num, gen_num)
1234 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1235 obj.as_dict().cloned()
1236 } else {
1237 item.as_dict().cloned()
1238 };
1239
1240 if let Some(dict) = intent_dict {
1241 if let Some(subtype) = dict.get("S") {
1243 if let Some(name) = subtype.as_name() {
1244 if name.0.contains("PDFA") || name.0.contains("PDF/A") {
1245 return Ok(true);
1246 }
1247 }
1248 }
1249 if dict.get("DestOutputProfile").is_some() {
1251 return Ok(true);
1252 }
1253 }
1254 }
1255 }
1256 }
1257
1258 Ok(false)
1259 }
1260
1261 fn check_colorspace_dict<R: Read + Seek>(
1263 &self,
1264 reader: &mut PdfReader<R>,
1265 cs_obj: &crate::parser::objects::PdfObject,
1266 page_idx: u32,
1267 has_output_intent: bool,
1268 errors: &mut Vec<ValidationError>,
1269 ) -> Result<(), PdfAError> {
1270 let cs_dict = if let Some((obj_num, gen_num)) = cs_obj.as_reference() {
1271 let obj = reader
1272 .get_object(obj_num, gen_num)
1273 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1274 obj.as_dict().cloned()
1275 } else {
1276 cs_obj.as_dict().cloned()
1277 };
1278
1279 let cs_dict = match cs_dict {
1280 Some(d) => d,
1281 None => return Ok(()),
1282 };
1283
1284 for (cs_name, cs_value) in cs_dict.0.iter() {
1285 self.validate_colorspace(
1286 reader,
1287 &cs_name.0,
1288 cs_value,
1289 page_idx,
1290 has_output_intent,
1291 errors,
1292 )?;
1293 }
1294
1295 Ok(())
1296 }
1297
1298 fn validate_colorspace<R: Read + Seek>(
1300 &self,
1301 reader: &mut PdfReader<R>,
1302 cs_name: &str,
1303 cs_value: &crate::parser::objects::PdfObject,
1304 page_idx: u32,
1305 has_output_intent: bool,
1306 errors: &mut Vec<ValidationError>,
1307 ) -> Result<(), PdfAError> {
1308 let cs_type = if let Some(name) = cs_value.as_name() {
1310 name.0.clone()
1311 } else if let Some(arr) = cs_value.as_array() {
1312 arr.0
1314 .first()
1315 .and_then(|o| o.as_name())
1316 .map(|n| n.0.clone())
1317 .unwrap_or_default()
1318 } else if let Some((obj_num, gen_num)) = cs_value.as_reference() {
1319 let obj = reader
1321 .get_object(obj_num, gen_num)
1322 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1323 if let Some(name) = obj.as_name() {
1324 name.0.clone()
1325 } else if let Some(arr) = obj.as_array() {
1326 arr.0
1327 .first()
1328 .and_then(|o| o.as_name())
1329 .map(|n| n.0.clone())
1330 .unwrap_or_default()
1331 } else {
1332 return Ok(());
1333 }
1334 } else {
1335 return Ok(());
1336 };
1337
1338 if self.is_device_dependent_colorspace(&cs_type) && !has_output_intent {
1340 errors.push(ValidationError::InvalidColorSpace {
1341 color_space: cs_type,
1342 location: format!("Page {}, ColorSpace/{}", page_idx + 1, cs_name),
1343 });
1344 }
1345
1346 Ok(())
1347 }
1348
1349 fn check_xobject_colorspaces<R: Read + Seek>(
1351 &self,
1352 reader: &mut PdfReader<R>,
1353 xobjects_obj: &crate::parser::objects::PdfObject,
1354 page_idx: u32,
1355 has_output_intent: bool,
1356 errors: &mut Vec<ValidationError>,
1357 ) -> Result<(), PdfAError> {
1358 let xobjects_dict = if let Some((obj_num, gen_num)) = xobjects_obj.as_reference() {
1359 let obj = reader
1360 .get_object(obj_num, gen_num)
1361 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1362 obj.as_dict().cloned()
1363 } else {
1364 xobjects_obj.as_dict().cloned()
1365 };
1366
1367 let xobjects_dict = match xobjects_dict {
1368 Some(d) => d,
1369 None => return Ok(()),
1370 };
1371
1372 for (xo_name, xo_ref) in xobjects_dict.0.iter() {
1373 let xo_dict = if let Some((obj_num, gen_num)) = xo_ref.as_reference() {
1374 let obj = reader
1375 .get_object(obj_num, gen_num)
1376 .map_err(|e| PdfAError::ParseError(e.to_string()))?;
1377 if let Some(stream) = obj.as_stream() {
1378 Some(stream.dict.clone())
1379 } else {
1380 obj.as_dict().cloned()
1381 }
1382 } else if let Some(stream) = xo_ref.as_stream() {
1383 Some(stream.dict.clone())
1384 } else {
1385 xo_ref.as_dict().cloned()
1386 };
1387
1388 if let Some(dict) = xo_dict {
1389 let is_image = dict
1391 .get("Subtype")
1392 .and_then(|s| s.as_name())
1393 .map(|n| n.0 == "Image")
1394 .unwrap_or(false);
1395
1396 if is_image {
1397 if let Some(cs) = dict.get("ColorSpace") {
1399 let cs_type = if let Some(name) = cs.as_name() {
1400 name.0.clone()
1401 } else if let Some(arr) = cs.as_array() {
1402 arr.0
1403 .first()
1404 .and_then(|o| o.as_name())
1405 .map(|n| n.0.clone())
1406 .unwrap_or_default()
1407 } else {
1408 String::new()
1409 };
1410
1411 if self.is_device_dependent_colorspace(&cs_type) && !has_output_intent {
1412 errors.push(ValidationError::InvalidColorSpace {
1413 color_space: cs_type,
1414 location: format!("Page {}, XObject/{}", page_idx + 1, &xo_name.0),
1415 });
1416 }
1417 }
1418 }
1419 }
1420 }
1421
1422 Ok(())
1423 }
1424
1425 fn is_device_dependent_colorspace(&self, cs_type: &str) -> bool {
1427 matches!(cs_type, "DeviceRGB" | "DeviceCMYK" | "DeviceGray")
1428 }
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433 use super::*;
1434
1435 #[test]
1436 fn test_validator_new() {
1437 let validator = PdfAValidator::new(PdfALevel::A1b);
1438 assert_eq!(validator.level(), PdfALevel::A1b);
1439 }
1440
1441 #[test]
1442 fn test_validator_level_a2b() {
1443 let validator = PdfAValidator::new(PdfALevel::A2b);
1444 assert_eq!(validator.level(), PdfALevel::A2b);
1445 }
1446
1447 #[test]
1448 fn test_validator_level_a3b() {
1449 let validator = PdfAValidator::new(PdfALevel::A3b);
1450 assert_eq!(validator.level(), PdfALevel::A3b);
1451 }
1452
1453 #[test]
1454 fn test_validator_collect_all_errors() {
1455 let validator = PdfAValidator::new(PdfALevel::A1b).collect_all_errors(false);
1456 assert!(!validator.collect_all_errors);
1457 }
1458
1459 #[test]
1460 fn test_validator_clone() {
1461 let validator = PdfAValidator::new(PdfALevel::A2u);
1462 let cloned = validator.clone();
1463 assert_eq!(cloned.level(), PdfALevel::A2u);
1464 }
1465
1466 #[test]
1467 fn test_validator_debug() {
1468 let validator = PdfAValidator::new(PdfALevel::A1a);
1469 let debug_str = format!("{:?}", validator);
1470 assert!(debug_str.contains("PdfAValidator"));
1471 assert!(debug_str.contains("A1a"));
1472 }
1473
1474 #[test]
1475 fn test_is_javascript_action_true() {
1476 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1477
1478 let validator = PdfAValidator::new(PdfALevel::A1b);
1479 let mut dict = PdfDictionary::new();
1480 dict.insert(
1481 "S".to_string(),
1482 PdfObject::Name(PdfName("JavaScript".to_string())),
1483 );
1484
1485 assert!(validator.is_javascript_action(&dict));
1486 }
1487
1488 #[test]
1489 fn test_is_javascript_action_false() {
1490 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1491
1492 let validator = PdfAValidator::new(PdfALevel::A1b);
1493 let mut dict = PdfDictionary::new();
1494 dict.insert(
1495 "S".to_string(),
1496 PdfObject::Name(PdfName("GoTo".to_string())),
1497 );
1498
1499 assert!(!validator.is_javascript_action(&dict));
1500 }
1501
1502 #[test]
1503 fn test_is_javascript_action_no_s_key() {
1504 use crate::parser::objects::PdfDictionary;
1505
1506 let validator = PdfAValidator::new(PdfALevel::A1b);
1507 let dict = PdfDictionary::new();
1508
1509 assert!(!validator.is_javascript_action(&dict));
1510 }
1511
1512 #[test]
1513 fn test_check_for_external_action_gotor() {
1514 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1515
1516 let validator = PdfAValidator::new(PdfALevel::A1b);
1517 let mut dict = PdfDictionary::new();
1518 dict.insert(
1519 "S".to_string(),
1520 PdfObject::Name(PdfName("GoToR".to_string())),
1521 );
1522
1523 let mut errors = Vec::new();
1524 validator.check_for_external_action(&dict, &mut errors);
1525
1526 assert_eq!(errors.len(), 1);
1527 assert!(matches!(
1528 errors[0],
1529 ValidationError::ExternalReferenceForbidden { .. }
1530 ));
1531 }
1532
1533 #[test]
1534 fn test_check_for_external_action_launch() {
1535 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1536
1537 let validator = PdfAValidator::new(PdfALevel::A1b);
1538 let mut dict = PdfDictionary::new();
1539 dict.insert(
1540 "S".to_string(),
1541 PdfObject::Name(PdfName("Launch".to_string())),
1542 );
1543
1544 let mut errors = Vec::new();
1545 validator.check_for_external_action(&dict, &mut errors);
1546
1547 assert_eq!(errors.len(), 1);
1548 }
1549
1550 #[test]
1551 fn test_check_for_external_action_goto_internal() {
1552 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1553
1554 let validator = PdfAValidator::new(PdfALevel::A1b);
1555 let mut dict = PdfDictionary::new();
1556 dict.insert(
1557 "S".to_string(),
1558 PdfObject::Name(PdfName("GoTo".to_string())),
1559 );
1560
1561 let mut errors = Vec::new();
1562 validator.check_for_external_action(&dict, &mut errors);
1563
1564 assert_eq!(errors.len(), 0); }
1566
1567 #[test]
1568 fn test_check_stream_for_lzw_single_filter() {
1569 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1570
1571 let validator = PdfAValidator::new(PdfALevel::A1b);
1572 let mut dict = PdfDictionary::new();
1573 dict.insert(
1574 "Filter".to_string(),
1575 PdfObject::Name(PdfName("LZWDecode".to_string())),
1576 );
1577
1578 let mut errors = Vec::new();
1579 validator.check_stream_for_lzw(&dict, 10, 0, &mut errors);
1580
1581 assert_eq!(errors.len(), 1);
1582 assert!(matches!(
1583 &errors[0],
1584 ValidationError::LzwCompressionForbidden { object_id } if object_id == "page 1, object 10 0"
1585 ));
1586 }
1587
1588 #[test]
1589 fn test_check_stream_for_lzw_array_filter() {
1590 use crate::parser::objects::{PdfArray, PdfDictionary, PdfName, PdfObject};
1591
1592 let validator = PdfAValidator::new(PdfALevel::A1b);
1593 let mut dict = PdfDictionary::new();
1594 let filters = PdfArray(vec![
1595 PdfObject::Name(PdfName("FlateDecode".to_string())),
1596 PdfObject::Name(PdfName("LZWDecode".to_string())),
1597 ]);
1598 dict.insert("Filter".to_string(), PdfObject::Array(filters));
1599
1600 let mut errors = Vec::new();
1601 validator.check_stream_for_lzw(&dict, 20, 2, &mut errors);
1602
1603 assert_eq!(errors.len(), 1);
1604 assert!(matches!(
1605 &errors[0],
1606 ValidationError::LzwCompressionForbidden { object_id } if object_id.contains("page 3") && object_id.contains("object 20 0")
1607 ));
1608 }
1609
1610 #[test]
1611 fn test_check_stream_for_lzw_no_lzw() {
1612 use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
1613
1614 let validator = PdfAValidator::new(PdfALevel::A1b);
1615 let mut dict = PdfDictionary::new();
1616 dict.insert(
1617 "Filter".to_string(),
1618 PdfObject::Name(PdfName("FlateDecode".to_string())),
1619 );
1620
1621 let mut errors = Vec::new();
1622 validator.check_stream_for_lzw(&dict, 30, 0, &mut errors);
1623
1624 assert_eq!(errors.len(), 0);
1625 }
1626
1627 #[test]
1628 fn test_check_stream_for_lzw_no_filter() {
1629 use crate::parser::objects::PdfDictionary;
1630
1631 let validator = PdfAValidator::new(PdfALevel::A1b);
1632 let dict = PdfDictionary::new();
1633
1634 let mut errors = Vec::new();
1635 validator.check_stream_for_lzw(&dict, 40, 0, &mut errors);
1636
1637 assert_eq!(errors.len(), 0);
1638 }
1639
1640 #[test]
1641 fn test_pdfa_level_allows_lzw() {
1642 assert!(!PdfALevel::A1b.allows_lzw());
1644 assert!(!PdfALevel::A1a.allows_lzw());
1645
1646 assert!(PdfALevel::A2b.allows_lzw());
1648 assert!(PdfALevel::A3b.allows_lzw());
1649 }
1650
1651 #[test]
1652 fn test_pdfa_level_allows_embedded_files() {
1653 assert!(!PdfALevel::A1b.allows_embedded_files());
1655 assert!(!PdfALevel::A2b.allows_embedded_files());
1656
1657 assert!(PdfALevel::A3b.allows_embedded_files());
1659 }
1660
1661 #[test]
1662 fn test_is_device_dependent_colorspace() {
1663 let validator = PdfAValidator::new(PdfALevel::A1b);
1664
1665 assert!(validator.is_device_dependent_colorspace("DeviceRGB"));
1667 assert!(validator.is_device_dependent_colorspace("DeviceCMYK"));
1668 assert!(validator.is_device_dependent_colorspace("DeviceGray"));
1669
1670 assert!(!validator.is_device_dependent_colorspace("CalRGB"));
1672 assert!(!validator.is_device_dependent_colorspace("CalGray"));
1673 assert!(!validator.is_device_dependent_colorspace("Lab"));
1674 assert!(!validator.is_device_dependent_colorspace("ICCBased"));
1675 assert!(!validator.is_device_dependent_colorspace("Indexed"));
1676 assert!(!validator.is_device_dependent_colorspace("Pattern"));
1677 }
1678
1679 #[test]
1680 fn test_pdfa_conformance_level_a() {
1681 assert_eq!(PdfALevel::A1a.conformance(), PdfAConformance::A);
1683 assert_eq!(PdfALevel::A2a.conformance(), PdfAConformance::A);
1684 assert_eq!(PdfALevel::A3a.conformance(), PdfAConformance::A);
1685
1686 assert_eq!(PdfALevel::A1b.conformance(), PdfAConformance::B);
1688 assert_eq!(PdfALevel::A2b.conformance(), PdfAConformance::B);
1689 assert_eq!(PdfALevel::A3b.conformance(), PdfAConformance::B);
1690
1691 assert_eq!(PdfALevel::A2u.conformance(), PdfAConformance::U);
1693 assert_eq!(PdfALevel::A3u.conformance(), PdfAConformance::U);
1694 }
1695}