mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 16:36:34 +00:00
Merge #4440: M11: PDF Optimizer can discard an embedded search index (optimize.discard-search-index)
# Conflicts: # crates/automation/src/tools.rs
This commit is contained in:
@@ -360,6 +360,7 @@ impl Automation {
|
||||
("discard_thumbnails", &mut settings.discard_thumbnails),
|
||||
("discard_alternate_images", &mut settings.discard_alternate_images),
|
||||
("discard_tags", &mut settings.discard_tags),
|
||||
("discard_search_index", &mut settings.discard_search_index),
|
||||
("discard_print_settings", &mut settings.discard_print_settings),
|
||||
("discard_form_actions", &mut settings.discard_form_actions),
|
||||
("flate_unencoded", &mut settings.flate_unencoded),
|
||||
@@ -472,6 +473,7 @@ impl Automation {
|
||||
"image_bytes_after": o.image_bytes_after,
|
||||
"thumbnails": o.thumbnails,
|
||||
"alternate_images": o.alternate_images,
|
||||
"search_indexes": o.search_indexes,
|
||||
"tags_removed": o.tags_removed,
|
||||
"print_settings": o.print_settings,
|
||||
"form_actions": o.form_actions,
|
||||
|
||||
@@ -1704,7 +1704,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
.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.")
|
||||
.with(schema(json!({ "doc": doc(), "revision": { "type": "integer", "minimum": 1 } }), &["doc", "revision"])),
|
||||
t("doc_optimize", "Optimize PDF", "Write an optimized copy to `path` (Acrobat's PDF Optimizer). preset: standard (default; Reduce File Size's choices) or mobile (images to 96 ppi above 144, JPEG high quality); the options below change it. compatibility: retain (default; keeps the document's version, or 1.5 when object streams are written), 1.4, 1.5, 1.6, 1.7 or 2.0. merge_fragments (default true) first puts a page picture drawn as many stacked image strips (as some PDF printers write it) back together into one image, so it can be resampled (merged_images, fragments_merged). color / gray: { downsample, ppi, above_ppi, compression: jpeg|zip|retain, quality 1–100 } (defaults: downsample to 150 ppi above 225, JPEG 60). mono: { downsample, ppi, above_ppi, compression: ccitt_g4|jbig2|retain } for black-and-white images and image masks (default: averaged down to 300 ppi above 450 ppi, counted in images_resampled; stored losslessly as CCITT Group 4 or JBIG2 (or 1-bit Flate, once resampled or packed from more bits) when that is smaller, up to 16 JBIG2 images sharing one symbol dictionary (/JBIG2Globals) where that is smaller still (image masks and DeviceGray images named directly, of at most 64 Mi pixels, which redaction can store again without it), Group 3 fax images, JBIG2 images, black-and-white RGB and indexed images using only palette entries 0 and 1 among them, counted in images_recompressed; retain resamples only, a CCITT image staying CCITT, a JBIG2 image JBIG2 and any other becoming Flate). Images past what one run may decode stay as they are, counted in images_refused; streams it only stores with Flate past that stay LZW, uncounted. Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings (entries removed: print_settings), form_actions (submit, import and reset actions removed: form_actions; the fields and links stay, and the actions chained after one still run); flate_unencoded (true); object_streams (default true; always off for 1.4); lzw_to_flate (true) decodes LZW streams and stores them with Flate instead, leaving any it cannot decode unchanged (lzw_streams counts the streams converted); remove_invalid_links and remove_unreferenced_dests (true); discard_external_refs takes GoToR, GoToE, Launch and ImportData actions, and Thread actions with /F, out of links and bookmarks, removes a link left with nothing to do, and keeps links within the document and web links (external_links, external_bookmarks); images and forms the pages list but never draw are always dropped (unused_xobjects). discard: Remove Hidden Information categories (metadata, attachments, comments, form-fields, hidden-text, hidden-layers, bookmarks, links-actions-scripts, private-data). Signed documents are refused. target_bytes (optional): if the copy is larger, retry with up to 5 progressively stronger image settings (downsample to at most 110, 96, 72, 54, 36 ppi, applied to colour and grey images; images set to JPEG also get JPEG quality at most 45, 35, 28, 20, 12, while zip and retain images keep their encoding and only get the lower ppi) and stop at the first copy that fits; reports target_reached, attempts and target_settings, and writes the smallest copy with target_reached false when none fits. Without it nothing changes. The open document is unchanged.")
|
||||
t("doc_optimize", "Optimize PDF", "Write an optimized copy to `path` (Acrobat's PDF Optimizer). preset: standard (default; Reduce File Size's choices) or mobile (images to 96 ppi above 144, JPEG high quality); the options below change it. compatibility: retain (default; keeps the document's version, or 1.5 when object streams are written), 1.4, 1.5, 1.6, 1.7 or 2.0. merge_fragments (default true) first puts a page picture drawn as many stacked image strips (as some PDF printers write it) back together into one image, so it can be resampled (merged_images, fragments_merged). color / gray: { downsample, ppi, above_ppi, compression: jpeg|zip|retain, quality 1–100 } (defaults: downsample to 150 ppi above 225, JPEG 60). mono: { downsample, ppi, above_ppi, compression: ccitt_g4|jbig2|retain } for black-and-white images and image masks (default: averaged down to 300 ppi above 450 ppi, counted in images_resampled; stored losslessly as CCITT Group 4 or JBIG2 (or 1-bit Flate, once resampled or packed from more bits) when that is smaller, up to 16 JBIG2 images sharing one symbol dictionary (/JBIG2Globals) where that is smaller still (image masks and DeviceGray images named directly, of at most 64 Mi pixels, which redaction can store again without it), Group 3 fax images, JBIG2 images, black-and-white RGB and indexed images using only palette entries 0 and 1 among them, counted in images_recompressed; retain resamples only, a CCITT image staying CCITT, a JBIG2 image JBIG2 and any other becoming Flate). Images past what one run may decode stay as they are, counted in images_refused; streams it only stores with Flate past that stay LZW, uncounted. Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, search_index (removes the catalog's /PieceInfo entries whose application name contains \"index\" in any letter case; search_indexes), print_settings (entries removed: print_settings), form_actions (submit, import and reset actions removed: form_actions; the fields and links stay, and the actions chained after one still run); flate_unencoded (true); object_streams (default true; always off for 1.4); lzw_to_flate (true) decodes LZW streams and stores them with Flate instead, leaving any it cannot decode unchanged (lzw_streams counts the streams converted); remove_invalid_links and remove_unreferenced_dests (true); discard_external_refs takes GoToR, GoToE, Launch and ImportData actions, and Thread actions with /F, out of links and bookmarks, removes a link left with nothing to do, and keeps links within the document and web links (external_links, external_bookmarks); images and forms the pages list but never draw are always dropped (unused_xobjects). discard: Remove Hidden Information categories (metadata, attachments, comments, form-fields, hidden-text, hidden-layers, bookmarks, links-actions-scripts, private-data). Signed documents are refused. target_bytes (optional): if the copy is larger, retry with up to 5 progressively stronger image settings (downsample to at most 110, 96, 72, 54, 36 ppi, applied to colour and grey images; images set to JPEG also get JPEG quality at most 45, 35, 28, 20, 12, while zip and retain images keep their encoding and only get the lower ppi) and stop at the first copy that fits; reports target_reached, attempts and target_settings, and writes the smallest copy with target_reached false when none fits. Without it nothing changes. The open document is unchanged.")
|
||||
.cmd("optimize.advanced")
|
||||
.with(schema(
|
||||
json!({
|
||||
@@ -1720,6 +1720,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"discard_thumbnails": { "type": "boolean" },
|
||||
"discard_alternate_images": { "type": "boolean" },
|
||||
"discard_tags": { "type": "boolean" },
|
||||
"discard_search_index": { "type": "boolean" },
|
||||
"discard_print_settings": { "type": "boolean" },
|
||||
"discard_form_actions": { "type": "boolean" },
|
||||
"flate_unencoded": { "type": "boolean" },
|
||||
|
||||
@@ -7700,6 +7700,36 @@ fn signing_with_translated_labels_through_tools() {
|
||||
assert!(stamped.contains("(Genehmigt)") && !stamped.contains("Reason") && !stamped.contains("Date"), "{stamped}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimizing_discards_an_embedded_search_index_through_tools() {
|
||||
use pdfcraft_cos::{Dict, Object};
|
||||
let dir = workdir("optimize-search-index");
|
||||
let mut cos = pdfcraft_cos::Document::open(std::sync::Arc::new(fixture(1))).unwrap();
|
||||
let mut info = Dict::new();
|
||||
for name in ["IBM-ODIndexes", "Illustrator"] {
|
||||
let mut data = Dict::new();
|
||||
data.set(b"Private".to_vec(), Object::Int(1));
|
||||
info.set(name.as_bytes().to_vec(), Object::Dict(data));
|
||||
}
|
||||
let root = cos.root().unwrap();
|
||||
cos.update_dict(root, |c| c.set(b"PieceInfo".to_vec(), Object::Dict(info))).unwrap();
|
||||
std::fs::write(dir.join("indexed.pdf"), pdfcraft_cos::write_full(&cos, &pdfcraft_cos::SaveOptions::default()).unwrap()).unwrap();
|
||||
let piece_info = |name: &str| {
|
||||
let d = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(dir.join(name)).unwrap())).unwrap();
|
||||
let cat = d.get(d.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let info = d.dict(cat.get(b"PieceInfo").unwrap()).unwrap();
|
||||
info.iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).collect::<Vec<_>>()
|
||||
};
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "indexed.pdf" }))["doc"].as_u64().unwrap();
|
||||
let kept = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "kept.pdf" }));
|
||||
assert_eq!(kept["search_indexes"], 0, "{kept}");
|
||||
assert_eq!(piece_info("kept.pdf"), ["IBM-ODIndexes", "Illustrator"]);
|
||||
let r = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "small.pdf", "discard_search_index": true }));
|
||||
assert_eq!(r["search_indexes"], 1, "{r}");
|
||||
assert_eq!(piece_info("small.pdf"), ["Illustrator"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimizing_through_tools() {
|
||||
let dir = workdir("optimize");
|
||||
|
||||
@@ -153,6 +153,13 @@ then this, then merges identical objects and writes a full, compressed save
|
||||
progress bar). JPEG decoding comes from `zune-jpeg` (the decoder behind `image` and the
|
||||
renderer), JPEG encoding and resampling from the `image` crate.
|
||||
|
||||
Discard embedded search index (`discard_search_index`, off by default) removes the entries of
|
||||
the catalog's `/PieceInfo` whose application name contains "index" in any letter case, such as
|
||||
`IBM-ODIndexes`, and removes `/PieceInfo` when no other entry is left. An indirect `/PieceInfo`
|
||||
is copied into the catalog without those entries, and the original object is not changed. The
|
||||
full save leaves out the objects that only the removed entries refer to. Pages keep their own
|
||||
`/PieceInfo`.
|
||||
|
||||
## Target PDF version: partial compatibility
|
||||
|
||||
The target selects the header version, clears the catalog `/Version`, controls object
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
//! - **Clean up:** drop images and forms that pages list but never draw (such as a deleted
|
||||
//! image); Flate-compress streams that have no filter; remove a redundant outer ASCII85 or
|
||||
//! ASCIIHex layer from streams that have one; store LZW-compressed streams with Flate instead.
|
||||
//! - **Discard embedded search index** (off by default): the search indexes stored in the
|
||||
//! catalog's `/PieceInfo` (see [`Settings::discard_search_index`]).
|
||||
//!
|
||||
//! Metadata, attachments, comments, scripts, private data, hidden layers, bookmarks and form
|
||||
//! fields are discarded by `pdfcraft-redact`'s Remove Hidden Information, which the engine
|
||||
@@ -204,6 +206,11 @@ pub struct Settings {
|
||||
pub discard_thumbnails: bool,
|
||||
pub discard_alternate_images: bool,
|
||||
pub discard_tags: bool,
|
||||
/// Remove the entries of the catalog's `/PieceInfo` (data an application keeps for the whole
|
||||
/// document, ISO 32000-2 §14.5) whose application name contains "index" in any letter case,
|
||||
/// and `/PieceInfo` itself when nothing else is left in it. Objects that only those entries
|
||||
/// refer to are left out of the saved file.
|
||||
pub discard_search_index: bool,
|
||||
pub discard_print_settings: bool,
|
||||
/// Remove form submission, import and reset actions; the fields and links that ran them stay.
|
||||
pub discard_form_actions: bool,
|
||||
@@ -246,6 +253,7 @@ impl Default for Settings {
|
||||
discard_thumbnails: true,
|
||||
discard_alternate_images: true,
|
||||
discard_tags: false,
|
||||
discard_search_index: false,
|
||||
discard_print_settings: false,
|
||||
discard_form_actions: false,
|
||||
flate_unencoded: true,
|
||||
@@ -312,6 +320,8 @@ pub struct Report {
|
||||
pub thumbnails: usize,
|
||||
pub alternate_images: usize,
|
||||
pub tags_removed: bool,
|
||||
/// Search index entries removed from the catalog's `/PieceInfo`.
|
||||
pub search_indexes: usize,
|
||||
pub print_settings: usize,
|
||||
/// Form submission, import and reset actions removed.
|
||||
pub form_actions: usize,
|
||||
@@ -424,6 +434,11 @@ pub fn optimize_with_progress(doc: &mut Document, settings: &Settings, progress:
|
||||
}
|
||||
report.tags_removed = true;
|
||||
}
|
||||
if settings.discard_search_index
|
||||
&& let Some(root) = root
|
||||
{
|
||||
report.search_indexes = discard_search_index(doc, root)?;
|
||||
}
|
||||
if settings.discard_print_settings
|
||||
&& let Some(root) = root
|
||||
{
|
||||
@@ -639,6 +654,48 @@ pub(crate) fn stored_len(s: &Stream) -> Option<usize> {
|
||||
out.len().checked_add(s.raw.len())
|
||||
}
|
||||
|
||||
/// Remove the search index entries of the catalog's `/PieceInfo` ([`Settings::discard_search_index`])
|
||||
/// and return how many there were. An indirect `/PieceInfo` is not edited: the catalog gets an
|
||||
/// indirect copy without those entries, so other objects that refer to it keep theirs. The objects
|
||||
/// the removed entries held are dropped by the full save, which writes only what the trailer
|
||||
/// reaches.
|
||||
fn discard_search_index(doc: &mut Document, root: ObjRef) -> Result<usize, OptimizeError> {
|
||||
let Some(info) = doc.get(root).as_dict().and_then(|c| c.get(b"PieceInfo").cloned()) else {
|
||||
return Ok(0);
|
||||
};
|
||||
let indirect = matches!(info, Object::Ref(_));
|
||||
let (info, removed) = match &*doc.resolve(&info) {
|
||||
Object::Dict(d) => {
|
||||
let kept: pdfcraft_cos::Dict = d.iter().filter(|(k, _)| !is_search_index(k)).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
let removed = d.len().saturating_sub(kept.len());
|
||||
(kept, removed)
|
||||
}
|
||||
_ => return Ok(0),
|
||||
};
|
||||
if removed == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let replacement = if info.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let copy = Object::Dict(info);
|
||||
Some(if indirect { Object::Ref(doc.add(copy)) } else { copy })
|
||||
};
|
||||
doc.update_dict(root, |c| match replacement {
|
||||
Some(info) => c.set(b"PieceInfo".to_vec(), info),
|
||||
None => {
|
||||
c.remove(b"PieceInfo");
|
||||
}
|
||||
})?;
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// Whether a `/PieceInfo` application name names a search index: it contains "index" in any
|
||||
/// letter case (such as `IBM-ODIndexes`).
|
||||
fn is_search_index(name: &[u8]) -> bool {
|
||||
name.windows(5).any(|w| w.eq_ignore_ascii_case(b"index"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4490,6 +4490,135 @@ fn named_dest_keys(doc: &Document) -> Vec<Vec<u8>> {
|
||||
arr.chunks(2).filter_map(|c| doc.resolve(&c[0]).as_string().map(|s| s.bytes.clone())).collect()
|
||||
}
|
||||
|
||||
/// A `/PieceInfo` data dictionary holding `private`.
|
||||
fn app_data(private: Object) -> Object {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"LastModified".to_vec(), Object::String(pdfcraft_cos::PdfString::literal(b"D:20260101000000Z".to_vec())));
|
||||
d.set(b"Private".to_vec(), private);
|
||||
Object::Dict(d)
|
||||
}
|
||||
|
||||
fn marked(doc: &mut Document, marker: &str, extra: &[(&str, Object)]) -> ObjRef {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Marker".to_vec(), Object::String(pdfcraft_cos::PdfString::literal(marker.as_bytes().to_vec())));
|
||||
for (k, v) in extra {
|
||||
d.set(k.as_bytes().to_vec(), v.clone());
|
||||
}
|
||||
doc.add(Object::Dict(d))
|
||||
}
|
||||
|
||||
fn saved(doc: &Document) -> Vec<u8> {
|
||||
write_full(doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap()
|
||||
}
|
||||
|
||||
fn contains(bytes: &[u8], text: &str) -> bool {
|
||||
bytes.windows(text.len()).any(|w| w == text.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_indexes_in_the_catalog_go_and_other_application_data_stays() {
|
||||
let mut doc = Document::new_empty();
|
||||
let p = page(&mut doc, &[], "BT ET");
|
||||
// The first index holds two objects that refer to each other, and one the page also uses.
|
||||
let shared = marked(&mut doc, "SHARED-DATA", &[]);
|
||||
let a = marked(&mut doc, "INDEX-A", &[("Shared", Object::Ref(shared))]);
|
||||
let b = marked(&mut doc, "INDEX-B", &[("Next", Object::Ref(a))]);
|
||||
doc.update_dict(a, |d| d.set(b"Next".to_vec(), Object::Ref(b))).unwrap();
|
||||
let c = marked(&mut doc, "INDEX-C", &[]);
|
||||
let keep = marked(&mut doc, "KEEP-APP", &[]);
|
||||
let mut info = Dict::new();
|
||||
info.set(b"IBM-ODIndexes".to_vec(), app_data(Object::Ref(a)));
|
||||
info.set(b"MySearchINDEX".to_vec(), app_data(Object::Ref(c)));
|
||||
info.set(b"Illustrator".to_vec(), app_data(Object::Ref(keep)));
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |cat| cat.set(b"PieceInfo".to_vec(), Object::Dict(info))).unwrap();
|
||||
let mut page_info = Dict::new();
|
||||
page_info.set(b"PageIndexer".to_vec(), app_data(Object::Ref(shared)));
|
||||
doc.update_dict(p, |d| d.set(b"PieceInfo".to_vec(), Object::Dict(page_info))).unwrap();
|
||||
|
||||
// Off by default: nothing changes.
|
||||
let mut untouched = doc.clone();
|
||||
assert!(!Settings::default().discard_search_index);
|
||||
assert_eq!(optimize(&mut untouched, &Settings::default()).unwrap().search_indexes, 0);
|
||||
let bytes = saved(&untouched);
|
||||
assert!(["INDEX-A", "INDEX-B", "INDEX-C", "KEEP-APP"].iter().all(|m| contains(&bytes, m)));
|
||||
|
||||
let r = optimize(&mut doc, &Settings { discard_search_index: true, ..Settings::default() }).unwrap();
|
||||
assert_eq!(r.search_indexes, 2);
|
||||
let cat = doc.get(root).as_dict().cloned().unwrap();
|
||||
let info = cat.get(b"PieceInfo").and_then(Object::as_dict).cloned().unwrap();
|
||||
assert_eq!(info.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>(), vec![b"Illustrator".to_vec()]);
|
||||
// The saved file leaves out the objects only the indexes held, the cycle included; the
|
||||
// object the page also uses, the other application's data and the page's own entry stay.
|
||||
let bytes = saved(&doc);
|
||||
assert!(!["INDEX-A", "INDEX-B", "INDEX-C"].iter().any(|m| contains(&bytes, m)));
|
||||
assert!(["SHARED-DATA", "KEEP-APP", "PageIndexer"].iter().all(|m| contains(&bytes, m)));
|
||||
let reopened = Document::open(Arc::new(bytes)).unwrap();
|
||||
let cat = reopened.get(reopened.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let info = reopened.dict(cat.get(b"PieceInfo").unwrap()).unwrap();
|
||||
assert!(info.contains(b"Illustrator") && info.len() == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_indirect_piece_info_is_left_for_its_other_users() {
|
||||
let mut doc = Document::new_empty();
|
||||
let p = page(&mut doc, &[], "BT ET");
|
||||
let idx = marked(&mut doc, "INDEX-DATA", &[]);
|
||||
let mut info = Dict::new();
|
||||
info.set(b"Index".to_vec(), app_data(Object::Ref(idx)));
|
||||
let info = doc.add(Object::Dict(info));
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |cat| cat.set(b"PieceInfo".to_vec(), Object::Ref(info))).unwrap();
|
||||
doc.update_dict(p, |d| d.set(b"PieceInfo".to_vec(), Object::Ref(info))).unwrap();
|
||||
let settings = Settings { discard_search_index: true, ..Settings::default() };
|
||||
let r = optimize(&mut doc, &settings).unwrap();
|
||||
// The catalog's only entry was an index, so /PieceInfo goes; the page keeps the shared one.
|
||||
assert_eq!(r.search_indexes, 1);
|
||||
assert!(!doc.get(root).as_dict().unwrap().contains(b"PieceInfo"));
|
||||
assert!(doc.get(info).as_dict().unwrap().contains(b"Index"));
|
||||
assert!(contains(&saved(&doc), "INDEX-DATA"));
|
||||
|
||||
// A /PieceInfo that is not a dictionary is left as it is.
|
||||
let mut doc = Document::new_empty();
|
||||
page(&mut doc, &[], "BT ET");
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |cat| cat.set(b"PieceInfo".to_vec(), Object::Int(7))).unwrap();
|
||||
assert_eq!(optimize(&mut doc, &settings).unwrap().search_indexes, 0);
|
||||
assert_eq!(doc.get(root).as_dict().unwrap().int(b"PieceInfo"), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_deep_application_data_reads_back_after_discarding_an_index() {
|
||||
let mut doc = Document::new_empty();
|
||||
page(&mut doc, &[], "BT ET");
|
||||
let deep = (0..127).fold(Object::Int(7), |child, _| {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Child".to_vec(), child);
|
||||
Object::Dict(d)
|
||||
});
|
||||
let mut info = Dict::new();
|
||||
info.set(b"Index".to_vec(), app_data(Object::Int(0)));
|
||||
info.set(b"OtherApp".to_vec(), deep);
|
||||
let info = doc.add(Object::Dict(info));
|
||||
let root = doc.root().unwrap();
|
||||
doc.update_dict(root, |c| c.set(b"PieceInfo".to_vec(), Object::Ref(info))).unwrap();
|
||||
let retained = |doc: &Document| {
|
||||
let catalog = doc.get(doc.root().unwrap());
|
||||
let info = doc.resolve(catalog.as_dict().unwrap().get(b"PieceInfo").unwrap());
|
||||
let mut value = info.as_dict().unwrap().get(b"OtherApp").unwrap().clone();
|
||||
for _ in 0..127 {
|
||||
value = doc.resolve(&value).as_dict().unwrap().get(b"Child").unwrap().clone();
|
||||
}
|
||||
value
|
||||
};
|
||||
let mut doc = Document::open(Arc::new(saved(&doc))).unwrap();
|
||||
assert_eq!(retained(&doc), Object::Int(7));
|
||||
let report = optimize(&mut doc, &Settings { discard_search_index: true, ..Settings::default() }).unwrap();
|
||||
assert_eq!(report.search_indexes, 1);
|
||||
let reopened = Document::open(Arc::new(saved(&doc))).unwrap();
|
||||
assert_eq!(retained(&reopened), Object::Int(7));
|
||||
}
|
||||
|
||||
/// Page content long enough for LZW codes to grow past 9 bits, where `EarlyChange` matters.
|
||||
fn lzw_ops() -> Vec<u8> {
|
||||
(0..300).flat_map(|i| format!("{} {} m {} {} l S\n", i % 97, i * 7 % 389, i * 13 % 612, i * 31 % 792).into_bytes()).collect()
|
||||
|
||||
@@ -250,6 +250,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
ui.add(widgets::checkbox(&mut s.discard_alternate_images, tl!("Discard alternate images")));
|
||||
ui.add(widgets::checkbox(&mut s.discard_thumbnails, tl!("Discard embedded page thumbnails")));
|
||||
ui.add(widgets::checkbox(&mut s.discard_tags, tl!("Discard document tags")));
|
||||
ui.add(widgets::checkbox(&mut s.discard_search_index, tl!("Discard embedded search index")));
|
||||
ui.add(widgets::checkbox(&mut s.discard_print_settings, tl!("Discard embedded print settings")));
|
||||
ui.add(widgets::checkbox(&mut s.discard_form_actions, tl!("Discard all form submission, import and reset actions")));
|
||||
discard_box(ui, &mut d.discard, Hidden::Bookmarks, "Discard bookmarks");
|
||||
|
||||
@@ -7642,11 +7642,11 @@ title = "Discard embedded search index"
|
||||
area = "K"
|
||||
tier = "P2"
|
||||
milestone = "M11"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["optimize.advanced"]
|
||||
tools = ["doc_optimize"]
|
||||
evidence = ["crates/optimize/tests/optimize.rs::search_indexes_in_the_catalog_go_and_other_application_data_stays", "crates/optimize/tests/optimize.rs::an_indirect_piece_info_is_left_for_its_other_users", "crates/automation/tests/it/automation.rs::optimizing_discards_an_embedded_search_index_through_tools"]
|
||||
notes = "PDF Optimizer ▸ Discard Objects, off by default. The public help page does not say where the index is stored; PdfCraft's rule: an entry of the catalog's /PieceInfo whose application name contains \"index\" in any letter case (such as IBM-ODIndexes) is a search index and is removed, with /PieceInfo itself when nothing else is left. An indirect /PieceInfo is copied into the catalog without those entries and the original object is left for its other users. The full save leaves out objects only the removed entries reach, cycles included; objects something else also uses stay. Page /PieceInfo is not changed. An index stored under an application name that does not contain \"index\" is kept."
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.discard-print-settings"
|
||||
|
||||
Reference in New Issue
Block a user