1use std::fmt;
5use std::time::Duration;
6
7use lopdf::ObjectId;
8
9#[derive(Debug, Clone)]
11#[non_exhaustive]
12pub struct ImageRow {
13 pub object: ObjectId,
14 pub width: u32,
15 pub height: u32,
16 pub bits_per_component: u8,
17 pub color_space: String,
18 pub filter_in: String,
19 pub effective_dpi: Option<f32>,
21 pub bytes_in: usize,
22 pub action: String,
24 pub filter_out: String,
25 pub bytes_out: usize,
26}
27
28#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub struct FontRow {
32 pub object: ObjectId,
33 pub name: String,
34 pub program: String,
36 pub bytes_in: usize,
37 pub action: String,
39 pub bytes_out: usize,
40}
41
42#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub struct StageSummary {
45 pub name: &'static str,
46 pub bytes_before: usize,
47 pub bytes_after: usize,
48 pub elapsed: Duration,
49}
50
51#[derive(Debug, Default)]
52#[non_exhaustive]
53pub struct Report {
54 pub input_bytes: usize,
55 pub output_bytes: usize,
56 pub images: Vec<ImageRow>,
57 pub fonts: Vec<FontRow>,
58 pub stages: Vec<StageSummary>,
59 pub notes: Vec<String>,
61}
62
63impl Report {
64 pub fn new(input_bytes: usize) -> Report {
65 Report {
66 input_bytes,
67 ..Report::default()
68 }
69 }
70
71 pub fn note(&mut self, msg: impl Into<String>) {
72 self.notes.push(msg.into());
73 }
74}
75
76fn human(bytes: usize) -> String {
77 const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
78 let mut v = bytes as f64;
79 let mut i = 0;
80 while v >= 1024.0 && i < UNITS.len() - 1 {
81 v /= 1024.0;
82 i += 1;
83 }
84 if i == 0 {
85 format!("{bytes} B")
86 } else {
87 format!("{v:.1} {}", UNITS[i])
88 }
89}
90
91fn pct(before: usize, after: usize) -> String {
92 if before == 0 {
93 return "-".into();
94 }
95 format!(
96 "{:+.1}%",
97 (after as f64 - before as f64) / before as f64 * 100.0
98 )
99}
100
101impl fmt::Display for Report {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 if !self.images.is_empty() {
104 writeln!(
105 f,
106 "{:<9} {:>11} {:>4} {:<12} {:<10} {:>7} {:>10} {:<14} {:<10} {:>10}",
107 "object",
108 "size",
109 "bpc",
110 "colorspace",
111 "filter",
112 "dpi",
113 "bytes in",
114 "action",
115 "filter out",
116 "bytes out"
117 )?;
118 for r in &self.images {
119 writeln!(
120 f,
121 "{:<9} {:>11} {:>4} {:<12} {:<10} {:>7} {:>10} {:<14} {:<10} {:>10}",
122 format!("{} {}", r.object.0, r.object.1),
123 format!("{}x{}", r.width, r.height),
124 r.bits_per_component,
125 r.color_space,
126 r.filter_in,
127 r.effective_dpi
128 .map(|d| format!("{d:.0}"))
129 .unwrap_or_else(|| "?".into()),
130 r.bytes_in,
131 r.action,
132 r.filter_out,
133 r.bytes_out,
134 )?;
135 }
136 writeln!(f)?;
137 }
138
139 if !self.fonts.is_empty() {
140 writeln!(
141 f,
142 "{:<9} {:<32} {:<14} {:>10} {:<22} {:>10}",
143 "object", "font", "program", "bytes in", "action", "bytes out"
144 )?;
145 for r in &self.fonts {
146 writeln!(
147 f,
148 "{:<9} {:<32} {:<14} {:>10} {:<22} {:>10}",
149 format!("{} {}", r.object.0, r.object.1),
150 r.name,
151 r.program,
152 r.bytes_in,
153 r.action,
154 r.bytes_out,
155 )?;
156 }
157 writeln!(f)?;
158 }
159
160 if !self.stages.is_empty() {
161 writeln!(
162 f,
163 "{:<12} {:>12} {:>12} {:>8} {:>8}",
164 "stage", "before", "after", "delta", "time"
165 )?;
166 for s in &self.stages {
167 writeln!(
168 f,
169 "{:<12} {:>12} {:>12} {:>8} {:>7.0}ms",
170 s.name,
171 human(s.bytes_before),
172 human(s.bytes_after),
173 pct(s.bytes_before, s.bytes_after),
174 s.elapsed.as_secs_f64() * 1000.0,
175 )?;
176 }
177 writeln!(f)?;
178 }
179
180 for n in &self.notes {
181 writeln!(f, "note: {n}")?;
182 }
183
184 writeln!(
185 f,
186 "total: {} -> {} ({})",
187 human(self.input_bytes),
188 human(self.output_bytes),
189 pct(self.input_bytes, self.output_bytes)
190 )
191 }
192}