mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M14: scan PdfCraft page-content markers once per document refresh (#1016)
* M14: Scan PdfCraft page-content markers once on open Combine the engine-open marks and added-content discovery passes while keeping the existing public scans and document state unchanged. Add scanner equivalence and engine-open coverage, and retain the local Clippy binding needed for the workspace gate. Co-authored-by: Codex <[email protected]> * M14: reuse the combined marker scan on full refresh Keep full refresh from repeating the separate marks and added-content traversals, with a mark_saved regression against the independent scans. Co-authored-by: Codex <[email protected]> * M14: scan marker metadata without cloning content containers Borrow direct content objects and keep referenced objects alive during marker inspection. This avoids copying /Contents arrays and /PCAdded dictionaries for the combined read-only scan while preserving one-reference resolution and content-object identity. Co-authored-by: Codex <[email protected]> * M14: record combined marker scan measurements Update the #307 progress entry with full-refresh reuse, the borrowed scan implementation, and exact before/after timing and memory measurements. Co-authored-by: Codex <[email protected]> * M14: align marker measurements with final benchmark run Replace the preliminary values with the final exact-revision alternating-run medians and corresponding pair counts. Co-authored-by: Codex <[email protected]> * marks_present and list_added read through scan_markers, so the parsing lives in one place --------- Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
6 files changed
+310
-55
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** M14 #307 Plan 3 uses one marker traversal on open and full refresh, and the combined scanner borrows metadata instead of cloning scan-only containers; includes a behavior-preserving `organize` binding for the workspace Clippy gate · earlier: minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (closed issues synced: #774, #756 and the #799–#821 batch; earlier: localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
PdfCraft's one-page summary: how close it is to Adobe Acrobat Pro's offline feature set, and what
|
||||
@@ -135,6 +136,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
Newest first: the date, what moved, and the new overall estimate.
|
||||
|
||||
- **2026-10-11 (M14, #307 Plan 3):** Engine open and full refresh use one page/content traversal to populate marks and added content. The independent public scans remain available; the combined scanner borrows direct objects and holds referenced objects through `Arc<Object>` instead of cloning scan-only `/Contents` arrays or `/PCAdded` dictionaries. Differential tests cover an indirect `/PCAdded` dictionary as well as ordering, malformed/direct/indirect/repeated entries and `/UserUnit`. Exact `bbbe600`→final release CLI measurements used 10 alternating pairs: wall-time medians were 183.645→174.440 ms on 10,000 marker-free pages (10/10 wins), 33.587→32.752 ms on 1,500 mixed pages (8/10), and 24.331→20.133 ms on the 100,000-entry array stress fixture (10/10). The stress fixture's maximum-RSS median fell 28,655,616→19,038,208 B; peak-footprint medians were 14,747,136→14,673,408 B. From P0, the follow-up stress run was 20.886→20.201 ms (10/10), with RSS 23,953,408→19,087,360 B and peak footprint 14,706,144→14,697,952 B. `doc_open` stdout hashes matched every pair. The ancillary `organize` binding preserves the prior `prune_legacy` update condition. Estimates unchanged.
|
||||
- **2026-10-11 (M4, Organize overview):** Opening Organize pages from All tools, Home, the tool palette or `ui.set tool=organize` opens the thumbnail grid directly. Checkboxes toggle individual pages without modifier keys; page controls rotate either way or delete one page, while the toolbar acts on the selection. Portrait and landscape previews retain their aspect ratio. UI and control regressions cover entry, selection, single-page versus batch actions, undo, permissions and retaining the last page. Milestone and effort estimates unchanged.
|
||||
|
||||
- **2026-10-10 (M9, signature graphics):** A visible digital signature can show a graphic where the signer's name is large, as Acrobat's Configure Signature Appearance does: the saved Fill & Sign signature (drawn strokes smoothed as ink, typed outlines in the script font, or an image) or an imported PNG/JPEG. Sign as previews it and remembers the choice for the next signing; `sign_document` takes `graphic` (`image`, `text` or `strokes`). Graphics are checked and capped before drawing (non-finite or unbounded points, mismatched pixel buffers, oversized images are refused with an error the caller can act on); images keep their alpha as a soft mask. `sign.signature-appearance` stays partial (no named saved appearances or PDF-page graphics). Overall estimates unchanged.
|
||||
|
||||
+79
-32
@@ -14,7 +14,7 @@ use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
|
||||
use pdfcraft_fonts::{GlyphError, GlyphOutline, ShapedCluster, arabic_glyph, helvetica_width, literal, shape_arabic, win_ansi};
|
||||
use unicode_bidi::{Level, ParagraphBidiInfo};
|
||||
|
||||
use crate::{EditError, check, contents, n, page_list, place_tagged};
|
||||
use crate::{EditError, MarkKind, check, contents, n, page_list, place_tagged};
|
||||
|
||||
const TAG: &str = "Added";
|
||||
|
||||
@@ -198,6 +198,13 @@ pub struct Added {
|
||||
pub content: Content,
|
||||
}
|
||||
|
||||
/// PdfCraft-owned page content discovered in one page/content traversal.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MarkerScan {
|
||||
pub marks: Vec<MarkKind>,
|
||||
pub added: Vec<Added>,
|
||||
}
|
||||
|
||||
fn nums(doc: &Document, o: Option<&Object>) -> Vec<f64> {
|
||||
o.map(|o| doc.resolve(o)).and_then(|a| a.as_array().map(|a| a.iter().filter_map(Object::as_f64).collect())).unwrap_or_default()
|
||||
}
|
||||
@@ -747,6 +754,76 @@ fn parse(doc: &Document, d: &Dict) -> Option<Content> {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_resolved<T>(doc: &Document, object: &Object, f: impl FnOnce(&Object) -> T) -> T {
|
||||
match object {
|
||||
Object::Ref(reference) => {
|
||||
let resolved = doc.get(*reference);
|
||||
f(&resolved)
|
||||
}
|
||||
direct => f(direct),
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_content_entry(doc: &Document, page: &pdfcraft_model::Page, mut visit: impl FnMut(&Object)) {
|
||||
let Some(contents) = page.dict.get(b"Contents") else { return };
|
||||
with_resolved(doc, contents, |resolved| match resolved {
|
||||
Object::Array(entries) => {
|
||||
for entry in entries {
|
||||
visit(entry);
|
||||
}
|
||||
}
|
||||
_ => visit(contents),
|
||||
});
|
||||
}
|
||||
|
||||
/// Finds page marks and editable added content in one pass over the page content entries.
|
||||
pub fn scan_markers(doc: &Document) -> MarkerScan {
|
||||
let mut scan = MarkerScan::default();
|
||||
for (pi, page) in page_list(doc).iter().enumerate() {
|
||||
for_each_content_entry(doc, page, |entry| {
|
||||
with_resolved(doc, entry, |resolved| {
|
||||
let Some(dict) = resolved.as_dict() else { return };
|
||||
let tag = dict.name(b"PCMark");
|
||||
let kind = if tag == Some(&b"HeaderFooter"[..]) {
|
||||
Some(MarkKind::HeaderFooter)
|
||||
} else if tag == Some(&b"Watermark"[..]) {
|
||||
Some(MarkKind::Watermark)
|
||||
} else if tag == Some(&b"Background"[..]) {
|
||||
Some(MarkKind::Background)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(kind) = kind
|
||||
&& !scan.marks.contains(&kind)
|
||||
{
|
||||
scan.marks.push(kind);
|
||||
}
|
||||
|
||||
let Some(obj) = Object::as_ref(entry) else { return };
|
||||
if tag != Some(TAG.as_bytes()) {
|
||||
return;
|
||||
}
|
||||
let Some(params_object) = dict.get(b"PCAdded") else { return };
|
||||
with_resolved(doc, params_object, |resolved_params| {
|
||||
let Some(params) = resolved_params.as_dict() else { return };
|
||||
if let Some(content) = parse(doc, params) {
|
||||
// Display points depend on the page's /UserUnit. An item records the one it was
|
||||
// written for; none means 1, as for items written before display space followed
|
||||
// /UserUnit.
|
||||
let written = params
|
||||
.get(b"UserUnit")
|
||||
.and_then(Object::as_f64)
|
||||
.filter(|u| u.is_finite() && *u >= 1.0)
|
||||
.map_or(1.0, |u| u.min(pdfcraft_model::MAX_USER_UNIT));
|
||||
scan.added.push(Added { page: pi, obj, content: content.scaled(page.user_unit(doc) / written) });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
scan
|
||||
}
|
||||
|
||||
fn validate(c: &Content) -> Result<(), EditError> {
|
||||
let r = c.rect();
|
||||
if !r.iter().all(|v| v.is_finite()) {
|
||||
@@ -835,37 +912,7 @@ fn write(doc: &mut Document, page: usize, c: &Content, obj: Option<ObjRef>) -> R
|
||||
|
||||
/// Every added item, page by page, in drawing order.
|
||||
pub fn list_added(doc: &Document) -> Vec<Added> {
|
||||
let mut out = Vec::new();
|
||||
for (pi, p) in page_list(doc).iter().enumerate() {
|
||||
let list: Vec<Object> = match p.dict.get(b"Contents") {
|
||||
Some(c) => match &*doc.resolve(c) {
|
||||
Object::Array(a) => a.clone(),
|
||||
_ => vec![c.clone()],
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
for o in list {
|
||||
let Some(r) = o.as_ref() else { continue };
|
||||
let obj = doc.get(r);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
if d.name(b"PCMark") != Some(TAG.as_bytes()) {
|
||||
continue;
|
||||
}
|
||||
let Some(params) = d.get(b"PCAdded").and_then(|o| doc.resolve(o).as_dict().cloned()) else { continue };
|
||||
if let Some(c) = parse(doc, ¶ms) {
|
||||
// Display points depend on the page's /UserUnit. An item records the one it was
|
||||
// written for; none means 1, as for items written before display space followed
|
||||
// /UserUnit.
|
||||
let written = params
|
||||
.get(b"UserUnit")
|
||||
.and_then(Object::as_f64)
|
||||
.filter(|u| u.is_finite() && *u >= 1.0)
|
||||
.map_or(1.0, |u| u.min(pdfcraft_model::MAX_USER_UNIT));
|
||||
out.push(Added { page: pi, obj: r, content: c.scaled(p.user_unit(doc) / written) });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
scan_markers(doc).added
|
||||
}
|
||||
|
||||
/// Add an item to a page (0-based). Returns its index among the page's added items.
|
||||
|
||||
+5
-19
@@ -619,24 +619,7 @@ pub fn remove_marks(doc: &mut Document, pages: &[usize], kind: MarkKind) -> Resu
|
||||
|
||||
/// Which kinds of marks the document has (for enabling Update/Remove).
|
||||
pub fn marks_present(doc: &Document) -> Vec<MarkKind> {
|
||||
let mut found = Vec::new();
|
||||
for page in page_list(doc) {
|
||||
let obj = doc.get(page.obj);
|
||||
let Some(c) = obj.as_dict().and_then(|d| d.get(b"Contents").cloned()) else { continue };
|
||||
let list = match &*doc.resolve(&c) {
|
||||
Object::Array(a) => a.clone(),
|
||||
_ => vec![c.clone()],
|
||||
};
|
||||
for o in list {
|
||||
if let Some(t) = stream_tag(doc, &o)
|
||||
&& let Some(kind) = [MarkKind::HeaderFooter, MarkKind::Watermark, MarkKind::Background].into_iter().find(|m| m.tag() == t)
|
||||
&& !found.contains(&kind)
|
||||
{
|
||||
found.push(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
found
|
||||
added::scan_markers(doc).marks
|
||||
}
|
||||
|
||||
pub mod objects;
|
||||
@@ -649,7 +632,10 @@ pub use images::{ImageChange, PageImage, change_image, page_images, reading_imag
|
||||
pub mod text;
|
||||
pub use text::{BlockStyle, LineEdit, TextBlock, TextLine, reading_blocks, replace_block, replace_line, rewrite_block, text_blocks, text_lines};
|
||||
pub mod added;
|
||||
pub use added::{Added, AddedImage, AddedText, Align, Content, Family, add_content, delete_content, first_undrawable, list_added, update_content};
|
||||
pub use added::{
|
||||
Added, AddedImage, AddedText, Align, Content, Family, MarkerScan, add_content, delete_content, first_undrawable, list_added, scan_markers,
|
||||
update_content,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -267,6 +267,170 @@ fn added_items_follow_the_page_user_unit() {
|
||||
assert_eq!((old.rect[0], old.rect[2], old.rect[3], old.size), (144.0, 600.0, 1400.0, 28.0), "scaled to points");
|
||||
}
|
||||
|
||||
fn tagged_dictionary(tag: &str, added: Option<Object>) -> Dict {
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"PCMark".to_vec(), Object::name(tag));
|
||||
if let Some(added) = added {
|
||||
dict.set(b"PCAdded".to_vec(), added);
|
||||
}
|
||||
dict
|
||||
}
|
||||
|
||||
fn tagged_stream(doc: &mut Document, tag: &str, added: Option<Object>) -> Object {
|
||||
Object::Ref(doc.add(Object::Stream(Stream::from_raw(tagged_dictionary(tag, added), Vec::new()))))
|
||||
}
|
||||
|
||||
fn added_text_params() -> Dict {
|
||||
let mut params = Dict::new();
|
||||
params.set(b"Rect".to_vec(), Object::Array([10, 20, 110, 40].into_iter().map(Object::Int).collect()));
|
||||
params.set(b"Kind".to_vec(), Object::name("Text"));
|
||||
params.set(b"Text".to_vec(), Object::String(pdfcraft_cos::PdfString::text("Added text")));
|
||||
params.set(b"Size".to_vec(), Object::Int(12));
|
||||
params
|
||||
}
|
||||
|
||||
fn added_image_params(image: pdfcraft_cos::ObjRef) -> Dict {
|
||||
let mut params = Dict::new();
|
||||
params.set(b"Rect".to_vec(), Object::Array([10, 20, 110, 70].into_iter().map(Object::Int).collect()));
|
||||
params.set(b"Kind".to_vec(), Object::name("Image"));
|
||||
params.set(b"Image".to_vec(), Object::Ref(image));
|
||||
params
|
||||
}
|
||||
|
||||
fn set_page_contents(doc: &mut Document, contents: Object, user_unit: Option<Object>) {
|
||||
let page = pdfcraft_model::pages(doc)[0].obj;
|
||||
doc.update_dict(page, |dict| {
|
||||
dict.set(b"Contents".to_vec(), contents);
|
||||
if let Some(user_unit) = user_unit {
|
||||
dict.set(b"UserUnit".to_vec(), user_unit);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_marker_scan_matches_legacy(doc: &Document, label: &str, marks: &[MarkKind], added: usize) -> MarkerScan {
|
||||
let expected_marks = marks_present(doc);
|
||||
let expected_added = list_added(doc);
|
||||
let scan = scan_markers(doc);
|
||||
assert_eq!(scan.marks, expected_marks, "{label}: marks differ from the independent scan");
|
||||
assert_eq!(scan.added, expected_added, "{label}: added content differs from the independent scan");
|
||||
assert_eq!(scan.marks, marks, "{label}: fixture's mark order or deduplication is wrong");
|
||||
assert_eq!(scan.added.len(), added, "{label}: fixture's added-item count is wrong");
|
||||
scan
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_marker_scan_matches_the_independent_scans() {
|
||||
let empty = build(&["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", "<< /Type /Page /Parent 2 0 R >>"]);
|
||||
assert_marker_scan_matches_legacy(&empty, "empty page", &[], 0);
|
||||
assert_marker_scan_matches_legacy(&fixture(), "ordinary streams and a page without contents", &[], 0);
|
||||
|
||||
for (tag, kind) in [("HeaderFooter", MarkKind::HeaderFooter), ("Watermark", MarkKind::Watermark), ("Background", MarkKind::Background)] {
|
||||
let mut doc = fixture();
|
||||
let marker = tagged_stream(&mut doc, tag, None);
|
||||
set_page_contents(&mut doc, marker, None);
|
||||
assert_marker_scan_matches_legacy(&doc, tag, &[kind], 0);
|
||||
}
|
||||
|
||||
let mut all_marks = fixture();
|
||||
let background = tagged_stream(&mut all_marks, "Background", None);
|
||||
let watermark = tagged_stream(&mut all_marks, "Watermark", None);
|
||||
let header = tagged_stream(&mut all_marks, "HeaderFooter", None);
|
||||
set_page_contents(
|
||||
&mut all_marks,
|
||||
Object::Array(vec![background.clone(), watermark.clone(), header.clone(), watermark, header, background]),
|
||||
None,
|
||||
);
|
||||
assert_marker_scan_matches_legacy(&all_marks, "all mark kinds repeated", &[MarkKind::Background, MarkKind::Watermark, MarkKind::HeaderFooter], 0);
|
||||
|
||||
let mut single_reference = fixture();
|
||||
let marker = tagged_stream(&mut single_reference, "Watermark", None);
|
||||
set_page_contents(&mut single_reference, marker, None);
|
||||
assert_marker_scan_matches_legacy(&single_reference, "contents as one reference", &[MarkKind::Watermark], 0);
|
||||
|
||||
let mut text_only = fixture();
|
||||
let text = tagged_stream(&mut text_only, "Added", Some(Object::Dict(added_text_params())));
|
||||
set_page_contents(&mut text_only, text, None);
|
||||
let text_scan = assert_marker_scan_matches_legacy(&text_only, "one added text item", &[], 1);
|
||||
assert!(matches!(&text_scan.added[0].content, Content::Text(text) if text.text == "Added text"));
|
||||
|
||||
let mut indirect_params = fixture();
|
||||
let params = indirect_params.add(Object::Dict(added_text_params()));
|
||||
let text = tagged_stream(&mut indirect_params, "Added", Some(Object::Ref(params)));
|
||||
set_page_contents(&mut indirect_params, text, None);
|
||||
let indirect_scan = assert_marker_scan_matches_legacy(&indirect_params, "indirect PCAdded dictionary", &[], 1);
|
||||
assert!(matches!(&indirect_scan.added[0].content, Content::Text(text) if text.text == "Added text"));
|
||||
|
||||
let mut image_only = fixture();
|
||||
let image = image_only.add(Object::Stream(Stream::from_raw(Dict::new(), vec![0])));
|
||||
let image_item = tagged_stream(&mut image_only, "Added", Some(Object::Dict(added_image_params(image))));
|
||||
set_page_contents(&mut image_only, image_item, None);
|
||||
let image_scan = assert_marker_scan_matches_legacy(&image_only, "one added image item", &[], 1);
|
||||
assert!(matches!(&image_scan.added[0].content, Content::Image(item) if item.image == image));
|
||||
|
||||
let mut interleaved = fixture();
|
||||
let text = tagged_stream(&mut interleaved, "Added", Some(Object::Dict(added_text_params())));
|
||||
let watermark = tagged_stream(&mut interleaved, "Watermark", None);
|
||||
let image = interleaved.add(Object::Stream(Stream::from_raw(Dict::new(), vec![0])));
|
||||
let image_item = tagged_stream(&mut interleaved, "Added", Some(Object::Dict(added_image_params(image))));
|
||||
let header = tagged_stream(&mut interleaved, "HeaderFooter", None);
|
||||
set_page_contents(&mut interleaved, Object::Array(vec![text, watermark, image_item, header]), None);
|
||||
let interleaved_scan =
|
||||
assert_marker_scan_matches_legacy(&interleaved, "marks and added content interleaved", &[MarkKind::Watermark, MarkKind::HeaderFooter], 2);
|
||||
assert!(matches!(&interleaved_scan.added[0].content, Content::Text(_)));
|
||||
assert!(matches!(&interleaved_scan.added[1].content, Content::Image(_)));
|
||||
|
||||
let mut array_contents = fixture();
|
||||
let background = tagged_stream(&mut array_contents, "Background", None);
|
||||
let array_ref = array_contents.add(Object::Array(vec![background]));
|
||||
set_page_contents(&mut array_contents, Object::Ref(array_ref), None);
|
||||
assert_marker_scan_matches_legacy(&array_contents, "contents as an indirect array", &[MarkKind::Background], 0);
|
||||
|
||||
let mut malformed = fixture();
|
||||
let indirect_mark = malformed.add(Object::Dict(tagged_dictionary("HeaderFooter", None)));
|
||||
let missing_added = tagged_stream(&mut malformed, "Added", None);
|
||||
let invalid_added = tagged_stream(&mut malformed, "Added", Some(Object::String(pdfcraft_cos::PdfString::text("bad params"))));
|
||||
let unknown = tagged_stream(&mut malformed, "FutureKind", None);
|
||||
let direct_watermark = Object::Dict(tagged_dictionary("Watermark", None));
|
||||
let direct_added = Object::Dict(tagged_dictionary("Added", Some(Object::Dict(added_text_params()))));
|
||||
set_page_contents(
|
||||
&mut malformed,
|
||||
Object::Array(vec![
|
||||
Object::Null,
|
||||
Object::Int(7),
|
||||
Object::name("not-a-stream"),
|
||||
direct_watermark,
|
||||
Object::Ref(indirect_mark),
|
||||
missing_added,
|
||||
invalid_added,
|
||||
unknown,
|
||||
direct_added,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
assert_marker_scan_matches_legacy(
|
||||
&malformed,
|
||||
"malformed, non-stream, unknown and direct entries",
|
||||
&[MarkKind::Watermark, MarkKind::HeaderFooter],
|
||||
0,
|
||||
);
|
||||
|
||||
let mut shared = fixture();
|
||||
let added = tagged_stream(&mut shared, "Added", Some(Object::Dict(added_text_params())));
|
||||
let mark = tagged_stream(&mut shared, "Background", None);
|
||||
set_page_contents(&mut shared, Object::Array(vec![added.clone(), mark.clone(), added.clone(), mark]), None);
|
||||
let shared_scan = assert_marker_scan_matches_legacy(&shared, "repeated shared stream references", &[MarkKind::Background], 2);
|
||||
assert_eq!(shared_scan.added[0].obj, shared_scan.added[1].obj);
|
||||
|
||||
let mut user_unit = fixture();
|
||||
let added = tagged_stream(&mut user_unit, "Added", Some(Object::Dict(added_text_params())));
|
||||
set_page_contents(&mut user_unit, added, Some(Object::Int(2)));
|
||||
let unit_scan = assert_marker_scan_matches_legacy(&user_unit, "non-1.0 UserUnit", &[], 1);
|
||||
let Content::Text(text) = &unit_scan.added[0].content else { panic!() };
|
||||
assert_eq!(text.rect, [20.0, 40.0, 220.0, 80.0]);
|
||||
assert_eq!(text.size, 24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_text_and_images_are_page_content_that_stays_editable() {
|
||||
let mut doc = fixture();
|
||||
|
||||
@@ -2428,8 +2428,13 @@ impl Session {
|
||||
};
|
||||
e.cos.note_repair(line);
|
||||
}
|
||||
let marks = editor.as_ref().map(|e| pdfcraft_edit::marks_present(&e.cos)).unwrap_or_default();
|
||||
let added = editor.as_ref().map(|e| pdfcraft_edit::list_added(&e.cos)).unwrap_or_default();
|
||||
let (marks, added) = editor
|
||||
.as_ref()
|
||||
.map(|e| {
|
||||
let scan = pdfcraft_edit::scan_markers(&e.cos);
|
||||
(scan.marks, scan.added)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let links = editor.as_ref().map(|e| pdfcraft_annot::links::list(&e.cos)).unwrap_or_default();
|
||||
let sig_cache = Arc::new(pdfcraft_sign::DigestCache::default());
|
||||
let signatures = editor.as_ref().map(|e| signatures_of(&e.cos, &bytes, &self.trust, &sig_cache)).unwrap_or_default();
|
||||
@@ -2715,8 +2720,9 @@ impl Session {
|
||||
let mut form = pdfcraft_forms::fields(&editor.cos);
|
||||
xfa::mark_script_buttons(&editor.cos, &mut form);
|
||||
doc.form = Arc::new(form);
|
||||
doc.marks = pdfcraft_edit::marks_present(&editor.cos);
|
||||
doc.added = pdfcraft_edit::list_added(&editor.cos);
|
||||
let marker_scan = pdfcraft_edit::scan_markers(&editor.cos);
|
||||
doc.marks = marker_scan.marks;
|
||||
doc.added = marker_scan.added;
|
||||
doc.links = pdfcraft_annot::links::list(&editor.cos);
|
||||
doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache);
|
||||
doc.display = display_bytes(Some(editor), &bytes);
|
||||
|
||||
@@ -1327,6 +1327,56 @@ fn headers_footers_watermarks_and_backgrounds_show_update_and_remove() {
|
||||
assert_eq!(s.get(id).unwrap().can_undo(), Some("Remove background"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_populates_marks_and_added_from_the_combined_marker_scan() {
|
||||
let (mut source, source_id) = session_with(1);
|
||||
let mut header = HeaderFooter::default();
|
||||
header.text[0] = "Header".into();
|
||||
source.apply(source_id, Edit::AddHeaderFooter { pages: vec![0], settings: header, replace: false }).unwrap();
|
||||
source
|
||||
.apply(
|
||||
source_id,
|
||||
Edit::AddWatermark { pages: vec![0], settings: Watermark { text: "Draft".into(), ..Watermark::default() }, file: None, replace: false },
|
||||
)
|
||||
.unwrap();
|
||||
source
|
||||
.apply(
|
||||
source_id,
|
||||
Edit::AddBackground {
|
||||
pages: vec![0],
|
||||
settings: Background { color: [0.9, 0.9, 1.0], opacity: 1.0, ..Background::default() },
|
||||
replace: false,
|
||||
file: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
source
|
||||
.apply(
|
||||
source_id,
|
||||
Edit::AddText { page: 0, text: AddedText { rect: [20.0, 250.0, 180.0, 280.0], text: "Added note".into(), ..Default::default() } },
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = source.save_bytes(source_id).unwrap();
|
||||
let reference = pdfcraft_cos::Document::open(bytes.clone()).unwrap();
|
||||
let expected_marks = pdfcraft_edit::marks_present(&reference);
|
||||
let expected_added = pdfcraft_edit::list_added(&reference);
|
||||
source.mark_saved(source_id, bytes.clone(), None).unwrap();
|
||||
let refreshed = source.get(source_id).unwrap();
|
||||
assert_eq!(refreshed.marks, expected_marks);
|
||||
assert_eq!(refreshed.added, expected_added);
|
||||
let mut reopened = Session::new();
|
||||
let reopened_id = reopened.open("marked.pdf", None, bytes, None).unwrap();
|
||||
let opened = reopened.get(reopened_id).unwrap();
|
||||
assert_eq!(opened.marks, expected_marks);
|
||||
assert_eq!(opened.added, expected_added);
|
||||
assert_eq!(opened.marks.len(), 3);
|
||||
assert!(opened.marks.contains(&MarkKind::HeaderFooter));
|
||||
assert!(opened.marks.contains(&MarkKind::Watermark));
|
||||
assert!(opened.marks.contains(&MarkKind::Background));
|
||||
assert_eq!(opened.added.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_images_and_text_follow_the_working_file() {
|
||||
let (mut s, id) = session_with(2);
|
||||
|
||||
Reference in new issue
Block a user