mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix: Space Audit lines up its columns and counts form fonts as fonts (#2118)
* fix(optimize): count what form fields and comments draw with by kind The space audit claimed the AcroForm and annotations before any page resources, so the fonts in /DR and in widget and comment appearance streams were counted as Forms or Comments. A fillable form with an embedded field font showed mostly "Forms" and almost no "Fonts". Resources are now sorted by kind first, wherever they are used: page resources, the form's default resources and every annotation appearance (one stream or a dictionary of states). A form XObject's other entries now count as X Object Forms instead of overhead. Sorting nested forms' resources recursed once per form, so a long chain of nested forms overflowed the stack. The recursion is now bounded at 64 levels; deeper forms are still counted as X Object Forms through the iterative walk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): line up the Space Audit columns and say what it shows The percentages were drawn at the dialog's far edge, away from their header and outside the striped rows: egui gives a grid's last column the rest of the window, and the right-to-left layout used to right-align the numbers filled all of it. Every cell is now laid out first and given exactly its column's width; numeric columns and their headers are right-aligned, and the description column takes the rest, so the stripes span the dialog. A caption under the title says the table is what takes up the file's space now (not what optimizing would save), translated in every catalogue. The parity notes no longer call the space audit missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refresh catalog hashes after merging main --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
716a957186
commit
b369a50b68
+21
-21
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "0576900abcb7d419ec81843274632db330daaa5964c91ccb5933b2a3053257cc"
|
||||
sha256 = "22f3fdd8849e6ef0f8dca9dcbb4f7a14835162379b84f89de05eafdc68f3557d"
|
||||
|
||||
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
||||
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
||||
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "aaf94f24b7fb7c7fcbbd9003ba01e8fe5e775497d5c691e161dadadb0a85ac18"
|
||||
sha256 = "96bf1d0993b3d1e19fb4953c37f47649d7b7e373d600b17f12c8aca2e3644d57"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "4d25e7a10678751d8fecf8ee8688e04343c807b9565a21e72bf784cdd8e30238"
|
||||
sha256 = "78569ef1bffd54432b57ce84d21f1c65e6e3cf60f47f19a284b4d5e7a4c4aa71"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
||||
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "bfa7f7d2cc008f676c5c533cda9f6b9415c8b6cfe43d2c88fab62440825cb57b"
|
||||
sha256 = "e3e2e7b926767e32d1d4c105749d8f2fa570245b4ed6cdddf0ec6ff0f19eee24"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "003747152cfb5f42ccb2b3e77351902a2594e2a263cc0750e122d6f1f94802d1"
|
||||
sha256 = "aca4600c31bbc48a9c46328643704850f383cf799f7fed4651aaf1f0bcecc1b7"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "dc3267d8d4882998e32de736fb28ebc247e5102530fea852caa585f1c43d3150"
|
||||
sha256 = "0d5992212ead5820c0686b9a1990f29949d347e73d4c485a66e029c8edbaccb0"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "869505afeb011a27067fa55f1521a8d06ab3efd026fed4854610816acca13926"
|
||||
sha256 = "bae65abd8480f3dfcb36fbdc9ad7578adf78e7630e3c81ffc4cc7351d61cfd55"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
||||
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "cffb456aac29be21da005ca52ae8131f0c07430e1723dd6da61073bba7501c8b"
|
||||
sha256 = "9db8eaa561c362f9efcf44f4b5f2c380615dead8da29e66fbe49085c71c4af13"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7db981779e3c478b51b8c8aba71a53dc8153165e31ea57096d1307d30027b658"
|
||||
sha256 = "96a8ed3759e00d5b400472d186d418b88b1a0c33fa15644399506d998975083b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
||||
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "6b6197bc0dd5b93813d5d6652c7ba2a3dc9b0d06eaed65292837a8f15d880f43"
|
||||
sha256 = "84900cd6c913d77f55d229f2204e778136029f52772d82a0230525a24fada4a1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e6c9510edf8cb9677be5b830d6626740b8f2821799211701d8d16708fa9408b5"
|
||||
sha256 = "944d8e2bb7f5ea24adac2251a4f3f17af971f521e70026727cf17e362339bbea"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "9f6d11f2ccf4c873a16d0f2ae3dddd449f7fad1238126b968ed8813e293d0ee8"
|
||||
sha256 = "4716f4e2124a669634e68f93a0d99e339c531ade667b1cb6c769f9349cd15517"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
||||
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "15f572ed291aebc26911915e7647aef30998787a2bc87128addd273a84678406"
|
||||
sha256 = "161b8fec2d1d832d2a88c479decc0f666cda01ccec5f60d1338b54ba11242358"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "35f1711fd3358dd29c784769bd9ff2094f977bf78cb9146eefa2f45f62f80d52"
|
||||
sha256 = "e7fd488de8417519614725f150ba3621c86abcf97e70bd4dfcc4fb01bd09d7ac"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/kk.tsv"
|
||||
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "8974dfe02d8c8f8453a156d68846f2171334fcda70d5fc795b53b52e13c2eb8f"
|
||||
sha256 = "df9c65e807e3bcd86b283248510ccca5ab470ae6209fe4a367a7238826416b4f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "5ef65d3f913a1f97cdd54a07f4cc819bf128dfc20de247eadb2519bb98c7f102"
|
||||
sha256 = "a7fbbd81686e0001b7173b51e4feaa8d4e00cfbbcb48fef350122088a494f6d6"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
||||
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "91da344ec2e6181f7c7b8f13dc0c878ccfe0600a8d464cbd188d082c6c402d4c"
|
||||
sha256 = "c7550bde0d8b00e348a7c89f6ade7bb960371a75d88968bd5af588257ebc68be"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
||||
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "3f7534373129ce75ef26a87aaef3f5074be3d8326ded9d2d77d873e24359ec87"
|
||||
sha256 = "10a56fcf63ac1714ec23dd57ee5ddf7e0d16fd7eaebf28b065e58ed96c9f5d8e"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/tr.tsv"
|
||||
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d045ebd97c1383e63ea8b9ff2a4279f8484357ff88b15f4bb4bb2cacce63022d"
|
||||
sha256 = "f111d02153db4277b6cddb77c5823ac30e1377f3aae59bca962a5884f3857d48"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
||||
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "afd09cdbcb97339d57a0be0010512ba8336d1b40ffcfbd2b4730808851c71d64"
|
||||
sha256 = "30b8a65a5fae8cf2c3fd5d43d53991095d2f00042899a66c3949573b28d4e3c4"
|
||||
|
||||
|
||||
[[asset]]
|
||||
@@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "714b6a58f5cd4f0c9220e88d2d3f7c584b64829b0336e23d7893209c84babf5d"
|
||||
sha256 = "ad4439af89320c314064d1062ab057b53366d857c6dd0deafbea7069ffc5920c"
|
||||
|
||||
[[asset]]
|
||||
path = "vendor/vello_common/assets/probe.png"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! 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.
|
||||
//! Every object is sized as it would be written (dictionary plus stream data). Resources go by
|
||||
//! kind first, wherever they are used: a font or image a form field or comment draws with is
|
||||
//! still a font or image. Every other object goes to 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};
|
||||
|
||||
@@ -80,6 +82,11 @@ pub struct SpaceUse {
|
||||
pub percent: f64,
|
||||
}
|
||||
|
||||
/// How deep form XObjects nested in each other's resources are sorted by kind. Deeper forms
|
||||
/// (and what they use) are still counted, as X Object Forms; the limit keeps a crafted chain
|
||||
/// of nested forms from overflowing the stack.
|
||||
const MAX_FORM_DEPTH: usize = 64;
|
||||
|
||||
struct Audit<'a> {
|
||||
doc: &'a Document,
|
||||
owner: HashMap<u32, SpaceCategory>,
|
||||
@@ -116,24 +123,22 @@ impl Audit<'_> {
|
||||
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) {
|
||||
/// A resource dictionary, by kind; `depth` counts the form XObjects it is nested in.
|
||||
fn resources(&mut self, res: &Dict, depth: usize) {
|
||||
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) {
|
||||
match &*self.doc.get(r) {
|
||||
Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Image") => self.claim(x, SpaceCategory::Images),
|
||||
Object::Stream(s) if depth < MAX_FORM_DEPTH && !self.owner.contains_key(&r.num) => {
|
||||
// A form: its own resources first, by kind, then the rest of it. Owning it
|
||||
// first stops a form that draws itself.
|
||||
self.owner.insert(r.num, SpaceCategory::XObjectForms);
|
||||
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.resources(&inner, depth + 1);
|
||||
self.claim(&Object::Dict(s.dict.clone()), SpaceCategory::XObjectForms);
|
||||
}
|
||||
self.claim(x, SpaceCategory::XObjectForms);
|
||||
_ => self.claim(x, SpaceCategory::XObjectForms),
|
||||
}
|
||||
}
|
||||
for (key, cat) in [
|
||||
@@ -148,6 +153,40 @@ impl Audit<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resources an annotation's appearances draw with, by kind. Each of /N, /R and /D is
|
||||
/// one appearance stream or a dictionary of them, one per state.
|
||||
fn appearance_resources(&mut self, annot: &Object) {
|
||||
let ap = self.resolve_dict(self.resolve_dict(Some(annot)).get(b"AP"));
|
||||
for (_, v) in ap.iter() {
|
||||
let streams = match &*self.doc.resolve(v) {
|
||||
Object::Stream(s) => vec![s.dict.clone()],
|
||||
Object::Dict(states) => states
|
||||
.iter()
|
||||
.filter_map(|(_, s)| match &*self.doc.resolve(s) {
|
||||
Object::Stream(s) => Some(s.dict.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for dict in streams {
|
||||
let res = self.resolve_dict(dict.get(b"Resources"));
|
||||
self.resources(&res, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A page's annotations.
|
||||
fn annotations(doc: &Document, page: &Dict) -> Vec<Object> {
|
||||
page.get(b"Annots").and_then(|a| doc.resolve(a).as_array().cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Form fields' widgets belong to the form; every other annotation is a comment.
|
||||
fn annotation_category(doc: &Document, annot: &Object) -> SpaceCategory {
|
||||
let widget = annot.as_ref().is_some_and(|r| doc.get(r).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Widget")));
|
||||
if widget { SpaceCategory::Forms } else { SpaceCategory::Comments }
|
||||
}
|
||||
|
||||
fn size(doc: &Document, num: u32) -> u64 {
|
||||
@@ -164,16 +203,27 @@ 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 = pdfcraft_model::pages(doc);
|
||||
// Order matters: the most specific owners first.
|
||||
// Order matters: the most specific owners first. Resources by kind before anything that
|
||||
// uses them: a content stream's dictionary may point at what it draws (PdfCraft's /PCAdded
|
||||
// records an added image), and form fields and comments draw with fonts and images too.
|
||||
for p in &pages {
|
||||
let res = a.resolve_dict(p.dict.get(b"Resources"));
|
||||
a.resources(&res, 0);
|
||||
}
|
||||
let form = a.resolve_dict(cat.get(b"AcroForm"));
|
||||
let form_resources = a.resolve_dict(form.get(b"DR"));
|
||||
a.resources(&form_resources, 0);
|
||||
for p in &pages {
|
||||
for x in annotations(doc, &p.dict) {
|
||||
a.appearance_resources(&x);
|
||||
}
|
||||
}
|
||||
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).
|
||||
// and annotations are claimed first).
|
||||
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 });
|
||||
}
|
||||
for x in annotations(doc, &p.dict) {
|
||||
a.claim(&x, annotation_category(doc, &x));
|
||||
}
|
||||
}
|
||||
a.claim(s, SpaceCategory::StructureInfo);
|
||||
@@ -182,16 +232,9 @@ pub fn audit_space(doc: &Document, file_len: u64) -> Vec<SpaceUse> {
|
||||
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 });
|
||||
}
|
||||
for x in annotations(doc, &p.dict) {
|
||||
a.claim(&x, annotation_category(doc, &x));
|
||||
}
|
||||
// Resources before the content: a content stream's dictionary may point at what it
|
||||
// draws (PdfCraft's /PCAdded records an added image), which stays an image or font.
|
||||
let res = a.resolve_dict(p.dict.get(b"Resources"));
|
||||
a.resources(&res);
|
||||
if let Some(c) = p.dict.get(b"Contents") {
|
||||
a.claim(c, SpaceCategory::ContentStreams);
|
||||
}
|
||||
|
||||
@@ -245,6 +245,100 @@ fn space_audit_counts_images_a_content_stream_dictionary_points_at_as_images() {
|
||||
assert!(images.percent > 80.0, "{audit:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_audit_counts_what_fields_and_comments_draw_with_by_kind() {
|
||||
// A font or image a form field or comment draws with is still a font or image. The form
|
||||
// was claimed first, so its fonts made a fillable form look like mostly "Forms".
|
||||
let mut doc = Document::new_empty();
|
||||
let p = page(&mut doc, &[], "BT ET");
|
||||
let font = |doc: &mut Document| {
|
||||
let file = doc.add(Object::Stream(Stream::from_raw(Dict::new(), vec![7; 40_000])));
|
||||
let mut desc = Dict::new();
|
||||
desc.set(b"Type".to_vec(), Object::name("FontDescriptor"));
|
||||
desc.set(b"FontFile2".to_vec(), Object::Ref(file));
|
||||
let desc = doc.add(Object::Dict(desc));
|
||||
let mut f = Dict::new();
|
||||
f.set(b"Type".to_vec(), Object::name("Font"));
|
||||
f.set(b"Subtype".to_vec(), Object::name("TrueType"));
|
||||
f.set(b"FontDescriptor".to_vec(), Object::Ref(desc));
|
||||
doc.add(Object::Dict(f))
|
||||
};
|
||||
let (field_font, comment_font) = (font(&mut doc), font(&mut doc));
|
||||
let icon = image(&mut doc, 200, 200, 3, false, None);
|
||||
let appearance = |doc: &mut Document, kind: &str, name: &str, r: ObjRef| {
|
||||
let mut entries = Dict::new();
|
||||
entries.set(name.as_bytes().to_vec(), Object::Ref(r));
|
||||
let mut res = Dict::new();
|
||||
res.set(kind.as_bytes().to_vec(), Object::Dict(entries));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
d.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
Object::Ref(doc.add(Object::Stream(Stream::from_raw(d, b"q Q".to_vec()))))
|
||||
};
|
||||
let annot = |doc: &mut Document, subtype: &str, ap: Object| {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Type".to_vec(), Object::name("Annot"));
|
||||
d.set(b"Subtype".to_vec(), Object::name(subtype));
|
||||
d.set(b"Rect".to_vec(), Object::Array(vec![0.into(), 0.into(), 10.into(), 10.into()]));
|
||||
d.set(b"P".to_vec(), Object::Ref(p));
|
||||
let mut aps = Dict::new();
|
||||
aps.set(b"N".to_vec(), ap);
|
||||
d.set(b"AP".to_vec(), Object::Dict(aps));
|
||||
Object::Ref(doc.add(Object::Dict(d)))
|
||||
};
|
||||
// A text field's appearance uses the form's default font; a check box's "on" state (one of
|
||||
// a dictionary of states) draws an image; a free-text comment has a font of its own.
|
||||
let text_ap = appearance(&mut doc, "Font", "F1", field_font);
|
||||
let text = annot(&mut doc, "Widget", text_ap);
|
||||
let mut states = Dict::new();
|
||||
states.set(b"On".to_vec(), appearance(&mut doc, "XObject", "Im0", icon));
|
||||
let check = annot(&mut doc, "Widget", Object::Dict(states));
|
||||
let comment_ap = appearance(&mut doc, "Font", "F2", comment_font);
|
||||
let comment = annot(&mut doc, "FreeText", comment_ap);
|
||||
doc.update_dict(p, |d| d.set(b"Annots".to_vec(), Object::Array(vec![text.clone(), check.clone(), comment]))).unwrap();
|
||||
let mut fonts = Dict::new();
|
||||
fonts.set(b"F1".to_vec(), Object::Ref(field_font));
|
||||
let mut dr = Dict::new();
|
||||
dr.set(b"Font".to_vec(), Object::Dict(fonts));
|
||||
let mut form = Dict::new();
|
||||
form.set(b"Fields".to_vec(), Object::Array(vec![text, check]));
|
||||
form.set(b"DR".to_vec(), Object::Dict(dr));
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |c| c.set(b"AcroForm".to_vec(), Object::Dict(form))).unwrap();
|
||||
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);
|
||||
let get = |c: SpaceCategory| audit.iter().find(|u| u.category == c).unwrap().bytes;
|
||||
assert!(get(SpaceCategory::Fonts) > 80_000, "both fonts are fonts: {audit:?}");
|
||||
assert!(get(SpaceCategory::Images) > 2_000, "the check box's image is an image: {audit:?}");
|
||||
assert!(get(SpaceCategory::Forms) < 2_000 && get(SpaceCategory::Comments) < 1_000, "{audit:?}");
|
||||
assert!(get(SpaceCategory::Forms) > 0 && get(SpaceCategory::Comments) > 0, "the fields and the comment are still counted: {audit:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_audit_survives_deeply_nested_forms() {
|
||||
// Each form draws the next; sorting their resources by kind recursed once per form, so a
|
||||
// long enough chain overflowed the stack.
|
||||
let mut doc = Document::new_empty();
|
||||
let mut next: Option<ObjRef> = None;
|
||||
for _ in 0..20_000 {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
if let Some(n) = next {
|
||||
let mut x = Dict::new();
|
||||
x.set(b"X".to_vec(), Object::Ref(n));
|
||||
let mut res = Dict::new();
|
||||
res.set(b"XObject".to_vec(), Object::Dict(x));
|
||||
d.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
}
|
||||
next = Some(doc.add(Object::Stream(Stream::from_raw(d, b"/X Do".to_vec()))));
|
||||
}
|
||||
page(&mut doc, &[("X", next.unwrap())], "/X Do");
|
||||
let audit = audit_space(&doc, 0);
|
||||
let forms = audit.iter().find(|u| u.category == SpaceCategory::XObjectForms).unwrap();
|
||||
assert!(forms.bytes > 20_000 * 40, "every form is counted, however deep: {audit:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_links_and_unreferenced_destinations_go() {
|
||||
let mut doc = Document::new_empty();
|
||||
|
||||
@@ -583,6 +583,7 @@
|
||||
Remove unused objects and merge identical ones إزالة الكائنات غير المستخدمة ودمج المتطابقة
|
||||
Audit space usage… تدقيق استخدام المساحة…
|
||||
Space Audit تدقيق المساحة
|
||||
What takes up the space in this file, by kind of content. ما الذي يشغل المساحة في هذا الملف، حسب نوع المحتوى.
|
||||
Bytes بايت
|
||||
Content Streams تدفقات المحتوى
|
||||
X Object Forms نماذج X Object
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -257,6 +257,7 @@
|
||||
Remove unused objects and merge identical ones Премахни неизползваните обекти и обедини еднаквите
|
||||
Audit space usage… Анализ на използваното място…
|
||||
Space Audit Анализ на мястото
|
||||
What takes up the space in this file, by kind of content. Какво заема мястото в този файл, по вид съдържание.
|
||||
Bytes Байтове
|
||||
Content Streams Потоци на съдържанието
|
||||
X Object Forms Форми X Object
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -663,6 +663,7 @@ key Space Mezerník
|
||||
Remove unused objects and merge identical ones Odebrat nepoužívané objekty a sloučit shodné
|
||||
Audit space usage… Analyzovat využití místa…
|
||||
Space Audit Analýza využití místa
|
||||
What takes up the space in this file, by kind of content. Co zabírá místo v tomto souboru, podle druhu obsahu.
|
||||
Bytes Bajty
|
||||
Content Streams Proudy obsahu
|
||||
X Object Forms Objekty Form XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -556,6 +556,7 @@
|
||||
Remove unused objects and merge identical ones Unbenutzte Objekte entfernen und identische zusammenführen
|
||||
Audit space usage… Speicherbelegung prüfen…
|
||||
Space Audit Speicherbelegung
|
||||
What takes up the space in this file, by kind of content. Wofür der Platz in dieser Datei verwendet wird, nach Art des Inhalts.
|
||||
Bytes Bytes
|
||||
Content Streams Inhalts-Streams
|
||||
X Object Forms XObject-Formulare
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -1883,6 +1883,7 @@
|
||||
Remove unused objects and merge identical ones Αφαίρεση αχρησιμοποίητων αντικειμένων και συγχώνευση πανομοιότυπων
|
||||
Audit space usage… Έλεγχος χρήσης χώρου…
|
||||
Space Audit Έλεγχος χώρου
|
||||
What takes up the space in this file, by kind of content. Τι καταλαμβάνει τον χώρο σε αυτό το αρχείο, ανά είδος περιεχομένου.
|
||||
Bytes Byte
|
||||
Content Streams Ροές περιεχομένου
|
||||
X Object Forms Φόρμες XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 475 and column 9.
|
@@ -579,6 +579,7 @@
|
||||
Remove unused objects and merge identical ones Quitar objetos sin usar y fusionar los idénticos
|
||||
Audit space usage… Auditar uso del espacio…
|
||||
Space Audit Auditoría de espacio
|
||||
What takes up the space in this file, by kind of content. Qué ocupa el espacio de este archivo, por tipo de contenido.
|
||||
Bytes Bytes
|
||||
Content Streams Secuencias de contenido
|
||||
X Object Forms Formularios XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -556,6 +556,7 @@
|
||||
Remove unused objects and merge identical ones Retirer les objets inutilisés et fusionner les identiques
|
||||
Audit space usage… Auditer l'espace utilisé…
|
||||
Space Audit Audit de l'espace
|
||||
What takes up the space in this file, by kind of content. Ce qui occupe l'espace dans ce fichier, par type de contenu.
|
||||
Bytes Octets
|
||||
Content Streams Flux de contenu
|
||||
X Object Forms Formulaires XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -554,6 +554,7 @@
|
||||
Remove unused objects and merge identical ones Nem használt objektumok eltávolítása és az azonosak egyesítése
|
||||
Audit space usage… Helyfoglalás vizsgálata…
|
||||
Space Audit Helyfoglalás
|
||||
What takes up the space in this file, by kind of content. Mi foglalja a helyet ebben a fájlban, tartalomtípusonként.
|
||||
Bytes Bájt
|
||||
Content Streams Tartalomfolyamok
|
||||
X Object Forms XObject-űrlapok
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
@@ -580,6 +580,7 @@
|
||||
Remove unused objects and merge identical ones Rimuovi oggetti inutilizzati e unisci quelli identici
|
||||
Audit space usage… Analizza uso dello spazio…
|
||||
Space Audit Analisi dello spazio
|
||||
What takes up the space in this file, by kind of content. Cosa occupa lo spazio in questo file, per tipo di contenuto.
|
||||
Bytes Byte
|
||||
Content Streams Flussi di contenuto
|
||||
X Object Forms Form XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -583,6 +583,7 @@
|
||||
Remove unused objects and merge identical ones 未使用オブジェクトを削除し同一オブジェクトを統合
|
||||
Audit space usage… 容量の使用状況を確認…
|
||||
Space Audit 容量の使用状況
|
||||
What takes up the space in this file, by kind of content. このファイルの容量を何が占めているか、コンテンツの種類別に示します。
|
||||
Bytes バイト数
|
||||
Content Streams コンテンツストリーム
|
||||
X Object Forms フォーム XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -543,6 +543,7 @@
|
||||
Remove unused objects and merge identical ones Пайдаланылмаған нысандарды алып тастау және ұқсастарын біріктіру
|
||||
Audit space usage… Кеңістікті пайдалануды тексеру…
|
||||
Space Audit Кеңістікті пайдалану аудиті
|
||||
What takes up the space in this file, by kind of content. Бұл файлдағы орынды не алып жатыр, мазмұн түрі бойынша.
|
||||
Bytes Байт
|
||||
Content Streams Мазмұн ағындары
|
||||
X Object Forms XObject формалары
|
||||
|
||||
|
@@ -1096,6 +1096,7 @@
|
||||
Sort items Rūšiuoti elementus
|
||||
Source Šaltinis
|
||||
Space Audit Vietos naudojimo analizė
|
||||
What takes up the space in this file, by kind of content. Kas užima vietą šiame faile, pagal turinio rūšį.
|
||||
Special Specialusis
|
||||
Split document Skaidyti dokumentą
|
||||
Split into files… Skaidyti į failus…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 401 and column 9.
|
@@ -578,6 +578,7 @@
|
||||
Remove unused objects and merge identical ones Usuń nieużywane obiekty i scal identyczne
|
||||
Audit space usage… Analiza wykorzystania miejsca…
|
||||
Space Audit Analiza miejsca
|
||||
What takes up the space in this file, by kind of content. Co zajmuje miejsce w tym pliku, według rodzaju zawartości.
|
||||
Bytes Bajty
|
||||
Content Streams Strumienie zawartości
|
||||
X Object Forms Obiekty Form XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -213,6 +213,7 @@
|
||||
Remove unused objects and merge identical ones Remover objetos não utilizados e mesclar os idênticos
|
||||
Audit space usage… Auditar uso de espaço…
|
||||
Space Audit Auditoria de espaço
|
||||
What takes up the space in this file, by kind of content. O que ocupa o espaço deste arquivo, por tipo de conteúdo.
|
||||
Bytes Bytes
|
||||
Content Streams Fluxos de conteúdo
|
||||
X Object Forms Formulários XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -576,6 +576,7 @@
|
||||
Remove unused objects and merge identical ones Удалить неиспользуемые объекты и объединить одинаковые
|
||||
Audit space usage… Анализ использования места…
|
||||
Space Audit Анализ места
|
||||
What takes up the space in this file, by kind of content. Что занимает место в этом файле, по типу содержимого.
|
||||
Bytes Байты
|
||||
Content Streams Потоки содержимого
|
||||
X Object Forms Формы X Object
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1705,6 +1705,7 @@
|
||||
Sort items Razvrsti elemente
|
||||
Source Vir
|
||||
Space Audit Pregled prostora
|
||||
What takes up the space in this file, by kind of content. Kaj zaseda prostor v tej datoteki, po vrsti vsebine.
|
||||
Special Posebno
|
||||
Split Razdeli
|
||||
Split document Razdeli dokument
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -560,6 +560,7 @@ comment menu Edit సవరించు
|
||||
Remove unused objects and merge identical ones ఉపయోగించని ఆబ్జెక్ట్లను తీసివేసి, ఒకేలా ఉన్నవాటిని విలీనం చేయి
|
||||
Audit space usage… స్థల వినియోగాన్ని పరిశీలించు…
|
||||
Space Audit స్థల వినియోగ పరిశీలన
|
||||
What takes up the space in this file, by kind of content. ఈ ఫైల్లో స్థలాన్ని ఏవి ఆక్రమిస్తున్నాయి, కంటెంట్ రకం వారీగా.
|
||||
Bytes బైట్లు
|
||||
Content Streams కంటెంట్ స్ట్రీమ్లు
|
||||
X Object Forms X ఆబ్జెక్ట్ ఫారాలు
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -541,6 +541,7 @@
|
||||
Remove unused objects and merge identical ones Kullanılmayan nesneleri kaldır ve aynı olanları birleştir
|
||||
Audit space usage… Alan kullanımını denetle…
|
||||
Space Audit Alan Denetimi
|
||||
What takes up the space in this file, by kind of content. Bu dosyadaki alanı neyin kapladığı, içerik türüne göre.
|
||||
Bytes Bayt
|
||||
Content Streams İçerik Akışları
|
||||
X Object Forms XObject Formları
|
||||
|
||||
|
@@ -581,6 +581,7 @@
|
||||
Remove unused objects and merge identical ones Видалити невикористані об’єкти й об’єднати однакові
|
||||
Audit space usage… Аналіз використання місця…
|
||||
Space Audit Аналіз місця
|
||||
What takes up the space in this file, by kind of content. Що займає місце в цьому файлі, за типом вмісту.
|
||||
Bytes Байти
|
||||
Content Streams Потоки вмісту
|
||||
X Object Forms Форми XObject
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -263,6 +263,7 @@
|
||||
Remove unused objects and merge identical ones 删除未使用的对象并合并相同的对象
|
||||
Audit space usage… 审计空间使用情况…
|
||||
Space Audit 空间审计
|
||||
What takes up the space in this file, by kind of content. 此文件的空间被哪些内容占用,按内容类型列出。
|
||||
Bytes 字节
|
||||
Content Streams 内容流
|
||||
X Object Forms XObject 表单
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -219,6 +219,7 @@
|
||||
Remove unused objects and merge identical ones 刪除未使用的物件併合並相同的物件
|
||||
Audit space usage… 審計空間使用情況…
|
||||
Space Audit 空間審計
|
||||
What takes up the space in this file, by kind of content. 此檔案的空間被哪些內容佔用,依內容類型列出。
|
||||
Bytes 位元組
|
||||
Content Streams 內容流
|
||||
X Object Forms XObject 表單
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -243,33 +243,52 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
(ok, cancel)
|
||||
}
|
||||
|
||||
/// Audit Space Usage: bytes and share of the file per kind of content.
|
||||
/// Audit Space Usage: how the file's bytes are spent now, by kind of content (not what
|
||||
/// optimizing would save).
|
||||
pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[pdfcraft_engine::optimize::SpaceUse], t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new(tl!("Space Audit")).font(crate::theme::semibold(18.0)));
|
||||
use egui::{RichText, TextWrapMode, WidgetText};
|
||||
ui.label(RichText::new(tl!("Space Audit")).font(crate::theme::semibold(18.0)));
|
||||
ui.add_space(4.0);
|
||||
ui.label(RichText::new(tl!("What takes up the space in this file, by kind of content.")).color(t.text_muted));
|
||||
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 [tl!("Description"), tl!("Bytes"), tl!("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) {
|
||||
// "Patterns" also names the redaction search patterns ("模式"); the audit's PDF
|
||||
// graphics objects need their own key.
|
||||
let category = if r.category == pdfcraft_engine::optimize::SpaceCategory::Patterns {
|
||||
tl!("Patterns (graphics objects)").to_string()
|
||||
} else {
|
||||
tl!(r.category.label()).to_string()
|
||||
};
|
||||
ui.label(category);
|
||||
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)));
|
||||
// Every cell is laid out first so each column can be exactly as wide as its widest cell. A
|
||||
// right-to-left layout in a grid's last column takes the rest of the window instead, which
|
||||
// drew the percentages at the dialog's far edge, away from their rows.
|
||||
let text = |s: RichText| WidgetText::from(s).into_galley(ui, Some(TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body);
|
||||
let header = |s: &str| text(RichText::new(s).color(t.text_muted));
|
||||
let mut table = vec![[header(tl!("Description")), header(tl!("Bytes")), header(tl!("Percentage"))]];
|
||||
for r in rows.iter().filter(|r| r.bytes > 0) {
|
||||
// "Patterns" also names the redaction search patterns ("模式"); the audit's PDF
|
||||
// graphics objects need their own key.
|
||||
let category = if r.category == pdfcraft_engine::optimize::SpaceCategory::Patterns {
|
||||
tl!("Patterns (graphics objects)")
|
||||
} else {
|
||||
tl!(r.category.label())
|
||||
};
|
||||
table.push([text(RichText::new(category)), text(RichText::new(r.bytes.to_string())), text(RichText::new(format!("{:.2}%", r.percent)))]);
|
||||
}
|
||||
let total: u64 = rows.iter().map(|r| r.bytes).sum();
|
||||
table.push([
|
||||
text(RichText::new(tl!("Total")).strong()),
|
||||
text(RichText::new(total.to_string()).strong()),
|
||||
text(RichText::new("100.00%").strong()),
|
||||
]);
|
||||
let gap = 24.0;
|
||||
let width = |column: usize| table.iter().filter_map(|row| row.get(column)).map(|g| g.size().x).fold(0.0, f32::max);
|
||||
// The description takes what the numbers leave, so the striped rows span the dialog.
|
||||
let numbers = width(1) + width(2) + 2.0 * gap;
|
||||
let widths = [width(0).max(ui.available_width() - numbers), width(1), width(2)];
|
||||
let height = table.iter().flatten().map(|g| g.size().y).fold(ui.spacing().interact_size.y, f32::max);
|
||||
egui::Grid::new("space-audit").num_columns(3).striped(true).spacing([gap, 4.0]).show(ui, |ui| {
|
||||
for row in table {
|
||||
for (column, (cell, width)) in row.into_iter().zip(widths).enumerate() {
|
||||
// Numbers line up on their last digit.
|
||||
let layout = if column == 0 { Layout::left_to_right(Align::Center) } else { Layout::right_to_left(Align::Center) };
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
|
||||
ui.new_child(egui::UiBuilder::new().max_rect(rect).layout(layout)).label(cell);
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(egui::RichText::new(tl!("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;
|
||||
|
||||
@@ -900,6 +900,22 @@ fn optimizer_audits_space_usage() {
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Optimize), "back to the optimizer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_audit_numbers_line_up_under_their_headers() {
|
||||
// The percentages were drawn at the dialog's far edge, away from their header and the
|
||||
// striped rows: a right-to-left layout in a grid's last column takes the rest of the window.
|
||||
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);
|
||||
let total = h.state().space_audit.iter().map(|r| r.bytes).sum::<u64>().to_string();
|
||||
let right = |label: &str| h.get_by_label(label).rect().right();
|
||||
assert!((right("Percentage") - right("100.00%")).abs() < 1.0, "{} vs {}", right("Percentage"), right("100.00%"));
|
||||
assert!((right("Bytes") - right(&total)).abs() < 1.0, "{} vs {}", right("Bytes"), right(&total));
|
||||
assert!(right("100.00%") < right("OK") + 1.0, "the table stays inside the dialog");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_document_keeps_its_own_scroll_position() {
|
||||
// #189: every document's page view shared one scroll position, so switching tabs showed a
|
||||
|
||||
@@ -7267,6 +7267,8 @@ commands = ["optimize.advanced"]
|
||||
tools = ["doc_optimize"]
|
||||
evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/src/optimize_ui.rs::the_progress_card_shows_the_stage_and_cancels", "crates/engine/src/tests.rs::an_optimize_job_reports_its_stages_and_can_be_cancelled", "crates/optimize/tests/optimize.rs::images_and_forms_nothing_draws_are_dropped", "crates/automation/tests/automation.rs::a_deleted_image_is_not_kept_by_reduce_or_optimize", "crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_picks_a_preset", "crates/automation/tests/automation.rs::a_page_printed_in_strips_gets_small_through_tools", "crates/optimize/tests/optimize.rs::the_mobile_preset_makes_a_printed_page_small"]
|
||||
notes = "Settings menu with the Standard and Mobile presets (Custom once an option changes; doc_optimize preset), Images (with Merge image fragments: a page picture drawn as one-pixel strips, as some PDF printers write it, is put back together so it can be resampled), Discard Objects, Discard User Data, Clean Up (also drops images and forms the pages list but never draw, such as deleted images); runs in the background with a progress bar (per image) and Cancel; no Fonts or Transparency panels, no saved custom presets, no space audit"
|
||||
evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/src/optimize_ui.rs::the_progress_card_shows_the_stage_and_cancels", "crates/engine/src/tests.rs::an_optimize_job_reports_its_stages_and_can_be_cancelled", "crates/optimize/tests/optimize.rs::images_and_forms_nothing_draws_are_dropped", "crates/automation/tests/automation.rs::a_deleted_image_is_not_kept_by_reduce_or_optimize"]
|
||||
notes = "Images, Discard Objects, Discard User Data, Clean Up (also drops images and forms the pages list but never draw, such as deleted images); runs in the background with a progress bar (per image) and Cancel; no Fonts or Transparency panels"
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.downsample-images"
|
||||
@@ -7603,8 +7605,8 @@ milestone = "M11"
|
||||
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"
|
||||
evidence = ["crates/optimize/tests/optimize.rs::space_audit_shares_the_file_out_by_kind", "crates/optimize/tests/optimize.rs::space_audit_counts_what_fields_and_comments_draw_with_by_kind", "crates/optimize/tests/optimize.rs::space_audit_survives_deeply_nested_forms", "crates/automation/tests/automation.rs::auditing_space_through_tools", "crates/ui-egui/tests/viewer.rs::optimizer_audits_space_usage", "crates/ui-egui/tests/viewer.rs::space_audit_numbers_line_up_under_their_headers"]
|
||||
notes = "PDF Optimizer ▸ Audit space usage: what the file's bytes are spent on now (not what optimizing would save), in 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. Resources are counted by kind wherever they are used, so the fonts and images form fields and comments draw with count as fonts and images. Still missing: sizes are each object as it would be written uncompressed, so objects packed in compressed object streams count larger than they take on disk (overhead, the rest of the file, shrinks to match)"
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.optimizer-presets"
|
||||
|
||||
Reference in New Issue
Block a user