Skip to main content

compress_pdf/
report.rs

1//! What the pipeline did, in a form a human can check against the reference
2//! tool. Every stage appends to this; nothing else prints.
3
4use std::fmt;
5use std::time::Duration;
6
7use lopdf::ObjectId;
8
9/// One row per image XObject the image stage looked at.
10#[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    /// Minimum effective resolution across all placements, if known.
20    pub effective_dpi: Option<f32>,
21    pub bytes_in: usize,
22    /// Short verb: "kept", "downsampled", "recoded", "gray", "skipped: `<why>`".
23    pub action: String,
24    pub filter_out: String,
25    pub bytes_out: usize,
26}
27
28/// One row per embedded font program the font stage looked at.
29#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub struct FontRow {
32    pub object: ObjectId,
33    pub name: String,
34    /// Program kind: Type1, TrueType, CFF, CIDFontType0C, OpenType.
35    pub program: String,
36    pub bytes_in: usize,
37    /// Short verb: "kept", "unembedded", "subset", "merged", "cff", "kept: `<why>`".
38    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    /// Free-form observations (unsupported features hit, fallbacks taken).
60    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}