Skip to main content

katana_document_viewer/export_quality/
score.rs

1use crate::export_quality::html_score::HtmlQualityScore;
2use crate::export_quality::image_score::ImageQualityScore;
3use crate::export_quality::types::{
4    ExportFormatQualityScore, ExportQualityArtifacts, ExportQualityReport, TOTAL_SCORE_MAX, check,
5};
6use crate::forge::ExportFormat;
7
8pub struct ExportQualityGate;
9
10impl ExportQualityGate {
11    pub fn evaluate(artifacts: &ExportQualityArtifacts<'_>) -> ExportQualityReport {
12        let format_scores = vec![
13            Self::score_html(artifacts.html),
14            Self::score_pdf(artifacts.pdf),
15            Self::score_png(artifacts.png),
16            Self::score_jpeg(artifacts.jpeg),
17        ];
18        let fatal_failures = format_scores
19            .iter()
20            .flat_map(|score| score.fatal_failures())
21            .collect::<Vec<_>>();
22        let warnings = format_scores
23            .iter()
24            .filter(|score| !score.is_pass())
25            .map(|score| format!("{:?} quality score is {}/100", score.format, score.score))
26            .collect::<Vec<_>>();
27        let total_score = format_scores.iter().map(|score| score.score).sum();
28        ExportQualityReport {
29            total_score,
30            max_score: TOTAL_SCORE_MAX,
31            format_scores,
32            fatal_failures,
33            warnings,
34        }
35    }
36
37    fn score_html(bytes: &[u8]) -> ExportFormatQualityScore {
38        HtmlQualityScore::score(bytes)
39    }
40
41    fn score_pdf(bytes: &[u8]) -> ExportFormatQualityScore {
42        let text = String::from_utf8_lossy(bytes);
43        ExportFormatQualityScore::new(
44            ExportFormat::Pdf,
45            vec![
46                check("pdf is non-empty", !bytes.is_empty(), true, 20),
47                check("pdf has header", bytes.starts_with(b"%PDF-1.4"), true, 20),
48                check(
49                    "pdf embeds page image",
50                    text.contains("/Subtype /Image"),
51                    true,
52                    20,
53                ),
54                check(
55                    "pdf has page tree",
56                    text.contains("/Type /Pages") && text.contains("/Type /Page"),
57                    true,
58                    20,
59                ),
60                check(
61                    "pdf keeps link annotations",
62                    text.contains("/Subtype /Link"),
63                    true,
64                    20,
65                ),
66            ],
67        )
68    }
69
70    fn score_png(bytes: &[u8]) -> ExportFormatQualityScore {
71        let decoded = ImageQualityScore::decode_dimensions(bytes);
72        Self::score_image(ExportFormat::Png, bytes, decoded, b"\x89PNG\r\n\x1a\n")
73    }
74
75    fn score_jpeg(bytes: &[u8]) -> ExportFormatQualityScore {
76        let decoded = ImageQualityScore::decode_dimensions(bytes);
77        Self::score_image(ExportFormat::Jpeg, bytes, decoded, b"\xff\xd8\xff")
78    }
79
80    fn score_image(
81        format: ExportFormat,
82        bytes: &[u8],
83        decoded: Result<(u32, u32), image::ImageError>,
84        signature: &[u8],
85    ) -> ExportFormatQualityScore {
86        ImageQualityScore::score(format, bytes, decoded, signature)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn evaluate_reports_warnings_for_failed_formats() {
96        let artifacts = ExportQualityArtifacts {
97            html: b"",
98            pdf: b"",
99            png: b"",
100            jpeg: b"",
101        };
102
103        let report = ExportQualityGate::evaluate(&artifacts);
104
105        assert!(!report.is_pass());
106        assert!(
107            report
108                .warnings
109                .iter()
110                .any(|warning| warning.contains("Html"))
111        );
112        assert!(!report.fatal_failures.is_empty());
113    }
114}