refactor(engine): move the open pipeline to open.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alehel
2026-10-25 23:02:51 +01:00
co-authored by Claude Opus 5.5
parent 631fc13f13
commit 05098de542
2 changed files with 358 additions and 342 deletions
+3 -342
View File
@@ -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<u32>,
}
/// 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,
}
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})"))))
}
}
/// Lay a dynamic XFA form out (pages and fields) and give its widgets appearances.
fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result<XfaLayout, String> {
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<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, 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<TrustStore>,
name: String,
path: Option<String>,
bytes: Arc<Vec<u8>>,
mut info: DocInfo,
cos: Result<Result<pdfcraft_cos::Document, pdfcraft_cos::CosError>, Box<dyn std::any::Any + Send>>,
render_password: Option<String>,
password: Option<&str>,
xfa: Option<XfaLayout>,
) -> 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)
}
+355
View File
@@ -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<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,
}
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, 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<TrustStore>,
name: String,
path: Option<String>,
bytes: Arc<Vec<u8>>,
mut info: DocInfo,
cos: Result<Result<pdfcraft_cos::Document, pdfcraft_cos::CosError>, Box<dyn std::any::Any + Send>>,
render_password: Option<String>,
password: Option<&str>,
xfa: Option<XfaLayout>,
) -> 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(),
}
}
}