mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M4: keep the render pool across metadata edits (#750)
* M4: keep the render pool across metadata edits Metadata edits (SetInfo) no longer rebuild the render pool or the display bytes. The engine refreshes the document-information fields, the file size, the signatures (when present) and the working bytes, and keeps the renderer. Undo and redo of metadata edits reuse the same pool. Comment and form edits still rebuild the renderer and their view data. Tests: - metadata_edits_and_history_reuse_the_render_pool - field_and_comment_edits_still_rebuild_the_render_pool - metadata_edit_keeps_the_renderer_without_changing_page_content (the metadata invalidation audit; its assertions now expect the kept renderer) Supersedes #711 and #712. Co-Authored-By: Claude Haiku 5.5 <[email protected]> * M4: a metadata edit reads the other Info entries as the full inspection does Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Haiku 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
2 files changed
+101
-1
No files matched your search
@@ -146,6 +146,8 @@ struct Keys {
|
||||
enum Scope {
|
||||
/// Anything: re-inspect the whole document.
|
||||
Full,
|
||||
/// Document-information entries; page content and appearances are unchanged.
|
||||
Metadata,
|
||||
/// Only comments: re-read the comment list from the object graph.
|
||||
Comments,
|
||||
/// Only form field values (and their widget appearances).
|
||||
@@ -163,6 +165,7 @@ fn uses_scripts(edit: &Edit) -> bool {
|
||||
|
||||
fn scope_of(edit: &Edit) -> Scope {
|
||||
match edit {
|
||||
Edit::SetInfo { .. } => Scope::Metadata,
|
||||
// A file attachment also changes the Attachments list.
|
||||
Edit::AddAnnotation(a) if matches!(a.shape, Shape::Attachment { .. }) => Scope::Full,
|
||||
Edit::AddMeasurement(_)
|
||||
@@ -2467,7 +2470,8 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the working bytes and renderer, and the view data that `scope` may have changed.
|
||||
/// Rebuild the working bytes and the view data that `scope` may have changed. Metadata edits
|
||||
/// keep the renderer; other scopes also replace it.
|
||||
/// Comment and form edits skip the full re-inspection (seconds on very large files): the
|
||||
/// comment list and field values are re-read from the object graph instead.
|
||||
fn refresh_scoped(doc: &mut Document, scope: Scope) -> Result<(), EditError> {
|
||||
@@ -2480,10 +2484,28 @@ impl Session {
|
||||
} else {
|
||||
editor.cos.bytes().clone()
|
||||
};
|
||||
if scope == Scope::Metadata {
|
||||
// Read the way the full inspection reads them (NULs and spaces trimmed, empty is
|
||||
// none), so editing one entry doesn't change how the others display.
|
||||
let info = |key| pdfcraft_organize::info(&editor.cos, key).map(|v| v.trim_matches('\0').trim().to_string()).filter(|v| !v.is_empty());
|
||||
doc.info.title = info("Title");
|
||||
doc.info.author = info("Author");
|
||||
doc.info.subject = info("Subject");
|
||||
doc.info.keywords = info("Keywords");
|
||||
doc.info.creator = info("Creator");
|
||||
doc.info.producer = info("Producer");
|
||||
doc.info.file_size = bytes.len();
|
||||
if !doc.signatures.is_empty() {
|
||||
doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache);
|
||||
}
|
||||
doc.bytes = bytes;
|
||||
return Ok(());
|
||||
}
|
||||
let mut form = pdfcraft_forms::fields(&editor.cos);
|
||||
xfa::mark_script_buttons(&editor.cos, &mut form);
|
||||
let form = Arc::new(form);
|
||||
match scope {
|
||||
Scope::Metadata => return Ok(()),
|
||||
Scope::Comments => doc.info.annotations = comment_list(&editor.cos),
|
||||
Scope::Form => {
|
||||
for f in &mut doc.info.fields {
|
||||
|
||||
@@ -133,6 +133,84 @@ fn edits_update_view_data_and_mark_dirty() {
|
||||
assert_eq!(page_texts(&s, id), ["Page 1", "Page 3"], "renderer refreshed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_edit_keeps_the_renderer_without_changing_page_content() {
|
||||
let (mut s, id) = session_with(1);
|
||||
let request = pdfcraft_render::RenderRequest { page: 0, kind: pdfcraft_render::RequestKind::Pixels, scale: 1.0, ..Default::default() };
|
||||
let render = |session: &Session| {
|
||||
let doc = session.get(id).unwrap();
|
||||
doc.renderer.set_queue(vec![request]);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
if let Some(page) = doc.renderer.try_recv() {
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
return (doc.renderer.stats(), page.rgba);
|
||||
}
|
||||
assert!(std::time::Instant::now() < deadline, "page render did not finish");
|
||||
std::thread::yield_now();
|
||||
}
|
||||
};
|
||||
|
||||
let (before, before_pixels) = render(&s);
|
||||
assert_eq!(before.page_interpretations, 1);
|
||||
s.apply(id, Edit::SetInfo { key: "Title".into(), value: "Report".into() }).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().info_value("Title").as_deref(), Some("Report"));
|
||||
assert_eq!(s.get(id).unwrap().renderer.stats().page_interpretations, 1, "a metadata edit keeps the renderer and its page interpretation");
|
||||
let (after, after_pixels) = render(&s);
|
||||
assert_eq!(after.page_interpretations, 2, "the same renderer serves the second request, so its count continues");
|
||||
assert_eq!(before_pixels, after_pixels, "document properties do not change page pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_edits_and_history_reuse_the_render_pool() {
|
||||
let (mut s, id) = session_with(1);
|
||||
let pixels = shown(&s, id, 0).rgba;
|
||||
let doc = s.get(id).unwrap();
|
||||
let stats = doc.renderer.stats();
|
||||
let display = doc.display.clone();
|
||||
assert_eq!(stats.page_interpretations, 1);
|
||||
|
||||
s.apply(id, Edit::SetInfo { key: "Title".into(), value: "Report".into() }).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.title.as_deref(), Some("Report"));
|
||||
assert_eq!(doc.renderer.stats(), stats);
|
||||
assert!(Arc::ptr_eq(&doc.display, &display));
|
||||
assert_eq!(pixels.as_ref().len(), 200 * 300 * 4);
|
||||
|
||||
s.undo(id).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.title, None);
|
||||
assert_eq!(doc.renderer.stats(), stats);
|
||||
assert!(Arc::ptr_eq(&doc.display, &display));
|
||||
|
||||
s.redo(id).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.title.as_deref(), Some("Report"));
|
||||
assert_eq!(doc.renderer.stats(), stats);
|
||||
assert!(Arc::ptr_eq(&doc.display, &display));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_and_comment_edits_still_rebuild_the_render_pool() {
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let id = s.open("form.pdf", None, Arc::new(scripted_form()), None).unwrap();
|
||||
let _ = shown(&s, id, 0);
|
||||
assert_eq!(s.get(id).unwrap().renderer.stats().page_interpretations, 1);
|
||||
|
||||
s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("4".into()) }).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().renderer.stats().page_interpretations, 0);
|
||||
let _ = shown(&s, id, 0);
|
||||
assert_eq!(s.get(id).unwrap().renderer.stats().page_interpretations, 1);
|
||||
|
||||
let shape = Shape::Rectangle { rect: [20.0, 20.0, 60.0, 60.0] };
|
||||
s.apply(
|
||||
id,
|
||||
Edit::AddAnnotation(NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: String::new(), author: "Test".into() }),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.get(id).unwrap().renderer.stats().page_interpretations, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_and_redo_restore_exact_states() {
|
||||
let (mut s, id) = session_with(3);
|
||||
|
||||
Reference in new issue
Block a user