mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 05:23:32 +00:00
* M9: redraw named-layer signature status layers - /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state - copies made once per original and state; originals never edited - Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: draw signature states in the display copy, exports and print - marks follow the bytes shown; a changed verdict redraws after a metadata edit - background opens pick up the trust and captions current at commit Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: status captions in the interface language - the three captions in all 22 catalogues - Signatures panel colours from sign::status_rgb Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: signature badge in the widget's corner - a quarter of the shorter side, 48pt cap; outline only under 32pt - dropped for a drawn valid state; bottom-right where the appearance draws one - sign_list reports verdict_shown; parity and roadmap Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep Arabic signature captions in logical order Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: read /ActualText over painted paths and pictures - A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn. - A span the page content leaves open ends before the annotations. - OCR and scan field detection still count such a page as having no text of its own. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M8: redaction strips /ActualText around paint it clips - A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: recognise only layer forms that draw n1 and n3 - Settle on the one form holding both first, then read its content once per call. - Skip an /N that is its own layer form; copies keep the layer's /OC. - A verdict counts as drawn when any visible widget shows it. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep the signature badge and redraw wherever a state changes - The badge stays for every state; its mark sits bottom-right as the widget lies on the page. - An edit that changes a verdict redraws every page; captions apply before startup files open. - Print, the CLI's render and the repair log follow the display copy. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: restore the attribution usage the rebase dropped - Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them. - Roadmap and localization notes for this round. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: pin signature field names on a nested form Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: bound the signature field walk - kids share their parent's name and type instead of each copying them - a widget stays listed under every field that claims it, as one shared copy - a field written in place in a shared /Kids array is walked once - caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: keep a document signed when the field walk stops early - list_cached adds a signed row of unknown validity for the fields the walk didn't read - Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M8: removing hidden content no longer strips the names of shadings - With no regions to clip, a shading is left alone instead of wrapped and marked clipped - clean_replacements skips scopes already marked, so deep nesting stays linear Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: read a picture's /ActualText over the whole picture - draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture - Painted grows in place instead of being copied through an Option Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: cap the bytes of names the signature field walk keeps - A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread - Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught - sig_fields is private again, and the cap docs say what happens past each cap Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: skip status-layer work that changes nothing; fix tests that could not fail - show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit - revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it - Scan form detection reads the page's text once; comment and badge tips share one hover rule - Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: test the signature field name cap at the engine boundary - A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: test signature states beside fields the viewer draws - One display copy holds both a field's drawn appearance and the signature's state. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: draw Thai and Devanagari status captions in their own script - script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English. - Localization, parity and the craft-fonts usage line list the new scripts. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M2: paint left out by an exclusion mixes its /ActualText replacement - A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does. - Glyphs still place a replacement that has them. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: a long-named field that can't hold a signature leaves the walk whole - A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned. - A signature field, or any field with kids, still counts as unread. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: status captions use document faces only - Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani. - The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: refuse a new signature field the field walk can't read back - Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's. - A field the walk already reads is signed as before. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: refuse a new signature field with a blank name - The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: an unnamed signed field or an unreadable form counts as unread - A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed. - Signing refuses a blank new field name before it adds the field, so the refusal still says why. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: no display copy when no signature state is drawn - display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the verdict mark stays on the page - A widget past the crop box takes its mark in the part on the page, or none when too little of it is there. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: typed values stay drawn while a signature redraws every page - A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the action, Save As and shading tests check their effects - The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: values typed earlier stay drawn while a signature redraws every page - A form edit that changes a signature's state keeps every value still waiting on its page, not only its own. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: comment and content edits keep typed values drawn too - An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit. - The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: the language-change test skips without catalogs - Like the other language tests, it returns early when the build has no craft-i18n catalogs. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> * M9: signed fields behind kids without a name count - A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name. - An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row. Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> --------- Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
374 lines
18 KiB
Rust
374 lines
18 KiB
Rust
//! 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, StatusCaptions, TrustStore, XFA_SCRIPTS_OFF, XfaLayout, display_bytes, display_config, 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>,
|
||
captions: Arc<StatusCaptions>,
|
||
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(),
|
||
captions: self.captions.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;
|
||
// Trust or the interface language may have changed while it was prepared on another
|
||
// thread.
|
||
if !Arc::ptr_eq(&document.trust, &self.trust) {
|
||
document.captions = self.captions.clone();
|
||
Self::revalidate(&mut document, self.trust.clone());
|
||
} else if *document.captions != *self.captions {
|
||
Self::draw_captions(&mut document, self.captions.clone());
|
||
}
|
||
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, 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(context: &OpenContext, 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 mut signatures = editor.as_ref().map(|e| signatures_of(&e.cos, &bytes, &context.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, &mut signatures, &context.captions);
|
||
let renderer = RenderPool::new(display.clone(), render_threads(), display_config(&config));
|
||
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: context.trust.clone(),
|
||
captions: context.captions.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(),
|
||
}
|
||
}
|
||
}
|