mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M11: PDF Optimizer ▸ Audit space usage: bytes and share of the file per kind of content; doc_audit_space
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
55275cd935
commit
763cd70ece
14 files changed
+356
-13
No files matched your search
Generated
+1
@@ -3504,6 +3504,7 @@ dependencies = [
|
||||
"printcraft-annot",
|
||||
"printcraft-content",
|
||||
"printcraft-cos",
|
||||
"printcraft-model",
|
||||
"printcraft-render",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
@@ -275,6 +275,15 @@ impl Automation {
|
||||
"doc_optimize" => self.doc_optimize(&a)?,
|
||||
"doc_initial_view" => self.doc_initial_view(&a)?,
|
||||
"doc_revisions" => self.doc_revisions(&a)?,
|
||||
"doc_audit_space" => {
|
||||
let rows: Vec<Value> = self
|
||||
.doc(&a)?
|
||||
.audit_space()
|
||||
.iter()
|
||||
.map(|u| json!({ "category": u.category.label(), "bytes": u.bytes, "percent": (u.percent * 100.0).round() / 100.0 }))
|
||||
.collect();
|
||||
json!({ "categories": rows })
|
||||
}
|
||||
"doc_open_revision" => {
|
||||
let id = self.doc(&a)?.id;
|
||||
let n = usize::try_from(a.opt_int("revision")?.ok_or_else(|| ToolError::InvalidArgs("revision is required".into()))?).unwrap_or(0);
|
||||
|
||||
@@ -804,6 +804,10 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
}),
|
||||
&["doc"],
|
||||
)),
|
||||
t("doc_audit_space", "Audit space usage", "How many bytes each kind of content takes and its share of the file (PDF Optimizer ▸ Audit space usage): images, content streams, fonts, forms, comments, structure, bookmarks, … and document overhead.")
|
||||
.ro()
|
||||
.cmd("optimize.advanced")
|
||||
.with(schema(json!({ "doc": doc() }), &["doc"])),
|
||||
t("doc_revisions", "List revisions", "List the document's saved revisions (oldest first): each incremental update is one. Returns revision number, where it ends in the file and its size, and which signatures sign exactly that revision.")
|
||||
.with(schema(json!({ "doc": doc() }), &["doc"])),
|
||||
t("doc_open_revision", "Open a revision", "Open saved revision `revision` (1 = the oldest) of a document as a new, unsaved document, to see the file as it was then.")
|
||||
|
||||
@@ -635,6 +635,19 @@ trailer << /Root 1 0 R >>
|
||||
assert!(matches!(a.call("accessibility_set_alt", &json!({ "doc": fd, "figure": 6, "alt": "x" })), Err(ToolError::Failed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auditing_space_through_tools() {
|
||||
let dir = workdir("audit");
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
|
||||
let r = ok(&mut a, "doc_audit_space", json!({ "doc": doc }));
|
||||
let rows = r["categories"].as_array().unwrap();
|
||||
let total: f64 = rows.iter().map(|x| x["percent"].as_f64().unwrap()).sum();
|
||||
assert!((total - 100.0).abs() < 0.1, "{r}");
|
||||
let content = rows.iter().find(|x| x["category"] == "Content Streams").unwrap();
|
||||
assert!(content["bytes"].as_u64().unwrap() > 0, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exporting_all_images_through_tools() {
|
||||
let dir = workdir("export-all-images");
|
||||
|
||||
@@ -162,6 +162,11 @@ impl Document {
|
||||
self.editor.as_ref().map(|e| a11y::check(&e.cos, options))
|
||||
}
|
||||
|
||||
/// PDF Optimizer ▸ Audit space usage.
|
||||
pub fn audit_space(&self) -> Vec<optimize::SpaceUse> {
|
||||
self.editor.as_ref().map(|e| optimize::audit_space(&e.cos, self.bytes.len() as u64)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Add alternate text: the figures, in document order.
|
||||
pub fn figures(&self) -> Vec<a11y::Figure> {
|
||||
self.editor.as_ref().map(|e| a11y::figures(&e.cos)).unwrap_or_default()
|
||||
|
||||
@@ -13,6 +13,7 @@ workspace = true
|
||||
[dependencies]
|
||||
printcraft-cos = { workspace = true }
|
||||
printcraft-content = { workspace = true }
|
||||
printcraft-model = { workspace = true }
|
||||
printcraft-annot = { workspace = true }
|
||||
thiserror.workspace = true
|
||||
# JPEG decoding (zune-jpeg) and encoding, and bicubic resampling; already in the tree.
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
//! PDF Optimizer ▸ Audit space usage: how many bytes each kind of content takes.
|
||||
//!
|
||||
//! Every object is sized as it would be written (dictionary plus stream data) and put in the
|
||||
//! first category that reaches it; what remains (the catalog, page tree, info, metadata,
|
||||
//! cross-reference data and anything unreachable) is document overhead.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use printcraft_cos::{Dict, Document, ObjRef, Object};
|
||||
|
||||
/// The categories, in Acrobat's order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum SpaceCategory {
|
||||
Images,
|
||||
ContentStreams,
|
||||
Fonts,
|
||||
XObjectForms,
|
||||
StructureInfo,
|
||||
Bookmarks,
|
||||
Thumbnails,
|
||||
Comments,
|
||||
Forms,
|
||||
NamedDestinations,
|
||||
ColorSpaces,
|
||||
Shadings,
|
||||
Patterns,
|
||||
ExtendedGraphicsStates,
|
||||
EmbeddedFiles,
|
||||
DocumentOverhead,
|
||||
}
|
||||
|
||||
impl SpaceCategory {
|
||||
pub const ALL: [SpaceCategory; 16] = [
|
||||
SpaceCategory::Images,
|
||||
SpaceCategory::ContentStreams,
|
||||
SpaceCategory::Fonts,
|
||||
SpaceCategory::XObjectForms,
|
||||
SpaceCategory::StructureInfo,
|
||||
SpaceCategory::Bookmarks,
|
||||
SpaceCategory::Thumbnails,
|
||||
SpaceCategory::Comments,
|
||||
SpaceCategory::Forms,
|
||||
SpaceCategory::NamedDestinations,
|
||||
SpaceCategory::ColorSpaces,
|
||||
SpaceCategory::Shadings,
|
||||
SpaceCategory::Patterns,
|
||||
SpaceCategory::ExtendedGraphicsStates,
|
||||
SpaceCategory::EmbeddedFiles,
|
||||
SpaceCategory::DocumentOverhead,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
SpaceCategory::Images => "Images",
|
||||
SpaceCategory::ContentStreams => "Content Streams",
|
||||
SpaceCategory::Fonts => "Fonts",
|
||||
SpaceCategory::XObjectForms => "X Object Forms",
|
||||
SpaceCategory::StructureInfo => "Structure Info",
|
||||
SpaceCategory::Bookmarks => "Bookmarks",
|
||||
SpaceCategory::Thumbnails => "Thumbnails",
|
||||
SpaceCategory::Comments => "Comments",
|
||||
SpaceCategory::Forms => "Forms",
|
||||
SpaceCategory::NamedDestinations => "Named Destinations",
|
||||
SpaceCategory::ColorSpaces => "Color Spaces",
|
||||
SpaceCategory::Shadings => "Shadings",
|
||||
SpaceCategory::Patterns => "Patterns",
|
||||
SpaceCategory::ExtendedGraphicsStates => "Extended Graphics States",
|
||||
SpaceCategory::EmbeddedFiles => "Embedded Files",
|
||||
SpaceCategory::DocumentOverhead => "Document Overhead",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One row of the audit.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SpaceUse {
|
||||
pub category: SpaceCategory,
|
||||
pub bytes: u64,
|
||||
/// Share of the file (0–100).
|
||||
pub percent: f64,
|
||||
}
|
||||
|
||||
struct Audit<'a> {
|
||||
doc: &'a Document,
|
||||
owner: HashMap<u32, SpaceCategory>,
|
||||
}
|
||||
|
||||
impl Audit<'_> {
|
||||
/// Claim `o` and everything it references for `cat` (objects already claimed keep theirs).
|
||||
fn claim(&mut self, o: &Object, cat: SpaceCategory) {
|
||||
let mut stack = vec![o.clone()];
|
||||
let mut seen = HashSet::new();
|
||||
while let Some(o) = stack.pop() {
|
||||
match o {
|
||||
Object::Ref(r) => {
|
||||
if !seen.insert(r.num) || self.owner.contains_key(&r.num) {
|
||||
continue;
|
||||
}
|
||||
// Pages belong to the page tree, not to whatever links to them.
|
||||
let obj = self.doc.get(r);
|
||||
if obj.as_dict().is_some_and(|d| matches!(d.name(b"Type"), Some(b"Page" | b"Pages"))) {
|
||||
continue;
|
||||
}
|
||||
self.owner.insert(r.num, cat);
|
||||
stack.push((*obj).clone());
|
||||
}
|
||||
Object::Array(a) => stack.extend(a),
|
||||
Object::Dict(d) => stack.extend(d.iter().filter(|(k, _)| k.as_slice() != b"Parent" && k.as_slice() != b"P").map(|(_, v)| v.clone())),
|
||||
Object::Stream(s) => stack.extend(s.dict.iter().filter(|(k, _)| k.as_slice() != b"Parent").map(|(_, v)| v.clone())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_dict(&self, o: Option<&Object>) -> Dict {
|
||||
o.map(|x| self.doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A page's resources, by kind.
|
||||
fn resources(&mut self, res: &Dict) {
|
||||
let xobjects = self.resolve_dict(res.get(b"XObject"));
|
||||
for (_, x) in xobjects.iter() {
|
||||
let Some(r) = x.as_ref() else { continue };
|
||||
let is_image = matches!(&*self.doc.get(r), Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Image"));
|
||||
if is_image {
|
||||
self.claim(x, SpaceCategory::Images);
|
||||
} else {
|
||||
// A form: its own resources first, by kind, then the form itself.
|
||||
if let Object::Stream(s) = &*self.doc.get(r) {
|
||||
let inner = self.resolve_dict(s.dict.get(b"Resources"));
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = self.owner.entry(r.num) {
|
||||
e.insert(SpaceCategory::XObjectForms);
|
||||
self.resources(&inner);
|
||||
}
|
||||
}
|
||||
self.claim(x, SpaceCategory::XObjectForms);
|
||||
}
|
||||
}
|
||||
for (key, cat) in [
|
||||
(&b"Font"[..], SpaceCategory::Fonts),
|
||||
(b"ColorSpace", SpaceCategory::ColorSpaces),
|
||||
(b"Shading", SpaceCategory::Shadings),
|
||||
(b"Pattern", SpaceCategory::Patterns),
|
||||
(b"ExtGState", SpaceCategory::ExtendedGraphicsStates),
|
||||
] {
|
||||
if let Some(v) = res.get(key) {
|
||||
self.claim(v, cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size(doc: &Document, num: u32) -> u64 {
|
||||
let obj = doc.get(ObjRef::new(num, doc.generation(num)));
|
||||
let mut out = Vec::new();
|
||||
printcraft_cos::serialize(&obj, &mut out);
|
||||
// "n g obj\n … \nendobj\n" and its cross-reference entry.
|
||||
out.len() as u64 + 20 + 20
|
||||
}
|
||||
|
||||
/// Audit the space a document uses; `file_len` is the size of the file on disk (the rest is
|
||||
/// overhead).
|
||||
pub fn audit_space(doc: &Document, file_len: u64) -> Vec<SpaceUse> {
|
||||
let mut a = Audit { doc, owner: HashMap::new() };
|
||||
let cat = doc.root().and_then(|r| doc.get(r).as_dict().cloned()).unwrap_or_default();
|
||||
let pages = printcraft_model::pages(doc);
|
||||
// Order matters: the most specific owners first.
|
||||
if let Some(s) = cat.get(b"StructTreeRoot") {
|
||||
// Structure elements point at pages and annotations; those stay out (claim skips pages,
|
||||
// and annotations are claimed first below).
|
||||
for p in &pages {
|
||||
if let Some(annots) = p.dict.get(b"Annots") {
|
||||
for x in doc.resolve(annots).as_array().cloned().unwrap_or_default() {
|
||||
let widget = x.as_ref().and_then(|r| doc.get(r).as_dict().map(|d| d.name(b"Subtype") == Some(b"Widget"))).unwrap_or(false);
|
||||
a.claim(&x, if widget { SpaceCategory::Forms } else { SpaceCategory::Comments });
|
||||
}
|
||||
}
|
||||
}
|
||||
a.claim(s, SpaceCategory::StructureInfo);
|
||||
}
|
||||
if let Some(f) = cat.get(b"AcroForm") {
|
||||
a.claim(f, SpaceCategory::Forms);
|
||||
}
|
||||
for p in &pages {
|
||||
if let Some(annots) = p.dict.get(b"Annots") {
|
||||
for x in doc.resolve(annots).as_array().cloned().unwrap_or_default() {
|
||||
let widget = x.as_ref().and_then(|r| doc.get(r).as_dict().map(|d| d.name(b"Subtype") == Some(b"Widget"))).unwrap_or(false);
|
||||
a.claim(&x, if widget { SpaceCategory::Forms } else { SpaceCategory::Comments });
|
||||
}
|
||||
}
|
||||
if let Some(c) = p.dict.get(b"Contents") {
|
||||
a.claim(c, SpaceCategory::ContentStreams);
|
||||
}
|
||||
let res = a.resolve_dict(p.dict.get(b"Resources"));
|
||||
a.resources(&res);
|
||||
if let Some(t) = p.dict.get(b"Thumb") {
|
||||
a.claim(t, SpaceCategory::Thumbnails);
|
||||
}
|
||||
}
|
||||
if let Some(o) = cat.get(b"Outlines") {
|
||||
a.claim(o, SpaceCategory::Bookmarks);
|
||||
}
|
||||
let names = a.resolve_dict(cat.get(b"Names"));
|
||||
if let Some(d) = names.get(b"Dests").or_else(|| cat.get(b"Dests")) {
|
||||
a.claim(d, SpaceCategory::NamedDestinations);
|
||||
}
|
||||
if let Some(e) = names.get(b"EmbeddedFiles") {
|
||||
a.claim(e, SpaceCategory::EmbeddedFiles);
|
||||
}
|
||||
let mut bytes: HashMap<SpaceCategory, u64> = HashMap::new();
|
||||
for num in doc.object_numbers() {
|
||||
if let Some(c) = a.owner.get(&num) {
|
||||
*bytes.entry(*c).or_default() += size(doc, num);
|
||||
}
|
||||
}
|
||||
let counted: u64 = bytes.values().sum();
|
||||
let total = file_len.max(counted).max(1);
|
||||
bytes.insert(SpaceCategory::DocumentOverhead, total - counted);
|
||||
SpaceCategory::ALL
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let b = bytes.get(&c).copied().unwrap_or(0);
|
||||
SpaceUse { category: c, bytes: b, percent: b as f64 * 100.0 / total as f64 }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -15,8 +15,11 @@
|
||||
//! fields are discarded by `printcraft-redact`'s Remove Hidden Information, which the engine
|
||||
//! runs alongside.
|
||||
|
||||
mod audit;
|
||||
mod images;
|
||||
|
||||
pub use audit::{SpaceCategory, SpaceUse, audit_space};
|
||||
|
||||
use printcraft_cos::{Document, ObjRef, Object, Stream};
|
||||
|
||||
pub use images::effective_resolutions;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use printcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full};
|
||||
use printcraft_optimize::{Compression, ImageSettings, Settings, effective_resolutions, optimize};
|
||||
use printcraft_optimize::{Compression, ImageSettings, Settings, SpaceCategory, audit_space, effective_resolutions, optimize};
|
||||
|
||||
fn image(doc: &mut Document, w: u32, h: u32, n: usize, jpeg: bool, smask: Option<ObjRef>) -> ObjRef {
|
||||
// A smooth gradient with a little texture (photo-like).
|
||||
@@ -175,3 +175,20 @@ fn discards_and_clean_up() {
|
||||
let vp = c.get(b"ViewerPreferences").and_then(|v| v.as_dict().cloned()).unwrap();
|
||||
assert!(vp.contains(b"HideToolbar") && !vp.contains(b"Duplex"), "other preferences stay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_audit_shares_the_file_out_by_kind() {
|
||||
let mut doc = Document::new_empty();
|
||||
let img = image(&mut doc, 300, 300, 3, false, None);
|
||||
page(&mut doc, &[("Im0", img)], "q 300 0 0 300 0 0 cm /Im0 Do Q BT ET");
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
let doc = Document::open(Arc::new(bytes.clone())).unwrap();
|
||||
let audit = audit_space(&doc, bytes.len() as u64);
|
||||
assert_eq!(audit.len(), SpaceCategory::ALL.len());
|
||||
let get = |c: SpaceCategory| audit.iter().find(|u| u.category == c).unwrap().clone();
|
||||
let (images, content, overhead) = (get(SpaceCategory::Images), get(SpaceCategory::ContentStreams), get(SpaceCategory::DocumentOverhead));
|
||||
assert!(images.percent > 80.0, "{audit:?}");
|
||||
assert!(content.bytes > 0 && overhead.bytes > 0, "{audit:?}");
|
||||
assert_eq!(audit.iter().map(|u| u.bytes).sum::<u64>(), bytes.len() as u64);
|
||||
assert!((audit.iter().map(|u| u.percent).sum::<f64>() - 100.0).abs() < 1e-6);
|
||||
}
|
||||
@@ -651,6 +651,16 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
||||
let (ok, cancel) = crate::optimize_ui::body(ui, &mut app.optimize_draft, &t);
|
||||
optimize_now = ok;
|
||||
close = ok || cancel;
|
||||
if std::mem::take(&mut app.optimize_draft.audit) {
|
||||
app.space_audit = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.audit_space()).unwrap_or_default();
|
||||
next = Dialog::AuditSpace;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Dialog::AuditSpace => {
|
||||
if crate::optimize_ui::audit_body(ui, &app.space_audit, &t) {
|
||||
next = Dialog::Optimize;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Dialog::Sign => {
|
||||
|
||||
@@ -187,6 +187,8 @@ pub enum Dialog {
|
||||
CreateStamp,
|
||||
/// Prepare for accessibility ▸ Add alternate text.
|
||||
AltText,
|
||||
/// PDF Optimizer ▸ Audit space usage (then back to the optimizer).
|
||||
AuditSpace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -290,6 +292,8 @@ pub struct PrintCraftApp {
|
||||
pub a11y: a11y_ui::A11yState,
|
||||
pub a11y_skipped: std::collections::BTreeSet<printcraft_engine::a11y::Rule>,
|
||||
pub alt_draft: a11y_ui::AltDraft,
|
||||
/// The last space audit.
|
||||
pub space_audit: Vec<printcraft_engine::optimize::SpaceUse>,
|
||||
/// Combine files: the files staged so far.
|
||||
pub combine_draft: Vec<combine_ui::CombineFile>,
|
||||
/// The custom stamp library, and the stamp being created.
|
||||
@@ -418,6 +422,7 @@ impl PrintCraftApp {
|
||||
a11y: a11y_ui::A11yState::default(),
|
||||
a11y_skipped: Default::default(),
|
||||
alt_draft: Default::default(),
|
||||
space_audit: Vec::new(),
|
||||
combine_draft: Vec::new(),
|
||||
custom_stamps: Vec::new(),
|
||||
stamp_draft: Default::default(),
|
||||
|
||||
@@ -22,11 +22,13 @@ pub struct OptimizeDraft {
|
||||
pub settings: Settings,
|
||||
/// Remove Hidden Information categories to discard.
|
||||
pub discard: Vec<Hidden>,
|
||||
/// "Audit space usage…" was pressed.
|
||||
pub audit: bool,
|
||||
}
|
||||
|
||||
impl Default for OptimizeDraft {
|
||||
fn default() -> Self {
|
||||
Self { tab: OptimizeTab::Images, settings: Settings::default(), discard: Vec::new() }
|
||||
Self { tab: OptimizeTab::Images, settings: Settings::default(), discard: Vec::new(), audit: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,17 +135,49 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
let (mut ok, mut cancel) = (false, false);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
ok = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
cancel = true;
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::pill_button(ui, "Audit space usage…", false).clicked() {
|
||||
d.audit = true;
|
||||
}
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, "OK", true).clicked() {
|
||||
ok = true;
|
||||
}
|
||||
if widgets::pill_button(ui, "Cancel", false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
(ok, cancel)
|
||||
}
|
||||
|
||||
/// Audit Space Usage: bytes and share of the file per kind of content.
|
||||
pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[printcraft_engine::optimize::SpaceUse], t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("Space Audit").font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("space-audit").num_columns(3).striped(true).spacing([24.0, 4.0]).show(ui, |ui| {
|
||||
for h in ["Description", "Bytes", "Percentage"] {
|
||||
ui.label(egui::RichText::new(h).color(t.text_muted));
|
||||
}
|
||||
ui.end_row();
|
||||
let total: u64 = rows.iter().map(|r| r.bytes).sum();
|
||||
for r in rows.iter().filter(|r| r.bytes > 0) {
|
||||
ui.label(r.category.label());
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(r.bytes.to_string()));
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(format!("{:.2}%", r.percent)));
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(egui::RichText::new("Total").strong());
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(egui::RichText::new(total.to_string()).strong()));
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ui.label(egui::RichText::new("100.00%").strong()));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
let mut ok = false;
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| ok = widgets::pill_button(ui, "OK", true).clicked());
|
||||
ok
|
||||
}
|
||||
|
||||
impl PrintCraftApp {
|
||||
/// Optimize PDF with the dialog's choices and save the copy.
|
||||
pub fn optimize_with_draft(&mut self) {
|
||||
|
||||
@@ -351,3 +351,18 @@ fn advanced_search_lists_results_with_context() {
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().views[i].find.as_ref().unwrap().in_panel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimizer_audits_space_usage() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("optimize.advanced"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Audit space usage…").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Space Audit");
|
||||
h.get_by_label("Content Streams");
|
||||
h.get_by_label("Total");
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Optimize), "back to the optimizer");
|
||||
}
|
||||
@@ -7427,11 +7427,11 @@ title = "Audit space usage"
|
||||
area = "K"
|
||||
tier = "P1"
|
||||
milestone = "M11"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["optimize.advanced"]
|
||||
tools = ["doc_audit_space"]
|
||||
evidence = ["crates/optimize/tests/optimize.rs::space_audit_shares_the_file_out_by_kind", "crates/automation/tests/automation.rs::auditing_space_through_tools", "crates/ui-egui/tests/viewer.rs::optimizer_audits_space_usage"]
|
||||
notes = "PDF Optimizer ▸ Audit space usage: 16 categories (images, content streams, fonts, X Object forms, structure, bookmarks, thumbnails, comments, forms, named destinations, colour spaces, shadings, patterns, graphics states, embedded files, overhead) with bytes and percentages"
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.optimizer-presets"
|
||||
|
||||
Reference in new issue
Block a user