1use std::fmt;
8
9use lopdf::Document;
10
11use crate::config::{Config, Preset};
12use crate::error::Refusal;
13use crate::report::Report;
14use crate::verify::Verification;
15use crate::verify::render::Comparison;
16use crate::{pipeline, verify};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Verify {
22 Structural,
25 Render { preset: Preset, strict: bool },
29}
30
31#[derive(Debug)]
33#[non_exhaustive]
34pub struct Compressed {
35 pub output: Vec<u8>,
37 pub report: Report,
38 pub verification: Option<Verification>,
41 pub render: Option<Comparison>,
44}
45
46#[derive(Debug)]
48#[non_exhaustive]
49pub struct Rejected {
50 pub report: Report,
51 pub reason: Refusal,
52}
53
54impl fmt::Display for Rejected {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.reason)
57 }
58}
59
60impl std::error::Error for Rejected {}
61
62#[derive(Default)]
65struct Progress {
66 report: Report,
67 verification: Option<Verification>,
68 render: Option<Comparison>,
69}
70
71pub fn compress(
76 input: &[u8],
77 config: &Config,
78 verify: Verify,
79) -> Result<Compressed, Box<Rejected>> {
80 let mut progress = Progress {
81 report: Report::new(input.len()),
82 ..Progress::default()
83 };
84 match run(input, config, verify, &mut progress) {
85 Ok(output) => Ok(Compressed {
86 output,
87 report: progress.report,
88 verification: progress.verification,
89 render: progress.render,
90 }),
91 Err(reason) => Err(Box::new(Rejected {
92 report: progress.report,
93 reason,
94 })),
95 }
96}
97
98fn run(
99 input: &[u8],
100 config: &Config,
101 verify: Verify,
102 progress: &mut Progress,
103) -> Result<Vec<u8>, Refusal> {
104 let mut doc = Document::load_mem(input).map_err(Refusal::Unparseable)?;
105 let pages = doc.get_pages().len();
106 pipeline::run(&mut doc, config, &mut progress.report)?;
107 let output = pipeline::serialize(&mut doc, input, &mut progress.report)?;
108 if output != input {
110 progress.verification = Some(check_structure(
111 input,
112 &output,
113 pages,
114 &mut progress.report,
115 )?);
116 progress.render = check_render(input, &output, verify, &mut progress.report)?;
117 }
118 Ok(output)
119}
120
121fn check_structure(
123 input: &[u8],
124 output: &[u8],
125 pages: usize,
126 report: &mut Report,
127) -> Result<Verification, Refusal> {
128 let verification = verify::verify(output, pages);
129 report.note(verification.to_string());
130 if verification.is_ok() {
131 return Ok(verification);
132 }
133 let baseline = verify::verify(input, pages);
134 let regressions = verification.regressions_from(&baseline);
135 if !regressions.is_empty() {
136 return Err(Refusal::VerificationRegressed(regressions));
137 }
138 report.note("warning: the input already had these problems; output written anyway");
139 Ok(verification)
140}
141
142fn check_render(
143 input: &[u8],
144 output: &[u8],
145 verify: Verify,
146 report: &mut Report,
147) -> Result<Option<Comparison>, Refusal> {
148 let Verify::Render { preset, strict } = verify else {
149 return Ok(None);
150 };
151 let comparison = match verify::render::compare(input, output, preset) {
152 Ok(comparison) => comparison,
153 Err(e) => {
154 report.note(format!("render: skipped, {e}"));
155 return Ok(None);
156 }
157 };
158 report.note(comparison.to_string());
159 if !comparison.below_floor().is_empty() {
160 if strict {
161 return Err(Refusal::BelowSimilarityFloor(comparison));
162 }
163 report.note("warning: pages below the similarity floor; output written anyway");
164 }
165 Ok(Some(comparison))
166}
167
168#[cfg(test)]
169mod tests {
170 use lopdf::{Object, dictionary};
171
172 use super::*;
173
174 fn small_pdf() -> Vec<u8> {
176 let mut doc = Document::with_version("1.5");
177 let pages_id = doc.new_object_id();
178 let content = doc.add_object(lopdf::Stream::new(
181 dictionary! {},
182 b"0 0 1 rg 10 10 100 100 re f\n".repeat(200),
183 ));
184 let page = doc.add_object(dictionary! {
185 "Type" => "Page", "Parent" => pages_id, "Contents" => content,
186 "MediaBox" => vec![0.into(), 0.into(), 200.into(), 200.into()],
187 });
188 doc.objects.insert(
189 pages_id,
190 Object::Dictionary(
191 dictionary! { "Type" => "Pages", "Kids" => vec![page.into()], "Count" => 1 },
192 ),
193 );
194 let catalog = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
195 doc.trailer.set("Root", catalog);
196 let mut bytes = Vec::new();
197 doc.save_to(&mut bytes).unwrap();
198 bytes
199 }
200
201 #[test]
202 fn compresses_and_verifies_a_small_document() {
203 let input = small_pdf();
204 let config = Config::preset(Preset::Standard);
205 let verify = Verify::Render {
206 preset: Preset::Standard,
207 strict: true,
208 };
209 let done = compress(&input, &config, verify).unwrap();
210 assert!(done.output.len() <= input.len());
211 assert!(done.verification.is_some_and(|v| v.is_ok()));
212 assert!(done.render.is_some_and(|r| r.min().unwrap_or(0.0) > 0.99));
213 }
214
215 #[test]
216 fn unparseable_input_is_rejected_with_its_report() {
217 let config = Config::preset(Preset::Less);
218 let rejected = compress(b"not a pdf", &config, Verify::Structural).unwrap_err();
219 assert!(matches!(rejected.reason, Refusal::Unparseable(_)));
220 assert!(rejected.to_string().contains("parsing the input"));
221 assert_eq!(rejected.report.notes.len(), 0);
222 }
223}