Skip to main content

katana_document_viewer/export_postprocess/
service.rs

1use super::contract::PdfPostprocessContract;
2use super::{
3    ExportPostprocessDiagnostic, ExportPostprocessEvaluationReport,
4    ExportPostprocessEvaluationRequest, ExportPostprocessMetrics, ExportPostprocessMode,
5    ExportPostprocessPolicy, ExportPostprocessStatus, PdfPostprocessAdapter, PdfPostprocessError,
6    PdfPostprocessInput, PdfPostprocessOutput,
7};
8use crate::{ExportQualityArtifacts, ExportQualityGate, ExportQualityReport};
9
10pub struct ExportPostprocessEvaluationService<A> {
11    adapter: A,
12    mode: ExportPostprocessMode,
13    policy: ExportPostprocessPolicy,
14}
15
16impl<A: PdfPostprocessAdapter> ExportPostprocessEvaluationService<A> {
17    pub fn new(adapter: A, mode: ExportPostprocessMode, policy: ExportPostprocessPolicy) -> Self {
18        Self {
19            adapter,
20            mode,
21            policy,
22        }
23    }
24
25    pub fn evaluate(
26        &self,
27        request: &ExportPostprocessEvaluationRequest<'_>,
28    ) -> ExportPostprocessEvaluationReport {
29        let baseline_quality = ExportQualityGate::evaluate(&request.artifacts);
30        if self.mode == ExportPostprocessMode::Disabled {
31            return self.skipped_report(request, baseline_quality);
32        }
33        let input = PdfPostprocessInput {
34            pdf: request.artifacts.pdf,
35        };
36        match self.adapter.postprocess_pdf(&input) {
37            Ok(output) => self.output_report(request, baseline_quality, output),
38            Err(error) => self.failure_report(request, baseline_quality, error),
39        }
40    }
41
42    fn skipped_report(
43        &self,
44        request: &ExportPostprocessEvaluationRequest<'_>,
45        baseline_quality: ExportQualityReport,
46    ) -> ExportPostprocessEvaluationReport {
47        let metrics = self.metrics(request, request.artifacts.pdf, 0);
48        self.report(
49            ExportPostprocessStatus::Skipped,
50            request.artifacts.pdf.to_vec(),
51            baseline_quality.clone(),
52            baseline_quality,
53            metrics,
54            vec![ExportPostprocessDiagnostic::new(
55                "postprocess-disabled",
56                "PDF postprocess is disabled by default",
57            )],
58        )
59    }
60
61    fn failure_report(
62        &self,
63        request: &ExportPostprocessEvaluationRequest<'_>,
64        baseline_quality: ExportQualityReport,
65        error: PdfPostprocessError,
66    ) -> ExportPostprocessEvaluationReport {
67        let metrics = self.metrics(request, request.artifacts.pdf, 0);
68        self.report(
69            ExportPostprocessStatus::Rejected,
70            request.artifacts.pdf.to_vec(),
71            baseline_quality.clone(),
72            baseline_quality,
73            metrics,
74            vec![ExportPostprocessDiagnostic::new(
75                "postprocess-failed",
76                &error.message,
77            )],
78        )
79    }
80
81    fn output_report(
82        &self,
83        request: &ExportPostprocessEvaluationRequest<'_>,
84        baseline_quality: ExportQualityReport,
85        output: PdfPostprocessOutput,
86    ) -> ExportPostprocessEvaluationReport {
87        let optimized_quality = self.optimized_quality(request, &output.pdf);
88        let metrics = self.metrics(request, &output.pdf, output.elapsed_millis);
89        let diagnostics =
90            self.output_diagnostics(request, &output.pdf, &optimized_quality, &metrics);
91        let accepted = diagnostics.is_empty();
92        let selected_pdf = if accepted {
93            output.pdf
94        } else {
95            request.artifacts.pdf.to_vec()
96        };
97        self.report(
98            status_for(accepted),
99            selected_pdf,
100            baseline_quality,
101            optimized_quality,
102            metrics,
103            diagnostics,
104        )
105    }
106
107    fn optimized_quality(
108        &self,
109        request: &ExportPostprocessEvaluationRequest<'_>,
110        optimized_pdf: &[u8],
111    ) -> ExportQualityReport {
112        ExportQualityGate::evaluate(&ExportQualityArtifacts {
113            html: request.artifacts.html,
114            pdf: optimized_pdf,
115            png: request.artifacts.png,
116            jpeg: request.artifacts.jpeg,
117        })
118    }
119
120    fn output_diagnostics(
121        &self,
122        request: &ExportPostprocessEvaluationRequest<'_>,
123        optimized_pdf: &[u8],
124        optimized_quality: &ExportQualityReport,
125        metrics: &ExportPostprocessMetrics,
126    ) -> Vec<ExportPostprocessDiagnostic> {
127        let mut diagnostics = Vec::new();
128        if !optimized_quality.is_pass() {
129            diagnostics.push(ExportPostprocessDiagnostic::new(
130                "postprocess-quality-regressed",
131                "optimized PDF did not pass ExportQualityGate",
132            ));
133        }
134        diagnostics.extend(PdfPostprocessContract::regression_diagnostics(
135            request.artifacts.pdf,
136            optimized_pdf,
137        ));
138        diagnostics.extend(self.policy.diagnostics(metrics));
139        diagnostics
140    }
141
142    fn metrics(
143        &self,
144        request: &ExportPostprocessEvaluationRequest<'_>,
145        optimized_pdf: &[u8],
146        postprocess_millis: u128,
147    ) -> ExportPostprocessMetrics {
148        ExportPostprocessMetrics::new(
149            request.artifacts.pdf.len(),
150            optimized_pdf.len(),
151            request.baseline_pdf_generation_millis,
152            postprocess_millis,
153        )
154    }
155
156    fn report(
157        &self,
158        status: ExportPostprocessStatus,
159        selected_pdf_bytes: Vec<u8>,
160        baseline_quality: ExportQualityReport,
161        optimized_quality: ExportQualityReport,
162        metrics: ExportPostprocessMetrics,
163        diagnostics: Vec<ExportPostprocessDiagnostic>,
164    ) -> ExportPostprocessEvaluationReport {
165        ExportPostprocessEvaluationReport {
166            adapter_name: self.adapter.name().to_string(),
167            status,
168            selected_pdf_bytes,
169            baseline_quality,
170            optimized_quality,
171            metrics,
172            diagnostics,
173        }
174    }
175}
176
177fn status_for(accepted: bool) -> ExportPostprocessStatus {
178    if accepted {
179        ExportPostprocessStatus::Accepted
180    } else {
181        ExportPostprocessStatus::Rejected
182    }
183}