katana_document_viewer/export_postprocess/
service.rs1use 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}