Skip to main content

oxidize_pdf/verification/
comparators.rs

1//! PDF Comparators for Verification
2//!
3//! This module provides functions to compare generated PDFs with reference PDFs
4//! to verify structural and content equivalence for ISO compliance testing.
5
6use crate::error::Result;
7use crate::verification::parser::{parse_pdf, ParsedPdf};
8use std::collections::HashMap;
9
10/// Difference between two PDFs
11#[derive(Debug, Clone)]
12pub struct PdfDifference {
13    pub location: String,
14    pub expected: String,
15    pub actual: String,
16    pub severity: DifferenceSeverity,
17}
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum DifferenceSeverity {
21    /// Critical differences that break ISO compliance
22    Critical,
23    /// Important differences that may affect functionality
24    Important,
25    /// Minor differences that don't affect compliance
26    Minor,
27    /// Cosmetic differences (timestamps, IDs, etc.)
28    Cosmetic,
29}
30
31/// Result of PDF comparison
32#[derive(Debug, Clone)]
33pub struct ComparisonResult {
34    pub structurally_equivalent: bool,
35    pub content_equivalent: bool,
36    pub differences: Vec<PdfDifference>,
37    pub similarity_score: f64, // 0.0 to 1.0
38}
39
40/// Compare two PDFs for structural equivalence
41pub fn compare_pdfs(generated: &[u8], reference: &[u8]) -> Result<ComparisonResult> {
42    let parsed_generated = parse_pdf(generated)?;
43    let parsed_reference = parse_pdf(reference)?;
44
45    let differences = find_differences(&parsed_generated, &parsed_reference);
46    let similarity_score = calculate_similarity_score(&differences);
47
48    let structurally_equivalent = differences.iter().all(|diff| {
49        diff.severity == DifferenceSeverity::Cosmetic || diff.severity == DifferenceSeverity::Minor
50    });
51
52    let content_equivalent = differences
53        .iter()
54        .all(|diff| diff.severity == DifferenceSeverity::Cosmetic);
55
56    Ok(ComparisonResult {
57        structurally_equivalent,
58        content_equivalent,
59        differences,
60        similarity_score,
61    })
62}
63
64/// Find differences between two parsed PDFs
65fn find_differences(generated: &ParsedPdf, reference: &ParsedPdf) -> Vec<PdfDifference> {
66    let mut differences = Vec::new();
67
68    // Compare versions (minor difference unless major version change)
69    if generated.version != reference.version {
70        let severity = if generated.version.chars().next() != reference.version.chars().next() {
71            DifferenceSeverity::Important
72        } else {
73            DifferenceSeverity::Minor
74        };
75
76        differences.push(PdfDifference {
77            location: "PDF Version".to_string(),
78            expected: reference.version.clone(),
79            actual: generated.version.clone(),
80            severity,
81        });
82    }
83
84    // Compare catalogs
85    differences.extend(compare_catalogs(&generated.catalog, &reference.catalog));
86
87    // Compare page trees
88    differences.extend(compare_page_trees(
89        &generated.page_tree,
90        &reference.page_tree,
91    ));
92
93    // Compare fonts
94    differences.extend(compare_fonts(&generated.fonts, &reference.fonts));
95
96    // Compare color spaces
97    differences.extend(compare_color_spaces(generated, reference));
98
99    // Compare graphics states
100    differences.extend(compare_graphics_states(
101        &generated.graphics_states,
102        &reference.graphics_states,
103    ));
104
105    // Compare text objects
106    differences.extend(compare_text_objects(
107        &generated.text_objects,
108        &reference.text_objects,
109    ));
110
111    // Compare annotations
112    differences.extend(compare_annotations(
113        &generated.annotations,
114        &reference.annotations,
115    ));
116
117    // Compare cross-reference validity
118    if generated.xref_valid != reference.xref_valid {
119        differences.push(PdfDifference {
120            location: "Cross-reference table".to_string(),
121            expected: reference.xref_valid.to_string(),
122            actual: generated.xref_valid.to_string(),
123            severity: DifferenceSeverity::Critical,
124        });
125    }
126
127    differences
128}
129
130/// Compare document catalogs
131fn compare_catalogs(
132    generated: &Option<HashMap<String, String>>,
133    reference: &Option<HashMap<String, String>>,
134) -> Vec<PdfDifference> {
135    let mut differences = Vec::new();
136
137    match (generated, reference) {
138        (Some(gen_catalog), Some(ref_catalog)) => {
139            // Check required entries
140            for key in ["Type", "Pages"] {
141                match (gen_catalog.get(key), ref_catalog.get(key)) {
142                    (Some(gen_val), Some(ref_val)) => {
143                        if gen_val != ref_val {
144                            differences.push(PdfDifference {
145                                location: format!("Catalog/{}", key),
146                                expected: ref_val.clone(),
147                                actual: gen_val.clone(),
148                                severity: DifferenceSeverity::Critical,
149                            });
150                        }
151                    }
152                    (None, Some(ref_val)) => {
153                        differences.push(PdfDifference {
154                            location: format!("Catalog/{}", key),
155                            expected: ref_val.clone(),
156                            actual: "missing".to_string(),
157                            severity: DifferenceSeverity::Critical,
158                        });
159                    }
160                    (Some(gen_val), None) => {
161                        differences.push(PdfDifference {
162                            location: format!("Catalog/{}", key),
163                            expected: "missing".to_string(),
164                            actual: gen_val.clone(),
165                            severity: DifferenceSeverity::Minor,
166                        });
167                    }
168                    (None, None) => {} // Both missing - check if required
169                }
170            }
171        }
172        (None, Some(_)) => {
173            differences.push(PdfDifference {
174                location: "Document Catalog".to_string(),
175                expected: "present".to_string(),
176                actual: "missing".to_string(),
177                severity: DifferenceSeverity::Critical,
178            });
179        }
180        (Some(_), None) => {
181            differences.push(PdfDifference {
182                location: "Document Catalog".to_string(),
183                expected: "missing".to_string(),
184                actual: "present".to_string(),
185                severity: DifferenceSeverity::Minor,
186            });
187        }
188        (None, None) => {
189            differences.push(PdfDifference {
190                location: "Document Catalog".to_string(),
191                expected: "present".to_string(),
192                actual: "missing".to_string(),
193                severity: DifferenceSeverity::Critical,
194            });
195        }
196    }
197
198    differences
199}
200
201/// Compare page trees
202fn compare_page_trees(
203    generated: &Option<crate::verification::parser::PageTree>,
204    reference: &Option<crate::verification::parser::PageTree>,
205) -> Vec<PdfDifference> {
206    let mut differences = Vec::new();
207
208    match (generated, reference) {
209        (Some(gen_tree), Some(ref_tree)) => {
210            if gen_tree.page_count != ref_tree.page_count {
211                differences.push(PdfDifference {
212                    location: "Page Tree/Count".to_string(),
213                    expected: ref_tree.page_count.to_string(),
214                    actual: gen_tree.page_count.to_string(),
215                    severity: DifferenceSeverity::Critical,
216                });
217            }
218
219            if gen_tree.root_type != ref_tree.root_type {
220                differences.push(PdfDifference {
221                    location: "Page Tree/Type".to_string(),
222                    expected: ref_tree.root_type.clone(),
223                    actual: gen_tree.root_type.clone(),
224                    severity: DifferenceSeverity::Critical,
225                });
226            }
227        }
228        (None, Some(_)) => {
229            differences.push(PdfDifference {
230                location: "Page Tree".to_string(),
231                expected: "present".to_string(),
232                actual: "missing".to_string(),
233                severity: DifferenceSeverity::Critical,
234            });
235        }
236        (Some(_), None) => {
237            differences.push(PdfDifference {
238                location: "Page Tree".to_string(),
239                expected: "missing".to_string(),
240                actual: "present".to_string(),
241                severity: DifferenceSeverity::Minor,
242            });
243        }
244        (None, None) => {} // Both missing - may be ok for minimal PDFs
245    }
246
247    differences
248}
249
250/// Compare font lists
251fn compare_fonts(generated: &[String], reference: &[String]) -> Vec<PdfDifference> {
252    let mut differences = Vec::new();
253
254    // Check for missing fonts
255    for ref_font in reference {
256        if !generated.contains(ref_font) {
257            differences.push(PdfDifference {
258                location: format!("Fonts/{}", ref_font),
259                expected: "present".to_string(),
260                actual: "missing".to_string(),
261                severity: DifferenceSeverity::Important,
262            });
263        }
264    }
265
266    // Check for extra fonts (usually not a problem)
267    for gen_font in generated {
268        if !reference.contains(gen_font) {
269            differences.push(PdfDifference {
270                location: format!("Fonts/{}", gen_font),
271                expected: "missing".to_string(),
272                actual: "present".to_string(),
273                severity: DifferenceSeverity::Minor,
274            });
275        }
276    }
277
278    differences
279}
280
281/// Compare color space usage
282fn compare_color_spaces(generated: &ParsedPdf, reference: &ParsedPdf) -> Vec<PdfDifference> {
283    let mut differences = Vec::new();
284
285    if generated.uses_device_rgb != reference.uses_device_rgb {
286        differences.push(PdfDifference {
287            location: "Color Spaces/DeviceRGB".to_string(),
288            expected: reference.uses_device_rgb.to_string(),
289            actual: generated.uses_device_rgb.to_string(),
290            severity: DifferenceSeverity::Important,
291        });
292    }
293
294    if generated.uses_device_cmyk != reference.uses_device_cmyk {
295        differences.push(PdfDifference {
296            location: "Color Spaces/DeviceCMYK".to_string(),
297            expected: reference.uses_device_cmyk.to_string(),
298            actual: generated.uses_device_cmyk.to_string(),
299            severity: DifferenceSeverity::Important,
300        });
301    }
302
303    if generated.uses_device_gray != reference.uses_device_gray {
304        differences.push(PdfDifference {
305            location: "Color Spaces/DeviceGray".to_string(),
306            expected: reference.uses_device_gray.to_string(),
307            actual: generated.uses_device_gray.to_string(),
308            severity: DifferenceSeverity::Important,
309        });
310    }
311
312    differences
313}
314
315/// Compare graphics states
316fn compare_graphics_states(
317    generated: &[crate::verification::parser::GraphicsState],
318    reference: &[crate::verification::parser::GraphicsState],
319) -> Vec<PdfDifference> {
320    let mut differences = Vec::new();
321
322    if generated.len() != reference.len() {
323        differences.push(PdfDifference {
324            location: "Graphics States/Count".to_string(),
325            expected: reference.len().to_string(),
326            actual: generated.len().to_string(),
327            severity: DifferenceSeverity::Important,
328        });
329    }
330
331    // Compare first few graphics states (detailed comparison would be complex)
332    let min_len = generated.len().min(reference.len());
333    for i in 0..min_len.min(3) {
334        // Only compare first 3 for performance
335        let gen_state = &generated[i];
336        let ref_state = &reference[i];
337
338        if gen_state.line_width != ref_state.line_width {
339            differences.push(PdfDifference {
340                location: format!("Graphics State {}/LineWidth", i),
341                expected: format!("{:?}", ref_state.line_width),
342                actual: format!("{:?}", gen_state.line_width),
343                severity: DifferenceSeverity::Minor,
344            });
345        }
346    }
347
348    differences
349}
350
351/// Compare text objects
352fn compare_text_objects(
353    generated: &[crate::verification::parser::TextObject],
354    reference: &[crate::verification::parser::TextObject],
355) -> Vec<PdfDifference> {
356    let mut differences = Vec::new();
357
358    if generated.len() != reference.len() {
359        differences.push(PdfDifference {
360            location: "Text Objects/Count".to_string(),
361            expected: reference.len().to_string(),
362            actual: generated.len().to_string(),
363            severity: DifferenceSeverity::Important,
364        });
365    }
366
367    // Compare text content (simplified)
368    let min_len = generated.len().min(reference.len());
369    for i in 0..min_len {
370        let gen_text = &generated[i];
371        let ref_text = &reference[i];
372
373        if gen_text.text_content != ref_text.text_content {
374            differences.push(PdfDifference {
375                location: format!("Text Object {}/Content", i),
376                expected: ref_text.text_content.clone(),
377                actual: gen_text.text_content.clone(),
378                severity: DifferenceSeverity::Important,
379            });
380        }
381    }
382
383    differences
384}
385
386/// Compare annotations
387fn compare_annotations(
388    generated: &[crate::verification::parser::Annotation],
389    reference: &[crate::verification::parser::Annotation],
390) -> Vec<PdfDifference> {
391    let mut differences = Vec::new();
392
393    if generated.len() != reference.len() {
394        differences.push(PdfDifference {
395            location: "Annotations/Count".to_string(),
396            expected: reference.len().to_string(),
397            actual: generated.len().to_string(),
398            severity: DifferenceSeverity::Important,
399        });
400    }
401
402    differences
403}
404
405/// Calculate similarity score based on differences
406fn calculate_similarity_score(differences: &[PdfDifference]) -> f64 {
407    if differences.is_empty() {
408        return 1.0;
409    }
410
411    let mut penalty = 0.0;
412    for diff in differences {
413        penalty += match diff.severity {
414            DifferenceSeverity::Critical => 0.3,
415            DifferenceSeverity::Important => 0.1,
416            DifferenceSeverity::Minor => 0.05,
417            DifferenceSeverity::Cosmetic => 0.01,
418        };
419    }
420
421    (1.0f64 - penalty).max(0.0)
422}
423
424/// Check if two PDFs are structurally equivalent for ISO compliance
425pub fn pdfs_structurally_equivalent(generated: &[u8], reference: &[u8]) -> bool {
426    match compare_pdfs(generated, reference) {
427        Ok(result) => result.structurally_equivalent,
428        Err(_) => false,
429    }
430}
431
432/// Extract structural differences between PDFs
433pub fn extract_pdf_differences(generated: &[u8], reference: &[u8]) -> Result<Vec<PdfDifference>> {
434    let result = compare_pdfs(generated, reference)?;
435    Ok(result.differences)
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::verification::parser::{Annotation, GraphicsState, PageTree, TextObject};
442
443    fn create_test_pdf(version: &str, catalog_type: &str) -> Vec<u8> {
444        format!(
445            "%PDF-{}\n1 0 obj\n<<\n/Type /{}\n>>\nendobj\n%%EOF",
446            version, catalog_type
447        )
448        .into_bytes()
449    }
450
451    #[test]
452    fn test_identical_pdfs() {
453        let pdf1 = create_test_pdf("1.4", "Catalog");
454        let pdf2 = create_test_pdf("1.4", "Catalog");
455
456        let result = compare_pdfs(&pdf1, &pdf2).unwrap();
457        assert!(result.content_equivalent);
458        assert_eq!(result.similarity_score, 1.0);
459    }
460
461    #[test]
462    fn test_version_difference() {
463        let pdf1 = create_test_pdf("1.4", "Catalog");
464        let pdf2 = create_test_pdf("1.7", "Catalog");
465
466        let result = compare_pdfs(&pdf1, &pdf2).unwrap();
467        assert!(!result.content_equivalent);
468        assert!(result.similarity_score < 1.0);
469        assert!(result
470            .differences
471            .iter()
472            .any(|d| d.location == "PDF Version"));
473    }
474
475    #[test]
476    fn test_structural_difference() {
477        let pdf1 = create_test_pdf("1.4", "Catalog");
478        let pdf2 = create_test_pdf("1.7", "Catalog"); // Different version should be minor difference
479
480        let result = compare_pdfs(&pdf1, &pdf2).unwrap();
481
482        // Version differences are minor, so should still be structurally equivalent
483        assert!(result.structurally_equivalent);
484        assert!(!result.differences.is_empty()); // But should have differences
485
486        // Check that version difference was detected
487        assert!(result
488            .differences
489            .iter()
490            .any(|d| d.location == "PDF Version"));
491    }
492
493    #[test]
494    fn test_calculate_similarity_score() {
495        let differences = vec![PdfDifference {
496            location: "test".to_string(),
497            expected: "a".to_string(),
498            actual: "b".to_string(),
499            severity: DifferenceSeverity::Critical,
500        }];
501
502        let score = calculate_similarity_score(&differences);
503        assert_eq!(score, 0.7); // 1.0 - 0.3 (critical penalty)
504    }
505
506    // New tests for complete coverage
507
508    #[test]
509    fn test_calculate_similarity_score_empty() {
510        let differences: Vec<PdfDifference> = vec![];
511        let score = calculate_similarity_score(&differences);
512        assert_eq!(score, 1.0);
513    }
514
515    #[test]
516    fn test_calculate_similarity_score_important() {
517        let differences = vec![PdfDifference {
518            location: "test".to_string(),
519            expected: "a".to_string(),
520            actual: "b".to_string(),
521            severity: DifferenceSeverity::Important,
522        }];
523
524        let score = calculate_similarity_score(&differences);
525        assert!((score - 0.9).abs() < 0.001); // 1.0 - 0.1
526    }
527
528    #[test]
529    fn test_calculate_similarity_score_minor() {
530        let differences = vec![PdfDifference {
531            location: "test".to_string(),
532            expected: "a".to_string(),
533            actual: "b".to_string(),
534            severity: DifferenceSeverity::Minor,
535        }];
536
537        let score = calculate_similarity_score(&differences);
538        assert!((score - 0.95).abs() < 0.001); // 1.0 - 0.05
539    }
540
541    #[test]
542    fn test_calculate_similarity_score_cosmetic() {
543        let differences = vec![PdfDifference {
544            location: "test".to_string(),
545            expected: "a".to_string(),
546            actual: "b".to_string(),
547            severity: DifferenceSeverity::Cosmetic,
548        }];
549
550        let score = calculate_similarity_score(&differences);
551        assert!((score - 0.99).abs() < 0.001); // 1.0 - 0.01
552    }
553
554    #[test]
555    fn test_calculate_similarity_score_multiple() {
556        let differences = vec![
557            PdfDifference {
558                location: "test1".to_string(),
559                expected: "a".to_string(),
560                actual: "b".to_string(),
561                severity: DifferenceSeverity::Critical, // -0.3
562            },
563            PdfDifference {
564                location: "test2".to_string(),
565                expected: "c".to_string(),
566                actual: "d".to_string(),
567                severity: DifferenceSeverity::Important, // -0.1
568            },
569        ];
570
571        let score = calculate_similarity_score(&differences);
572        assert!((score - 0.6).abs() < 0.001); // 1.0 - 0.4
573    }
574
575    #[test]
576    fn test_calculate_similarity_score_max_penalty() {
577        // Multiple critical differences that exceed 1.0 penalty
578        let differences = vec![
579            PdfDifference {
580                location: "test1".to_string(),
581                expected: "a".to_string(),
582                actual: "b".to_string(),
583                severity: DifferenceSeverity::Critical,
584            },
585            PdfDifference {
586                location: "test2".to_string(),
587                expected: "a".to_string(),
588                actual: "b".to_string(),
589                severity: DifferenceSeverity::Critical,
590            },
591            PdfDifference {
592                location: "test3".to_string(),
593                expected: "a".to_string(),
594                actual: "b".to_string(),
595                severity: DifferenceSeverity::Critical,
596            },
597            PdfDifference {
598                location: "test4".to_string(),
599                expected: "a".to_string(),
600                actual: "b".to_string(),
601                severity: DifferenceSeverity::Critical,
602            },
603        ];
604
605        let score = calculate_similarity_score(&differences);
606        assert_eq!(score, 0.0); // Should clamp to 0.0
607    }
608
609    #[test]
610    fn test_difference_severity_equality() {
611        assert_eq!(DifferenceSeverity::Critical, DifferenceSeverity::Critical);
612        assert_eq!(DifferenceSeverity::Important, DifferenceSeverity::Important);
613        assert_eq!(DifferenceSeverity::Minor, DifferenceSeverity::Minor);
614        assert_eq!(DifferenceSeverity::Cosmetic, DifferenceSeverity::Cosmetic);
615        assert_ne!(DifferenceSeverity::Critical, DifferenceSeverity::Minor);
616    }
617
618    #[test]
619    fn test_pdf_difference_clone() {
620        let diff = PdfDifference {
621            location: "test".to_string(),
622            expected: "a".to_string(),
623            actual: "b".to_string(),
624            severity: DifferenceSeverity::Critical,
625        };
626        let cloned = diff.clone();
627        assert_eq!(diff.location, cloned.location);
628        assert_eq!(diff.expected, cloned.expected);
629        assert_eq!(diff.actual, cloned.actual);
630    }
631
632    #[test]
633    fn test_comparison_result_clone() {
634        let result = ComparisonResult {
635            structurally_equivalent: true,
636            content_equivalent: false,
637            differences: vec![],
638            similarity_score: 0.95,
639        };
640        let cloned = result.clone();
641        assert_eq!(
642            result.structurally_equivalent,
643            cloned.structurally_equivalent
644        );
645        assert_eq!(result.content_equivalent, cloned.content_equivalent);
646        assert_eq!(result.similarity_score, cloned.similarity_score);
647    }
648
649    #[test]
650    fn test_compare_fonts_missing_reference() {
651        let generated = vec!["Font1".to_string(), "Font2".to_string()];
652        let reference = vec!["Font1".to_string(), "Font3".to_string()];
653
654        let differences = compare_fonts(&generated, &reference);
655
656        // Should have Font3 missing from generated
657        assert!(differences
658            .iter()
659            .any(|d| { d.location.contains("Font3") && d.actual == "missing" }));
660
661        // Should have Font2 extra in generated
662        assert!(differences
663            .iter()
664            .any(|d| { d.location.contains("Font2") && d.expected == "missing" }));
665    }
666
667    #[test]
668    fn test_compare_fonts_empty() {
669        let generated: Vec<String> = vec![];
670        let reference: Vec<String> = vec![];
671
672        let differences = compare_fonts(&generated, &reference);
673        assert!(differences.is_empty());
674    }
675
676    #[test]
677    fn test_compare_fonts_identical() {
678        let generated = vec!["Font1".to_string(), "Font2".to_string()];
679        let reference = vec!["Font1".to_string(), "Font2".to_string()];
680
681        let differences = compare_fonts(&generated, &reference);
682        assert!(differences.is_empty());
683    }
684
685    #[test]
686    fn test_compare_annotations_different_count() {
687        let generated: Vec<Annotation> = vec![];
688        let reference = vec![Annotation {
689            subtype: "Link".to_string(),
690            rect: None,
691            contents: None,
692        }];
693
694        let differences = compare_annotations(&generated, &reference);
695
696        assert!(differences
697            .iter()
698            .any(|d| { d.location.contains("Annotations/Count") }));
699    }
700
701    #[test]
702    fn test_compare_annotations_same_count() {
703        let generated = vec![Annotation {
704            subtype: "Link".to_string(),
705            rect: None,
706            contents: None,
707        }];
708        let reference = vec![Annotation {
709            subtype: "Text".to_string(),
710            rect: None,
711            contents: None,
712        }];
713
714        let differences = compare_annotations(&generated, &reference);
715        assert!(differences.is_empty()); // Only counts are compared
716    }
717
718    #[test]
719    fn test_compare_text_objects_different_content() {
720        let generated = vec![TextObject {
721            text_content: "Hello".to_string(),
722            font: Some("Helvetica".to_string()),
723            font_size: Some(12.0),
724        }];
725        let reference = vec![TextObject {
726            text_content: "World".to_string(),
727            font: Some("Helvetica".to_string()),
728            font_size: Some(12.0),
729        }];
730
731        let differences = compare_text_objects(&generated, &reference);
732
733        assert!(differences
734            .iter()
735            .any(|d| { d.location.contains("Text Object") && d.location.contains("Content") }));
736    }
737
738    #[test]
739    fn test_compare_text_objects_different_count() {
740        let generated: Vec<TextObject> = vec![];
741        let reference = vec![TextObject {
742            text_content: "Test".to_string(),
743            font: Some("Helvetica".to_string()),
744            font_size: Some(12.0),
745        }];
746
747        let differences = compare_text_objects(&generated, &reference);
748
749        assert!(differences
750            .iter()
751            .any(|d| { d.location.contains("Text Objects/Count") }));
752    }
753
754    #[test]
755    fn test_compare_graphics_states_different_count() {
756        let generated: Vec<GraphicsState> = vec![];
757        let reference = vec![GraphicsState {
758            line_width: Some(1.0),
759            line_cap: None,
760            line_join: None,
761            fill_color: None,
762            stroke_color: None,
763        }];
764
765        let differences = compare_graphics_states(&generated, &reference);
766
767        assert!(differences
768            .iter()
769            .any(|d| { d.location.contains("Graphics States/Count") }));
770    }
771
772    #[test]
773    fn test_compare_graphics_states_different_line_width() {
774        let generated = vec![GraphicsState {
775            line_width: Some(2.0),
776            line_cap: None,
777            line_join: None,
778            fill_color: None,
779            stroke_color: None,
780        }];
781        let reference = vec![GraphicsState {
782            line_width: Some(1.0),
783            line_cap: None,
784            line_join: None,
785            fill_color: None,
786            stroke_color: None,
787        }];
788
789        let differences = compare_graphics_states(&generated, &reference);
790
791        assert!(differences
792            .iter()
793            .any(|d| { d.location.contains("LineWidth") }));
794    }
795
796    #[test]
797    fn test_compare_catalogs_both_present_with_diff() {
798        let mut gen_catalog = HashMap::new();
799        gen_catalog.insert("Type".to_string(), "Catalog".to_string());
800        gen_catalog.insert("Pages".to_string(), "1 0 R".to_string());
801
802        let mut ref_catalog = HashMap::new();
803        ref_catalog.insert("Type".to_string(), "Catalog".to_string());
804        ref_catalog.insert("Pages".to_string(), "2 0 R".to_string()); // Different
805
806        let differences = compare_catalogs(&Some(gen_catalog), &Some(ref_catalog));
807
808        assert!(differences
809            .iter()
810            .any(|d| { d.location.contains("Catalog/Pages") }));
811    }
812
813    #[test]
814    fn test_compare_catalogs_generated_missing_key() {
815        let mut gen_catalog = HashMap::new();
816        gen_catalog.insert("Type".to_string(), "Catalog".to_string());
817        // Missing "Pages" key
818
819        let mut ref_catalog = HashMap::new();
820        ref_catalog.insert("Type".to_string(), "Catalog".to_string());
821        ref_catalog.insert("Pages".to_string(), "1 0 R".to_string());
822
823        let differences = compare_catalogs(&Some(gen_catalog), &Some(ref_catalog));
824
825        assert!(differences
826            .iter()
827            .any(|d| { d.location.contains("Catalog/Pages") && d.actual == "missing" }));
828    }
829
830    #[test]
831    fn test_compare_catalogs_reference_missing_key() {
832        let mut gen_catalog = HashMap::new();
833        gen_catalog.insert("Type".to_string(), "Catalog".to_string());
834        gen_catalog.insert("Pages".to_string(), "1 0 R".to_string());
835
836        let mut ref_catalog = HashMap::new();
837        ref_catalog.insert("Type".to_string(), "Catalog".to_string());
838        // Missing "Pages" key
839
840        let differences = compare_catalogs(&Some(gen_catalog), &Some(ref_catalog));
841
842        assert!(differences
843            .iter()
844            .any(|d| { d.location.contains("Catalog/Pages") && d.expected == "missing" }));
845    }
846
847    #[test]
848    fn test_compare_catalogs_generated_none() {
849        let ref_catalog = HashMap::new();
850        let differences = compare_catalogs(&None, &Some(ref_catalog));
851
852        assert!(differences
853            .iter()
854            .any(|d| { d.location.contains("Document Catalog") && d.actual == "missing" }));
855    }
856
857    #[test]
858    fn test_compare_catalogs_reference_none() {
859        let gen_catalog = HashMap::new();
860        let differences = compare_catalogs(&Some(gen_catalog), &None);
861
862        assert!(differences
863            .iter()
864            .any(|d| { d.location.contains("Document Catalog") && d.expected == "missing" }));
865    }
866
867    #[test]
868    fn test_compare_catalogs_both_none() {
869        let differences = compare_catalogs(&None, &None);
870
871        assert!(differences.iter().any(|d| {
872            d.location.contains("Document Catalog") && d.severity == DifferenceSeverity::Critical
873        }));
874    }
875
876    #[test]
877    fn test_compare_page_trees_different_count() {
878        let gen_tree = PageTree {
879            page_count: 5,
880            root_type: "Pages".to_string(),
881            kids_arrays: vec![],
882        };
883        let ref_tree = PageTree {
884            page_count: 3,
885            root_type: "Pages".to_string(),
886            kids_arrays: vec![],
887        };
888
889        let differences = compare_page_trees(&Some(gen_tree), &Some(ref_tree));
890
891        assert!(differences
892            .iter()
893            .any(|d| { d.location.contains("Page Tree/Count") }));
894    }
895
896    #[test]
897    fn test_compare_page_trees_different_type() {
898        let gen_tree = PageTree {
899            page_count: 1,
900            root_type: "Page".to_string(),
901            kids_arrays: vec![],
902        };
903        let ref_tree = PageTree {
904            page_count: 1,
905            root_type: "Pages".to_string(),
906            kids_arrays: vec![],
907        };
908
909        let differences = compare_page_trees(&Some(gen_tree), &Some(ref_tree));
910
911        assert!(differences
912            .iter()
913            .any(|d| { d.location.contains("Page Tree/Type") }));
914    }
915
916    #[test]
917    fn test_compare_page_trees_generated_none() {
918        let ref_tree = PageTree {
919            page_count: 1,
920            root_type: "Pages".to_string(),
921            kids_arrays: vec![],
922        };
923
924        let differences = compare_page_trees(&None, &Some(ref_tree));
925
926        assert!(differences
927            .iter()
928            .any(|d| { d.location.contains("Page Tree") && d.actual == "missing" }));
929    }
930
931    #[test]
932    fn test_compare_page_trees_reference_none() {
933        let gen_tree = PageTree {
934            page_count: 1,
935            root_type: "Pages".to_string(),
936            kids_arrays: vec![],
937        };
938
939        let differences = compare_page_trees(&Some(gen_tree), &None);
940
941        assert!(differences
942            .iter()
943            .any(|d| { d.location.contains("Page Tree") && d.expected == "missing" }));
944    }
945
946    #[test]
947    fn test_compare_page_trees_both_none() {
948        let differences = compare_page_trees(&None, &None);
949        assert!(differences.is_empty()); // Both missing is ok for minimal PDFs
950    }
951
952    #[test]
953    fn test_pdfs_structurally_equivalent_true() {
954        let pdf1 = create_test_pdf("1.4", "Catalog");
955        let pdf2 = create_test_pdf("1.4", "Catalog");
956
957        assert!(pdfs_structurally_equivalent(&pdf1, &pdf2));
958    }
959
960    #[test]
961    fn test_pdfs_structurally_equivalent_invalid_pdf() {
962        let pdf1 = b"not a pdf".to_vec();
963        let pdf2 = b"also not a pdf".to_vec();
964
965        // Should return false on parse error
966        assert!(!pdfs_structurally_equivalent(&pdf1, &pdf2));
967    }
968
969    #[test]
970    fn test_extract_pdf_differences() {
971        let pdf1 = create_test_pdf("1.4", "Catalog");
972        let pdf2 = create_test_pdf("1.7", "Catalog");
973
974        let differences = extract_pdf_differences(&pdf1, &pdf2).unwrap();
975        assert!(!differences.is_empty());
976    }
977
978    #[test]
979    fn test_extract_pdf_differences_identical() {
980        let pdf1 = create_test_pdf("1.4", "Catalog");
981        let pdf2 = create_test_pdf("1.4", "Catalog");
982
983        let differences = extract_pdf_differences(&pdf1, &pdf2).unwrap();
984        assert!(differences.is_empty());
985    }
986
987    #[test]
988    fn test_major_version_difference() {
989        let pdf1 = create_test_pdf("1.4", "Catalog");
990        let pdf2 = create_test_pdf("2.0", "Catalog"); // Major version change
991
992        let result = compare_pdfs(&pdf1, &pdf2).unwrap();
993
994        // Should have Important severity for major version change
995        assert!(result.differences.iter().any(|d| {
996            d.location == "PDF Version" && d.severity == DifferenceSeverity::Important
997        }));
998    }
999}