mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M11: target PDF version and object streams option in PDF Optimizer (#3090)
This commit is contained in:
@@ -1059,7 +1059,7 @@ impl Automation {
|
||||
}
|
||||
|
||||
fn doc_optimize(&mut self, a: &Args) -> Result<Value> {
|
||||
use pdfcraft_engine::optimize::{Compression, ImageSettings, Preset};
|
||||
use pdfcraft_engine::optimize::{Compatibility, Compression, ImageSettings, Preset};
|
||||
let id = self.doc(a)?.id;
|
||||
let before = self.doc(a)?.bytes.len();
|
||||
let path = self.resolve(a.str("path")?, true)?;
|
||||
@@ -1067,6 +1067,10 @@ impl Automation {
|
||||
None => Preset::Standard.settings(),
|
||||
Some(p) => Preset::from_name(p).ok_or_else(|| ToolError::InvalidArgs(format!("unknown preset {p:?} (standard, mobile)")))?.settings(),
|
||||
};
|
||||
if let Some(c) = a.opt_str("compatibility")? {
|
||||
settings.compatibility = Compatibility::from_name(c)
|
||||
.ok_or_else(|| ToolError::InvalidArgs(format!("unknown compatibility {c:?} (retain, 1.4, 1.5, 1.6, 1.7, 2.0)")))?;
|
||||
}
|
||||
let images = |key: &str, base: ImageSettings| -> Result<ImageSettings> {
|
||||
let Some(v) = a.get(key) else { return Ok(base) };
|
||||
let mut s = base;
|
||||
@@ -1100,6 +1104,7 @@ impl Automation {
|
||||
("discard_tags", &mut settings.discard_tags),
|
||||
("discard_print_settings", &mut settings.discard_print_settings),
|
||||
("flate_unencoded", &mut settings.flate_unencoded),
|
||||
("object_streams", &mut settings.object_streams),
|
||||
("remove_invalid_links", &mut settings.remove_invalid_links),
|
||||
("remove_unreferenced_dests", &mut settings.remove_unreferenced_dests),
|
||||
] {
|
||||
@@ -1122,9 +1127,12 @@ impl Automation {
|
||||
};
|
||||
let (bytes, r) = self.session.optimized_bytes(id, &settings, &discard).map_err(failed)?;
|
||||
write_atomic(&path, &bytes)?;
|
||||
let pdf_version = std::str::from_utf8(bytes.get(5..8).unwrap_or_default()).unwrap_or_default();
|
||||
let o = &r.optimize;
|
||||
Ok(json!({
|
||||
"path": path.to_string_lossy(),
|
||||
"pdf_version": pdf_version,
|
||||
"object_streams": settings.effective_object_streams(),
|
||||
"bytes_before": before,
|
||||
"bytes_after": bytes.len(),
|
||||
"merged_images": o.merged_images,
|
||||
|
||||
@@ -1303,13 +1303,14 @@ 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. 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). Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings; flate_unencoded (true); remove_invalid_links and remove_unreferenced_dests (true); 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. 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). Images are measured where pages draw them and replaced only when smaller. discard_*: thumbnails (default true), alternate_images (true), tags, print_settings; flate_unencoded (true); object_streams (default true; always off for 1.4); remove_invalid_links and remove_unreferenced_dests (true); 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. The open document is unchanged.")
|
||||
.cmd("optimize.advanced")
|
||||
.with(schema(
|
||||
json!({
|
||||
"doc": doc(),
|
||||
"path": { "type": "string" },
|
||||
"preset": { "type": "string", "enum": ["standard", "mobile"] },
|
||||
"compatibility": { "type": "string", "enum": ["retain", "1.4", "1.5", "1.6", "1.7", "2.0"] },
|
||||
"merge_fragments": { "type": "boolean" },
|
||||
"color": { "type": "object" },
|
||||
"gray": { "type": "object" },
|
||||
@@ -1318,6 +1319,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
"discard_tags": { "type": "boolean" },
|
||||
"discard_print_settings": { "type": "boolean" },
|
||||
"flate_unencoded": { "type": "boolean" },
|
||||
"object_streams": { "type": "boolean" },
|
||||
"remove_invalid_links": { "type": "boolean" },
|
||||
"remove_unreferenced_dests": { "type": "boolean" },
|
||||
"discard": { "type": "array", "items": { "type": "string" } },
|
||||
|
||||
@@ -4626,6 +4626,18 @@ fn optimizing_through_tools() {
|
||||
assert!(reduced["bytes_after"].as_u64().unwrap() < reduced["bytes_before"].as_u64().unwrap());
|
||||
assert_eq!(reduced["written"], true);
|
||||
assert_eq!(std::fs::metadata(dir.join("reduced.pdf")).unwrap().len(), reduced["bytes_after"].as_u64().unwrap());
|
||||
|
||||
// Target PDF 1.4 (disables object streams) and PDF 2.0 (with object_streams toggled off).
|
||||
let v14 = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "v14.pdf", "compatibility": "1.4" }));
|
||||
assert_eq!((v14["pdf_version"].as_str(), v14["object_streams"].as_bool()), (Some("1.4"), Some(false)), "{v14}");
|
||||
let v14_bytes = std::fs::read(dir.join("v14.pdf")).unwrap();
|
||||
assert!(v14_bytes.starts_with(b"%PDF-1.4\n") && !v14_bytes.windows(7).any(|w| w == b"/ObjStm"));
|
||||
|
||||
let v20 = ok(&mut a, "doc_optimize", json!({ "doc": doc, "path": "v20.pdf", "compatibility": "2.0", "object_streams": false }));
|
||||
assert_eq!((v20["pdf_version"].as_str(), v20["object_streams"].as_bool()), (Some("2.0"), Some(false)), "{v20}");
|
||||
assert!(std::fs::read(dir.join("v20.pdf")).unwrap().starts_with(b"%PDF-2.0\n"));
|
||||
|
||||
assert!(matches!(a.call("doc_optimize", &json!({ "doc": doc, "path": "x.pdf", "compatibility": "1.3" })), Err(ToolError::InvalidArgs(_))));
|
||||
assert!(matches!(
|
||||
a.call("doc_optimize", &json!({ "doc": doc, "path": "x.pdf", "color": { "compression": "gif" } })),
|
||||
Err(ToolError::InvalidArgs(_))
|
||||
|
||||
@@ -805,6 +805,11 @@ impl Document {
|
||||
&self.version
|
||||
}
|
||||
|
||||
/// Set the PDF header version (`"1.4"`, `"1.7"`, `"2.0"`, …) written by a full save.
|
||||
pub fn set_version(&mut self, version: impl Into<String>) {
|
||||
self.version = version.into();
|
||||
}
|
||||
|
||||
pub fn trailer(&self) -> &Dict {
|
||||
&self.trailer
|
||||
}
|
||||
|
||||
@@ -68,7 +68,11 @@ impl Session {
|
||||
cos: editor.cos.clone(),
|
||||
settings: settings.clone(),
|
||||
discard: discard.to_vec(),
|
||||
opts: SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() },
|
||||
opts: SaveOptions {
|
||||
mod_date: self.now().map(pdfcraft_cos::pdf_date),
|
||||
object_streams: settings.effective_object_streams(),
|
||||
..SaveOptions::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,58 @@ pub use images::effective_resolutions;
|
||||
pub enum OptimizeError {
|
||||
#[error("the document has no page tree")]
|
||||
NoPages,
|
||||
#[error("the document's encryption requires PDF {required} or later (chosen: PDF {chosen})")]
|
||||
IncompatibleEncryption { required: &'static str, chosen: &'static str },
|
||||
#[error(transparent)]
|
||||
Cos(#[from] pdfcraft_cos::CosError),
|
||||
#[error("the optimization was cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// The PDF version an optimized copy is made compatible with (Acrobat: Make compatible with).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Compatibility {
|
||||
/// Keep the document's version (upgraded to 1.5 when object streams are written).
|
||||
#[default]
|
||||
Retain,
|
||||
Pdf1_4,
|
||||
Pdf1_5,
|
||||
Pdf1_6,
|
||||
Pdf1_7,
|
||||
Pdf2_0,
|
||||
}
|
||||
|
||||
impl Compatibility {
|
||||
pub const ALL: [Compatibility; 6] =
|
||||
[Compatibility::Retain, Compatibility::Pdf1_4, Compatibility::Pdf1_5, Compatibility::Pdf1_6, Compatibility::Pdf1_7, Compatibility::Pdf2_0];
|
||||
|
||||
/// The name tools use (`"retain"`, `"1.4"`, `"1.5"`, `"1.6"`, `"1.7"`, `"2.0"`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Compatibility::Retain => "retain",
|
||||
Compatibility::Pdf1_4 => "1.4",
|
||||
Compatibility::Pdf1_5 => "1.5",
|
||||
Compatibility::Pdf1_6 => "1.6",
|
||||
Compatibility::Pdf1_7 => "1.7",
|
||||
Compatibility::Pdf2_0 => "2.0",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<Compatibility> {
|
||||
let trimmed = name.trim();
|
||||
let stripped = if trimmed.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("pdf ")) { &trimmed[4..] } else { trimmed };
|
||||
Compatibility::ALL.into_iter().find(|c| c.name().eq_ignore_ascii_case(stripped))
|
||||
}
|
||||
|
||||
/// The three-character `%PDF-` header version, when a target version is chosen.
|
||||
pub fn version(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Compatibility::Retain => None,
|
||||
_ => Some(self.name()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an optimization is, reported to [`optimize_with_progress`]'s callback.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
@@ -89,6 +135,8 @@ impl ImageSettings {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Settings {
|
||||
/// Target PDF version (`Retain` by default).
|
||||
pub compatibility: Compatibility,
|
||||
/// Put page pictures drawn as many stacked fragments back together first ([`fragments`]).
|
||||
pub merge_fragments: bool,
|
||||
pub color: ImageSettings,
|
||||
@@ -100,16 +148,28 @@ pub struct Settings {
|
||||
/// Flate-compress streams stored without a filter, and strip a redundant outer ASCII85 or
|
||||
/// ASCIIHex layer from streams that have one.
|
||||
pub flate_unencoded: bool,
|
||||
/// Clean Up: pack non-stream objects into compressed object streams with a cross-reference
|
||||
/// stream (PDF 1.5+, §7.5.7–7.5.8). Always off when `compatibility` is [`Compatibility::Pdf1_4`].
|
||||
pub object_streams: bool,
|
||||
/// Clean Up: remove links (and bookmark destinations) that point nowhere.
|
||||
pub remove_invalid_links: bool,
|
||||
/// Clean Up: remove named destinations nothing refers to.
|
||||
pub remove_unreferenced_dests: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Whether `write_full` should pack non-stream objects into compressed object streams:
|
||||
/// enabled when `object_streams` is set and the target version is not PDF 1.4 (§7.5.7).
|
||||
pub fn effective_object_streams(&self) -> bool {
|
||||
self.object_streams && self.compatibility != Compatibility::Pdf1_4
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
/// Acrobat's Reduce File Size choices.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
compatibility: Compatibility::Retain,
|
||||
merge_fragments: true,
|
||||
color: ImageSettings::REDUCE,
|
||||
gray: ImageSettings::REDUCE,
|
||||
@@ -118,6 +178,7 @@ impl Default for Settings {
|
||||
discard_tags: false,
|
||||
discard_print_settings: false,
|
||||
flate_unencoded: true,
|
||||
object_streams: true,
|
||||
remove_invalid_links: true,
|
||||
remove_unreferenced_dests: true,
|
||||
}
|
||||
@@ -186,6 +247,20 @@ pub struct Report {
|
||||
pub unused_xobjects: usize,
|
||||
}
|
||||
|
||||
/// The minimum PDF version required by `doc`'s active security handler, when it uses features
|
||||
/// introduced after PDF 1.4 (crypt filters in 1.5, AES-128 in 1.6, AES-256 in 1.7).
|
||||
fn required_encryption_version(doc: &Document) -> Option<&'static str> {
|
||||
let h = doc.output_handler()?;
|
||||
let d = h.dict();
|
||||
if d.v >= 5 || d.crypt_filters.iter().any(|(_, m)| *m == pdfcraft_cos::CryptMethod::Aes256) {
|
||||
Some("1.7")
|
||||
} else if d.v == 4 {
|
||||
if d.crypt_filters.iter().any(|(_, m)| *m == pdfcraft_cos::CryptMethod::Aes128) { Some("1.6") } else { Some("1.5") }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimize `doc` in place.
|
||||
pub fn optimize(doc: &mut Document, settings: &Settings) -> Result<Report, OptimizeError> {
|
||||
optimize_with_progress(doc, settings, &mut |_| true)
|
||||
@@ -195,6 +270,12 @@ pub fn optimize(doc: &mut Document, settings: &Settings) -> Result<Report, Optim
|
||||
/// during a long one). Returning `false` stops with [`OptimizeError::Cancelled`]; `doc` is then
|
||||
/// partly optimized and should be dropped.
|
||||
pub fn optimize_with_progress(doc: &mut Document, settings: &Settings, progress: &mut dyn FnMut(Stage) -> bool) -> Result<Report, OptimizeError> {
|
||||
if let Some(chosen) = settings.compatibility.version()
|
||||
&& let Some(required) = required_encryption_version(doc)
|
||||
&& chosen < required
|
||||
{
|
||||
return Err(OptimizeError::IncompatibleEncryption { required, chosen });
|
||||
}
|
||||
let mut report = Report::default();
|
||||
let pages = pdfcraft_annot::page_refs(doc).map_err(|_| OptimizeError::NoPages)?;
|
||||
if settings.merge_fragments {
|
||||
@@ -230,6 +311,16 @@ pub fn optimize_with_progress(doc: &mut Document, settings: &Settings, progress:
|
||||
}
|
||||
}
|
||||
let root = doc.root();
|
||||
if let Some(chosen) = settings.compatibility.version() {
|
||||
doc.set_version(chosen);
|
||||
if let Some(root) = root
|
||||
&& doc.get(root).as_dict().is_some_and(|c| c.contains(b"Version"))
|
||||
{
|
||||
doc.update_dict(root, |c| {
|
||||
c.remove(b"Version");
|
||||
})?;
|
||||
}
|
||||
}
|
||||
if settings.discard_tags
|
||||
&& let Some(root) = root
|
||||
&& doc.get(root).as_dict().is_some_and(|c| c.contains(b"StructTreeRoot"))
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full};
|
||||
use pdfcraft_optimize::{Compression, ImageSettings, Settings, SpaceCategory, audit_space, effective_resolutions, optimize};
|
||||
use pdfcraft_optimize::{
|
||||
Compatibility, Compression, ImageSettings, OptimizeError, 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).
|
||||
@@ -1022,3 +1024,74 @@ fn images_are_not_resampled_when_some_uses_could_not_be_measured() {
|
||||
let refs = pdfcraft_annot::page_refs(&doc).unwrap();
|
||||
assert_eq!(effective_resolutions(&doc, &refs)[&img], f64::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_level_sets_version_unpacks_for_1_4_and_checks_encryption() {
|
||||
let mut base = Document::new_empty();
|
||||
page(&mut base, &[], "BT /F1 12 Tf 72 720 Td (Compatibility test) Tj ET");
|
||||
let root = base.root().unwrap();
|
||||
base.update_dict(root, |c| c.set(b"Version".to_vec(), Object::name("1.6"))).unwrap();
|
||||
|
||||
for (compat, expected) in [
|
||||
(Compatibility::Pdf1_4, "1.4"),
|
||||
(Compatibility::Pdf1_5, "1.5"),
|
||||
(Compatibility::Pdf1_6, "1.6"),
|
||||
(Compatibility::Pdf1_7, "1.7"),
|
||||
(Compatibility::Pdf2_0, "2.0"),
|
||||
] {
|
||||
let mut doc = base.clone();
|
||||
let settings = Settings { compatibility: compat, ..Settings::default() };
|
||||
optimize(&mut doc, &settings).unwrap();
|
||||
assert_eq!(doc.version(), expected);
|
||||
assert!(!doc.get(root).as_dict().unwrap().contains(b"Version"), "overriding catalog /Version is cleared");
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: settings.effective_object_streams(), ..SaveOptions::default() }).unwrap();
|
||||
assert!(bytes.starts_with(format!("%PDF-{expected}\n").as_bytes()), "{expected}");
|
||||
let has_objstm = bytes.windows(7).any(|w| w == b"/ObjStm");
|
||||
assert_eq!(has_objstm, compat != Compatibility::Pdf1_4, "{compat:?}");
|
||||
let info = pdfcraft_render::inspect(Arc::new(bytes), None).unwrap();
|
||||
assert_eq!(info.pdf_version, expected);
|
||||
}
|
||||
|
||||
// Disabling object_streams on PDF 1.7 writes a classic cross-reference table.
|
||||
let mut doc = base.clone();
|
||||
let settings = Settings { compatibility: Compatibility::Pdf1_7, object_streams: false, ..Settings::default() };
|
||||
optimize(&mut doc, &settings).unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: settings.effective_object_streams(), ..SaveOptions::default() }).unwrap();
|
||||
assert!(bytes.starts_with(b"%PDF-1.7\n") && !bytes.windows(7).any(|w| w == b"/ObjStm"));
|
||||
|
||||
// Encryption that requires a newer PDF version than the chosen compatibility is refused.
|
||||
let mut aes256 = base.clone();
|
||||
aes256
|
||||
.set_encryption(&pdfcraft_cos::NewEncryption {
|
||||
algorithm: pdfcraft_cos::Algorithm::Aes256,
|
||||
user_password: "u",
|
||||
owner_password: "o",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [1; 32],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
optimize(&mut aes256, &Settings { compatibility: Compatibility::Pdf1_6, ..Settings::default() }),
|
||||
Err(OptimizeError::IncompatibleEncryption { required: "1.7", chosen: "1.6" })
|
||||
));
|
||||
optimize(&mut aes256, &Settings { compatibility: Compatibility::Pdf2_0, ..Settings::default() }).unwrap();
|
||||
assert_eq!(aes256.version(), "2.0");
|
||||
|
||||
let mut aes128 = base;
|
||||
aes128
|
||||
.set_encryption(&pdfcraft_cos::NewEncryption {
|
||||
algorithm: pdfcraft_cos::Algorithm::Aes128,
|
||||
user_password: "u",
|
||||
owner_password: "o",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [1; 32],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
optimize(&mut aes128, &Settings { compatibility: Compatibility::Pdf1_5, ..Settings::default() }),
|
||||
Err(OptimizeError::IncompatibleEncryption { required: "1.6", chosen: "1.5" })
|
||||
));
|
||||
optimize(&mut aes128, &Settings { compatibility: Compatibility::Pdf1_6, ..Settings::default() }).unwrap();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use egui::{Align, Layout};
|
||||
use pdfcraft_engine::optimize::{Compression, ImageSettings, Preset, QUALITIES, Settings};
|
||||
use pdfcraft_engine::optimize::{Compatibility, Compression, ImageSettings, Preset, QUALITIES, Settings};
|
||||
use pdfcraft_engine::optimizer::{OptimizeStage, Optimized};
|
||||
use pdfcraft_engine::{DocId, EditError, Hidden, OptimizeReport};
|
||||
|
||||
@@ -172,6 +172,17 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(tl!("Compatibility"));
|
||||
let compat_label = |c: Compatibility| match c {
|
||||
Compatibility::Retain => tl!("Retain existing").to_string(),
|
||||
_ => format!("PDF {}", c.name()),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("optimize-compatibility").selected_text(compat_label(d.settings.compatibility)).show_ui(ui, |ui| {
|
||||
for c in Compatibility::ALL {
|
||||
ui.selectable_value(&mut d.settings.compatibility, c, compat_label(c));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
@@ -221,7 +232,16 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
ui.add(widgets::checkbox(&mut s.flate_unencoded, tl!("Use Flate to encode streams that are not encoded")));
|
||||
ui.add(widgets::checkbox(&mut s.remove_invalid_links, tl!("Remove invalid links and bookmarks")));
|
||||
ui.add(widgets::checkbox(&mut s.remove_unreferenced_dests, tl!("Remove unreferenced named destinations")));
|
||||
ui.add_enabled(false, widgets::checkbox(&mut true, tl!("Compress document structure (object streams)")));
|
||||
let mut object_streams = s.effective_object_streams();
|
||||
if ui
|
||||
.add_enabled(
|
||||
s.compatibility != Compatibility::Pdf1_4,
|
||||
widgets::checkbox(&mut object_streams, tl!("Compress document structure (object streams)")),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
s.object_streams = object_streams;
|
||||
}
|
||||
ui.add_enabled(false, widgets::checkbox(&mut true, tl!("Remove unused objects and merge identical ones")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,19 @@ fn the_pdf_optimizer_dialog_picks_a_preset() {
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().optimize_draft.settings.merge_fragments);
|
||||
h.get_by_value("Custom");
|
||||
|
||||
// Pick a target PDF version and toggle structure compression on Clean Up.
|
||||
h.get_by_value("Retain existing").click();
|
||||
h.run_steps(4);
|
||||
h.get_by_label("PDF 2.0").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().optimize_draft.settings.compatibility, pdfcraft_engine::optimize::Compatibility::Pdf2_0);
|
||||
h.get_by_label("Clean Up").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().optimize_draft.settings.object_streams);
|
||||
h.get_by_label("Compress document structure (object streams)").click();
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().optimize_draft.settings.object_streams);
|
||||
}
|
||||
|
||||
fn mixed_files(app: &PdfCraftApp) -> Vec<(String, Vec<u8>)> {
|
||||
|
||||
@@ -7245,11 +7245,11 @@ title = "Make compatible with a chosen PDF version"
|
||||
area = "K"
|
||||
tier = "P1"
|
||||
milestone = "M11"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["optimize.advanced"]
|
||||
tools = ["doc_optimize"]
|
||||
evidence = ["crates/optimize/tests/optimize.rs::compatibility_level_sets_version_unpacks_for_1_4_and_checks_encryption", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_picks_a_preset"]
|
||||
notes = "Retain existing (default), PDF 1.4, 1.5, 1.6, 1.7 or 2.0 in PDF Optimizer and doc_optimize; writes the chosen %PDF- header, clears any overriding catalog /Version, disables and unpacks object streams and cross-reference streams for PDF 1.4, and refuses when the document's encryption requires a newer PDF version (#3090)"
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.optimizer"
|
||||
@@ -7545,11 +7545,11 @@ title = "Compress document structure (object streams)"
|
||||
area = "K"
|
||||
tier = "P1"
|
||||
milestone = "M11"
|
||||
status = "partial"
|
||||
commands = ["file.save_as"]
|
||||
tools = ["doc_save"]
|
||||
evidence = ["crates/cos/tests/writer.rs::full_save_packs_objects_into_object_streams"]
|
||||
notes = "on by default for full saves; no Optimizer option yet"
|
||||
status = "shipped"
|
||||
commands = ["file.save_as", "optimize.advanced"]
|
||||
tools = ["doc_save", "doc_optimize"]
|
||||
evidence = ["crates/cos/tests/writer.rs::full_save_packs_objects_into_object_streams", "crates/optimize/tests/optimize.rs::compatibility_level_sets_version_unpacks_for_1_4_and_checks_encryption", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_picks_a_preset"]
|
||||
notes = "on by default for full saves and in PDF Optimizer ▸ Clean Up (and doc_optimize object_streams); turning it off or targeting PDF 1.4 unpacks object streams into standalone objects with a classic cross-reference table"
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.flate-unencoded"
|
||||
|
||||
Reference in New Issue
Block a user