mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 02:40:09 +00:00
fix(annot): a comment listed many times gets one display appearance
with_missing_appearances_cached checked each /Annots entry against the source document, which never gets the appearance, so an annotation listed K times added K appearance streams to the display copy. The engine writes that copy on open and after every edit, so the display bytes grew with the number of listings: 20,000 listings of one Ink comment in a 136 KB file came to 428 MB. Each annotation reference is now handled once per call, across all pages.
This commit is contained in:
@@ -1433,8 +1433,15 @@ pub fn with_missing_appearances(doc: &Document) -> Option<Document> {
|
||||
/// redrawing it. See [`AppearanceCache`] for when entries stay valid.
|
||||
pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCache) -> Option<Document> {
|
||||
let mut copy: Option<Document> = None;
|
||||
// An annotation listed more than once, on one page or several, is drawn once. `has_normal`
|
||||
// reads `doc`, which never gets the appearance, so every repeat used to add another stream to
|
||||
// the copy, and the display bytes grew with the number of listings.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for page in page_refs(doc).ok()? {
|
||||
for r in annots(doc, page).iter().filter_map(Object::as_ref) {
|
||||
if !seen.insert(r) {
|
||||
continue;
|
||||
}
|
||||
let d = annot_dict(doc, r);
|
||||
if matches!(d.name(b"Subtype"), None | Some(b"Link" | b"Widget" | b"Popup")) {
|
||||
continue;
|
||||
|
||||
@@ -1301,6 +1301,28 @@ fn cached_appearances_are_reused_while_annotations_are_unchanged() {
|
||||
assert_eq!(a, b, "the reused appearance is byte-identical");
|
||||
}
|
||||
|
||||
/// A comment that `/Annots` lists many times, and that a second page lists too, is drawn once in
|
||||
/// the display copy. Every listing used to add another appearance stream, so a short array of
|
||||
/// repeated references multiplied the display bytes the engine writes on each refresh.
|
||||
#[test]
|
||||
fn a_comment_listed_many_times_is_drawn_once_for_display() {
|
||||
let first = format!("<< /Type /Page /Parent 2 0 R /Annots [{}] >>", vec!["5 0 R"; 1_000].join(" "));
|
||||
let doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 612 792] >>",
|
||||
first.as_bytes(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [5 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] /C [1 0 0] >>",
|
||||
]);
|
||||
let mut cache = AppearanceCache::default();
|
||||
let display = with_missing_appearances_cached(&doc, &mut cache).expect("the square needs an appearance");
|
||||
assert_eq!(display.object_numbers().len(), doc.object_numbers().len() + 1, "one appearance stream, not one per listing");
|
||||
assert_eq!((cache.builds(), cache.hits()), (1, 0));
|
||||
assert!(list(&display, 0).iter().chain(&list(&display, 1)).all(|d| d.contains(b"AP")), "every listing shows it");
|
||||
with_missing_appearances_cached(&doc, &mut cache).expect("a refresh");
|
||||
assert_eq!((cache.builds(), cache.hits()), (1, 1), "a refresh reuses the cached stream once");
|
||||
}
|
||||
|
||||
/// #1908: a text box's font size changes in `/DA` (colour kept), the rich text follows, the box
|
||||
/// grows to hold the bigger text but never shrinks, and bad sizes and other subtypes are refused.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user