mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
142 lines
6.5 KiB
Rust
142 lines
6.5 KiB
Rust
//! Optimize PDF ▸ Advanced optimization as a job that reports its progress.
|
|
//!
|
|
//! Optimizing a large file takes a while (every image is decoded, resampled and re-encoded), so
|
|
//! it is split like OCR: [`Session::optimize_job`] captures a copy of the document and the
|
|
//! choices, and [`OptimizeJob::run`] works anywhere (the UI runs it on a worker thread),
|
|
//! reporting each [`OptimizeStage`] and stopping when asked to.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use pdfcraft_cos::{SaveOptions, write_full};
|
|
|
|
use crate::{DocId, EditError, Hidden, OptimizeReport, Session, guard, optimize};
|
|
|
|
/// Where an optimization is.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum OptimizeStage {
|
|
/// Removing the user data chosen under Discard User Data, if any was.
|
|
Discarding,
|
|
/// Reading what the pages draw: `done` of `total` read.
|
|
Reading { done: usize, total: usize },
|
|
/// Merging the fragments of a page picture into one image.
|
|
Fragments,
|
|
/// About to process image `done` (0-based) of `total`.
|
|
Images { done: usize, total: usize },
|
|
/// Discarding objects and compressing unencoded streams.
|
|
CleaningUp,
|
|
/// Merging identical fonts and images.
|
|
Merging,
|
|
/// Writing the compressed copy.
|
|
Writing,
|
|
}
|
|
|
|
impl Default for OptimizeStage {
|
|
fn default() -> Self {
|
|
OptimizeStage::Reading { done: 0, total: 0 }
|
|
}
|
|
}
|
|
|
|
impl OptimizeStage {
|
|
/// How far along the whole run is, from 0 to 1, weighed by where the time goes: reading pages
|
|
/// and pictures. The rest are small fixed steps.
|
|
pub fn fraction(self) -> f32 {
|
|
let part = |done: usize, total: usize| if total == 0 { 1.0 } else { done.min(total) as f32 / total as f32 };
|
|
match self {
|
|
OptimizeStage::Discarding | OptimizeStage::Fragments => 0.0,
|
|
OptimizeStage::Reading { done, total } => 0.25 * if total == 0 { 0.0 } else { part(done, total) },
|
|
OptimizeStage::Images { done, total } => 0.25 + 0.55 * part(done, total),
|
|
OptimizeStage::CleaningUp => 0.8,
|
|
OptimizeStage::Merging => 0.85,
|
|
OptimizeStage::Writing => 0.9,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The optimized copy's bytes and what was done.
|
|
pub type Optimized = (Arc<Vec<u8>>, OptimizeReport);
|
|
|
|
/// What an optimization needs, independent of the session.
|
|
pub struct OptimizeJob {
|
|
cos: pdfcraft_cos::Document,
|
|
settings: optimize::Settings,
|
|
discard: Vec<Hidden>,
|
|
opts: SaveOptions,
|
|
}
|
|
|
|
impl Session {
|
|
/// Capture what optimizing document `id` with `settings` and the Remove Hidden Information
|
|
/// `discard` categories needs. A full rewrite: signed documents are refused.
|
|
pub fn optimize_job(&self, id: DocId, settings: &optimize::Settings, discard: &[Hidden]) -> Result<OptimizeJob, EditError> {
|
|
let doc = self.get(id).ok_or(EditError::NoDocument)?;
|
|
if doc.is_signed() {
|
|
return Err(EditError::Signed);
|
|
}
|
|
let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?;
|
|
Ok(OptimizeJob {
|
|
cos: editor.cos.clone(),
|
|
settings: settings.clone(),
|
|
discard: discard.to_vec(),
|
|
opts: SaveOptions {
|
|
mod_date: self.now().map(pdfcraft_cos::pdf_date),
|
|
object_streams: settings.effective_object_streams(),
|
|
..SaveOptions::default()
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
impl OptimizeJob {
|
|
/// Optimize and write the copy. `progress` is called as each stage starts and before each
|
|
/// image; returning `false` stops with [`EditError::Cancelled`]. A panic is returned as an
|
|
/// error, so a worker thread always reports back.
|
|
pub fn run(self, mut progress: impl FnMut(OptimizeStage) -> bool) -> Result<Optimized, EditError> {
|
|
guard(move || self.work(&mut progress)).map_err(EditError::Optimize)?
|
|
}
|
|
|
|
fn work(self, progress: &mut dyn FnMut(OptimizeStage) -> bool) -> Result<Optimized, EditError> {
|
|
let OptimizeJob { mut cos, settings, discard, opts } = self;
|
|
// Like every file written, without the values of Password-flag fields (#3926, #3944).
|
|
pdfcraft_forms::clear_password_values(&mut cos).map_err(|e| EditError::Write(e.to_string()))?;
|
|
pdfcraft_xfa::clear_password_values(&mut cos).map_err(|e| EditError::Write(e.to_string()))?;
|
|
let mut step = |stage| if progress(stage) { Ok(()) } else { Err(EditError::Cancelled) };
|
|
let discarded = if discard.is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
step(OptimizeStage::Discarding)?;
|
|
// Every ticked category, as `remove_hidden` decides between them: with form fields
|
|
// ticked, discarding hidden text also takes the hidden text the flattened appearances
|
|
// bring onto the page, which no scan before flattening counts. It refuses when its scan
|
|
// counts every one as 0, which would end the whole optimization, and may have changed
|
|
// the copy by then (an empty outline root goes with bookmarks), so the copy from before
|
|
// is kept instead: nothing is discarded.
|
|
let before = cos.clone();
|
|
match pdfcraft_redact::sanitize::remove_hidden(&mut cos, &discard) {
|
|
Err(pdfcraft_redact::RedactError::NothingToApply) => {
|
|
cos = before;
|
|
Vec::new()
|
|
}
|
|
r => r?,
|
|
}
|
|
};
|
|
let report = optimize::optimize_with_progress(&mut cos, &settings, &mut |s| {
|
|
step(match s {
|
|
optimize::Stage::Reading { done, total } => OptimizeStage::Reading { done, total },
|
|
optimize::Stage::Fragments => OptimizeStage::Fragments,
|
|
optimize::Stage::Images { done, total } => OptimizeStage::Images { done, total },
|
|
optimize::Stage::CleanUp => OptimizeStage::CleaningUp,
|
|
})
|
|
.is_ok()
|
|
})
|
|
.map_err(|e| match e {
|
|
optimize::OptimizeError::Cancelled => EditError::Cancelled,
|
|
e => EditError::Optimize(e.to_string()),
|
|
})?;
|
|
step(OptimizeStage::Merging)?;
|
|
let all: Vec<pdfcraft_cos::ObjRef> = cos.object_numbers().into_iter().map(|n| pdfcraft_cos::ObjRef::new(n, cos.generation(n))).collect();
|
|
let merged = pdfcraft_organize::dedupe_resources(&mut cos, &all, false);
|
|
step(OptimizeStage::Writing)?;
|
|
let bytes = write_full(&cos, &opts).map_err(|e| EditError::Write(e.to_string()))?;
|
|
Ok((Arc::new(bytes), OptimizeReport { optimize: report, merged, discarded }))
|
|
}
|
|
}
|