From 05098de5421130dfe2ae013b4e6e1f06afa5008e Mon Sep 17 00:00:00 2001 From: alehel Date: Sun, 25 Oct 2026 22:22:07 +0100 Subject: [PATCH] refactor(engine): move the open pipeline to open.rs Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/lib.rs | 345 +----------------------------------- crates/engine/src/open.rs | 355 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 358 insertions(+), 342 deletions(-) create mode 100644 crates/engine/src/open.rs diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fdf57c87f..ac836aabc 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -31,6 +31,7 @@ mod history; pub mod js; pub mod links; pub mod ocr; +mod open; pub mod optimizer; pub mod print_raster; pub mod printcheck; @@ -234,7 +235,8 @@ pub use edit::{BATCH_LABELS, EDIT_LABELS, Edit}; use edit::{EditCtx, check_permission, run_edit}; pub use error::{EditError, SourceProblem}; pub use history::Label; -use history::{Editor, Keys, MAX_UNDO, Scope, TextBlocksCache, keys_after, scope_of, uses_scripts}; +use history::{Editor, Keys, MAX_UNDO, Scope, keys_after, scope_of, uses_scripts}; +pub use open::{OpenContext, PreparedDocument}; use pdfcraft_cos::{SaveOptions, write_full, write_incremental}; use pdfcraft_render::{DocInfo, Layer, LayerOp, OpenError, ReadingColors, RenderConfig, RenderPool, inspect}; use protection::unrestricted; @@ -1230,35 +1232,6 @@ pub struct Session { xfa_time_scale: Option, } -/// Immutable session state needed while a document is prepared away from its owning thread. -#[derive(Clone)] -pub struct OpenContext { - now: Option, - js_off: bool, - trust: Arc, - xfa_time_scale: u32, -} - -/// A fully prepared document that has not yet been registered in a session. -pub struct PreparedDocument { - document: Document, -} - -impl OpenContext { - /// Prepare a document without mutating the session that supplied this context. - pub fn prepare_open( - &self, - name: impl Into, - path: Option, - bytes: Arc>, - password: Option<&str>, - ) -> Result { - 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})")))) - } -} - /// Lay a dynamic XFA form out (pages and fields) and give its widgets appearances. fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result { let mut report = pdfcraft_xfa::render_into(doc).map_err(|e| e.to_string())?; @@ -1649,318 +1622,6 @@ impl Session { self.open(name, None, bytes, password.as_deref()).map_err(|e| e.to_string()) } - /// 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, - path: Option, - bytes: Arc>, - password: Option<&str>, - ) -> Result { - 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, path: Option, bytes: Arc>, password: Option<&str>) -> Result { - let prepared = self.prepare_open(name, path, bytes, password)?; - Ok(self.commit_open(prepared)) - } - - fn prepare_open_unguarded( - context: &OpenContext, - name: String, - path: Option, - bytes: Arc>, - password: Option<&str>, - ) -> Result { - 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>, 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 = if info.xfa.is_some() { info.warnings.clone() } else { Vec::new() }; - let mut document = Self::build_document(&context.trust, name, path, bytes, info, 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. - #[allow(clippy::too_many_arguments)] - fn build_document( - trust: &Arc, - name: String, - path: Option, - bytes: Arc>, - mut info: DocInfo, - cos: Result, Box>, - render_password: Option, - password: Option<&str>, - xfa: Option, - ) -> Document { - 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(), - } - } - fn doc_mut(&mut self, id: DocId) -> Result<&mut Document, EditError> { self.docs.iter_mut().find(|d| d.id == id).ok_or(EditError::NoDocument) } diff --git a/crates/engine/src/open.rs b/crates/engine/src/open.rs new file mode 100644 index 000000000..d8950d1d6 --- /dev/null +++ b/crates/engine/src/open.rs @@ -0,0 +1,355 @@ +//! 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, + js_off: bool, + trust: Arc, + xfa_time_scale: u32, +} + +/// A fully prepared document that has not yet been registered in a session. +pub struct PreparedDocument { + document: Document, +} + +impl OpenContext { + /// Prepare a document without mutating the session that supplied this context. + pub fn prepare_open( + &self, + name: impl Into, + path: Option, + bytes: Arc>, + password: Option<&str>, + ) -> Result { + 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, + path: Option, + bytes: Arc>, + password: Option<&str>, + ) -> Result { + 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, path: Option, bytes: Arc>, password: Option<&str>) -> Result { + let prepared = self.prepare_open(name, path, bytes, password)?; + Ok(self.commit_open(prepared)) + } + + fn prepare_open_unguarded( + context: &OpenContext, + name: String, + path: Option, + bytes: Arc>, + password: Option<&str>, + ) -> Result { + 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>, 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 = if info.xfa.is_some() { info.warnings.clone() } else { Vec::new() }; + let mut document = Self::build_document(&context.trust, name, path, bytes, info, 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. + #[allow(clippy::too_many_arguments)] + fn build_document( + trust: &Arc, + name: String, + path: Option, + bytes: Arc>, + mut info: DocInfo, + cos: Result, Box>, + render_password: Option, + password: Option<&str>, + xfa: Option, + ) -> Document { + 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(), + } + } +}