Files
pdfcraft/crates/engine/src/open.rs
T

358 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Opening a document: preparing it away from the session, then registering it.
use std::sync::{Arc, OnceLock};
use pdfcraft_cos::{SaveOptions, write_incremental};
use pdfcraft_render::{DocInfo, OpenError, RenderConfig, RenderPool, inspect};
use crate::history::{Editor, Keys, TextBlocksCache};
use crate::{
AppearanceCache, DocId, Document, Session, TrustStore, XFA_SCRIPTS_OFF, XfaLayout, display_bytes, guard, info_fields, js, note_warnings,
recovered_render_password, render_threads, same_pages, signatures_of, xfa, xfa_layout, xfa_values_from_datasets,
};
/// Immutable session state needed while a document is prepared away from its owning thread.
#[derive(Clone)]
pub struct OpenContext {
now: Option<i64>,
js_off: bool,
trust: Arc<TrustStore>,
xfa_time_scale: u32,
}
/// A fully prepared document that has not yet been registered in a session.
pub struct PreparedDocument {
document: Document,
}
struct DocumentOpenData<'a> {
name: String,
path: Option<String>,
bytes: Arc<Vec<u8>>,
info: DocInfo,
structure: Result<Result<pdfcraft_cos::Document, pdfcraft_cos::CosError>, Box<dyn std::any::Any + Send>>,
render_password: Option<String>,
password: Option<&'a str>,
xfa: Option<XfaLayout>,
}
impl OpenContext {
/// Prepare a document without mutating the session that supplied this context.
pub fn prepare_open(
&self,
name: impl Into<String>,
path: Option<String>,
bytes: Arc<Vec<u8>>,
password: Option<&str>,
) -> Result<PreparedDocument, OpenError> {
let name = name.into();
guard(|| Session::prepare_open_unguarded(self, name, path, bytes, password))
.unwrap_or_else(|m| Err(OpenError::Invalid(format!("reading it failed unexpectedly ({m})"))))
}
}
impl Session {
/// Snapshot the session state that affects open-time scripts, dates and signature trust.
pub fn open_context(&self) -> OpenContext {
OpenContext { now: self.now(), js_off: self.js_off, trust: self.trust.clone(), xfa_time_scale: self.xfa_time_scale.unwrap_or(1) }
}
/// Prepare a document without adding it to this session.
pub fn prepare_open(
&self,
name: impl Into<String>,
path: Option<String>,
bytes: Arc<Vec<u8>>,
password: Option<&str>,
) -> Result<PreparedDocument, OpenError> {
self.open_context().prepare_open(name, path, bytes, password)
}
/// Register a prepared document. This is the only open step that mutates the session.
pub fn commit_open(&mut self, prepared: PreparedDocument) -> DocId {
self.next_id += 1;
let id = DocId(self.next_id);
let mut document = prepared.document;
document.id = id;
self.docs.push(document);
id
}
/// Synchronous convenience API used by the CLI and automation.
pub fn open(&mut self, name: impl Into<String>, path: Option<String>, bytes: Arc<Vec<u8>>, password: Option<&str>) -> Result<DocId, OpenError> {
let prepared = self.prepare_open(name, path, bytes, password)?;
Ok(self.commit_open(prepared))
}
fn prepare_open_unguarded(
context: &OpenContext,
name: String,
path: Option<String>,
bytes: Arc<Vec<u8>>,
password: Option<&str>,
) -> Result<PreparedDocument, OpenError> {
let cos = std::panic::catch_unwind(|| pdfcraft_cos::Document::open_with_password(bytes.clone(), password));
// The renderer authenticates on its own. It cannot use the owner password of R2–R4
// files, so give it the user password that owner authentication recovers.
let (info, render_password) = match inspect(bytes.clone(), password) {
Ok(info) => (info, password.map(str::to_owned)),
Err(OpenError::WrongPassword) => {
let user = match &cos {
Ok(Ok(d)) => recovered_render_password(d),
_ => None,
};
let user = user.ok_or(OpenError::WrongPassword)?;
(inspect(bytes.clone(), Some(&user))?, Some(user))
}
Err(e) => return Err(e),
};
// A dynamic XFA form is a shell around an XML template; lay the template out into real
// pages and fields so the rest of the engine works on it. The original bytes stay: the
// laid-out form is one appended revision.
let opts = SaveOptions { mod_date: context.now.map(pdfcraft_cos::pdf_date), ..SaveOptions::default() };
// Write `work` as one more revision and reopen it: what the document then is.
let rebase = |work: &pdfcraft_cos::Document| -> Result<(Arc<Vec<u8>>, DocInfo, pdfcraft_cos::Document), String> {
let new_bytes = write_incremental(work, &opts).map(Arc::new).map_err(|e| e.to_string())?;
let new_info = inspect(new_bytes.clone(), render_password.as_deref()).map_err(|e| e.to_string())?;
let new_cos = pdfcraft_cos::Document::open_with_password(new_bytes.clone(), password).map_err(|e| e.to_string())?;
Ok((new_bytes, new_info, new_cos))
};
let (bytes, info, cos, xfa) = match (info.xfa, cos) {
(Some(pdfcraft_render::Xfa::Dynamic), Ok(Ok(cos))) if pdfcraft_xfa::existing_layout(&cos).is_none() => {
let mut work = cos.clone();
let laid_out = guard(|| xfa_layout(&mut work))
.unwrap_or_else(|m| Err(format!("laying it out failed unexpectedly ({m})")))
.and_then(|report| rebase(&work).map(|(b, i, c)| (b, i, c, report)));
match laid_out {
Ok((b, i, c, report)) => (b, i, Ok(Ok(c)), Some(report)),
Err(e) => {
let mut info = info;
info.warnings.push(format!("This dynamic XFA form could not be laid out: {e}"));
(bytes, info, Ok(Ok(cos)), None)
}
}
}
// A static XFA form, or a dynamic one laid out earlier: its datasets may hold values
// filled in by another viewer since; give the fields those values.
(Some(_), Ok(Ok(cos))) => {
let report = pdfcraft_xfa::existing_layout(&cos);
let mut work = cos.clone();
let synced =
guard(|| xfa_values_from_datasets(&mut work)).unwrap_or_else(|m| Err(format!("reading its data failed unexpectedly ({m})")));
match synced {
Ok(names) if names.is_empty() => (bytes, info, Ok(Ok(cos)), report),
Ok(names) => match rebase(&work) {
Ok((b, mut i, c)) => {
// The fields were rewritten from the XFA data: say so, and which.
let shown: Vec<&str> = names.iter().take(5).map(String::as_str).collect();
let more = if names.len() > shown.len() { format!(" and {} more", names.len() - shown.len()) } else { String::new() };
i.warnings.push(format!(
"{} form field value{} were taken from this form's XFA data (filled in by another viewer): {}{more}",
names.len(),
if names.len() == 1 { "" } else { "s" },
shown.join(", ")
));
(b, i, Ok(Ok(c)), report)
}
Err(e) => {
let mut info = info;
info.warnings.push(format!("The values in this form's XFA data could not be applied: {e}"));
(bytes, info, Ok(Ok(cos)), report)
}
},
Err(e) => {
let mut info = info;
info.warnings.push(format!("The values in this form's XFA data could not be applied: {e}"));
(bytes, info, Ok(Ok(cos)), report)
}
}
}
(_, cos) => (bytes, info, cos, None),
};
// A laid-out XFA form: its initialize and calculate scripts run now, as on opening in
// Acrobat; what they change is one more revision, still nothing to save.
let mut script_output = js::JsOutput::default();
let mut ran_away = false;
let xfa_template = match (&xfa, &cos) {
(Some(_), Ok(Ok(c))) => xfa::template(c),
_ => None,
};
let (bytes, info, cos) = match (&xfa, cos) {
(Some(_), Ok(Ok(cos))) if !context.js_off => match xfa_template.clone() {
Some(tpl) => {
let mut work = cos.clone();
let pages = info.pages.len();
let ran = guard(|| {
let mut run = xfa::XfaRun {
page_count: pages,
out: &mut script_output,
datasets: None,
ran_away: false,
time_scale: context.xfa_time_scale,
};
let changes = xfa::on_open(&mut work, &tpl, &mut run);
if run.ran_away {
ran_away = true;
}
changes
})
.unwrap_or_else(|m| Err(format!("its scripts failed unexpectedly ({m})")));
match ran {
Ok(changes) if work.is_modified() => match rebase(&work) {
Ok((b, mut i, c)) => {
// The file as saved differs from what opening shows: say so.
if !changes.is_empty() {
i.warnings.push(format!(
"This form's scripts changed it on opening ({}); saving keeps those changes",
changes.describe()
));
}
(b, i, Ok(Ok(c)))
}
Err(e) => {
script_output.errors.push(format!("The form's scripts could not be applied: {e}"));
(bytes, info, Ok(Ok(cos)))
}
},
Ok(_) => (bytes, info, Ok(Ok(cos))),
Err(e) => {
script_output.errors.push(format!("The form's scripts could not run: {e}"));
(bytes, info, Ok(Ok(cos)))
}
}
}
None => (bytes, info, Ok(Ok(cos))),
},
(_, cos) => (bytes, info, cos),
};
// The catalog leads to no page, but the reader found pages by scanning: gather the page
// objects into a page tree as one more revision, or page edits address none of them
// (#1656). Kept only when the reader then shows the same pages, and never on a signed
// document, where writing the repair could rewrite the file under its signatures.
let (bytes, info, cos) = match cos {
Ok(Ok(cos)) if !info.pages.is_empty() && pdfcraft_organize::page_count(&cos).unwrap_or(0) == 0 => {
let mut work = cos.clone();
let signed = || pdfcraft_sign::pdf::list(&cos, &bytes, &context.trust).iter().any(|s| s.signed);
let rebuilt = match pdfcraft_organize::recover_page_tree(&mut work) {
Ok(n) if n > 0 && !signed() => rebase(&work).ok().filter(|(_, i, _)| same_pages(&i.pages, &info.pages)),
_ => None,
};
match rebuilt {
Some((b, i, mut c)) => {
for line in cos.repair_log() {
c.note_repair(line.clone());
}
c.note_repair(format!("the page tree is missing; its {} pages were found by scanning the file", i.pages.len()));
(b, i, Ok(Ok(c)))
}
None => (bytes, info, Ok(Ok(cos))),
}
}
cos => (bytes, info, cos),
};
// XFA notes survive the document being re-read after edits.
let xfa_warnings: Vec<String> = if info.xfa.is_some() { info.warnings.clone() } else { Vec::new() };
let mut document =
Self::build_document(&context.trust, DocumentOpenData { name, path, bytes, info, structure: cos, render_password, password, xfa });
// A script that ran away on open turns the form's scripts off for this document.
document.xfa_template = if ran_away { None } else { xfa_template };
document.js_output.append(script_output);
if ran_away {
document.xfa_scripts_off = true;
document.js_output.errors.push(XFA_SCRIPTS_OFF.into());
}
note_warnings(&mut document.xfa_warnings, &xfa_warnings);
Ok(PreparedDocument { document })
}
/// The last step of opening: build the document record and register it.
fn build_document(trust: &Arc<TrustStore>, data: DocumentOpenData<'_>) -> Document {
let DocumentOpenData { name, path, bytes, mut info, structure: cos, render_password, password, xfa } = data;
let config = RenderConfig { password: render_password.as_deref().map(Arc::from), ..Default::default() };
let (mut editor, read_only_reason) = match cos {
Ok(Ok(cos)) => {
let keys = Keys { render: render_password.clone(), reopen: password.map(str::to_owned) };
let saved = cos.bytes().clone();
(
Some(Editor {
cos,
saved,
saved_keys: keys.clone(),
unsaved_file: false,
undo: Vec::new(),
redo: Vec::new(),
keys,
appearances: AppearanceCache::default(),
field_appearances: pdfcraft_forms::FieldAppearanceCache::default(),
text_blocks: TextBlocksCache::default(),
}),
None,
)
}
Ok(Err(e)) => (None, Some(e.to_string())),
Err(_) => (None, Some("the document structure could not be read for editing".into())),
};
let sig_cache = Arc::new(pdfcraft_sign::DigestCache::default());
let signatures = editor.as_ref().map(|e| signatures_of(&e.cos, &bytes, trust, &sig_cache)).unwrap_or_default();
// The inspection reads only `/Info`; a file stating its title or author only in XMP still
// shows them (#2373).
if let Some(e) = &editor {
for (field, value) in info_fields(&mut info, &e.cos) {
if field.is_none() {
*field = value;
}
}
}
let display = display_bytes(editor.as_mut(), &bytes);
let renderer = RenderPool::new(display.clone(), render_threads(), config.clone());
let (form, adopted) = editor
.as_ref()
.map(|e| {
let mut fields = pdfcraft_forms::fields_with_adopted(&e.cos);
xfa::mark_script_buttons(&e.cos, &mut fields.items);
(fields.items, fields.adopted_page_fields)
})
.unwrap_or_default();
// Fields the file lists only as page widgets are adopted leniently; the file says so.
if adopted > 0
&& let Some(e) = &mut editor
{
let line = if adopted == 1 {
"the form's /Fields list omits 1 field; it was read from the page annotations".to_string()
} else {
format!("the form's /Fields list omits {adopted} fields; they were read from the page annotations")
};
e.cos.note_repair(line);
}
Document {
id: DocId(0),
name,
path,
bytes,
display,
info,
renderer,
password: render_password,
dirty: false,
generation: 0,
snapshot_generation: 0,
read_only_reason,
form: Arc::new(form),
marker_scan: OnceLock::new(),
links: OnceLock::new(),
signatures: Arc::new(signatures),
trust: trust.clone(),
sig_cache,
editor,
config,
js_output: Default::default(),
xfa,
xfa_template: None,
xfa_scripts_off: false,
fonts_stale: false,
xfa_warnings: Vec::new(),
edit_notices: Vec::new(),
}
}
}