mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
doc_flatten with comments chose every non-widget annotation except Link and Popup, so a comments-only flatten baked in and deleted 3D, Screen, Movie, RichMedia, PrinterMark, TrapNet and Watermark annotations with their data (a 3D annotation's U3D model and default view). The comments branch now uses pdfcraft_annot::is_comment_subtype, the classifier the comment list uses; widgets follow `fields` only. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f0c275669a
commit
2bd6d1357d
@@ -63,9 +63,15 @@ enum Which {
|
||||
fn selected(doc: &Document, d: &Dict, which: Which) -> bool {
|
||||
let subtype = d.name(b"Subtype").unwrap_or_default();
|
||||
match which {
|
||||
// Comments are what the comment list calls comments: links, pop-ups and the non-markup
|
||||
// multimedia and print-production annotations (3D, Screen, Movie, RichMedia,
|
||||
// PrinterMark, TrapNet, Watermark) are neither comments nor fields, and stay (#3087).
|
||||
Which::Comments { comments, fields } => {
|
||||
let widget = subtype == b"Widget";
|
||||
!matches!(subtype, b"Link" | b"Popup") && (!widget || fields) && (widget || comments)
|
||||
if subtype == b"Widget" {
|
||||
fields
|
||||
} else {
|
||||
comments && pdfcraft_annot::is_comment_subtype(&String::from_utf8_lossy(subtype))
|
||||
}
|
||||
}
|
||||
Which::FillSign => !matches!(subtype, b"Link" | b"Popup" | b"Widget") && pdfcraft_annot::is_fill_sign(doc, d),
|
||||
}
|
||||
|
||||
@@ -4498,3 +4498,43 @@ fn removing_marks_strips_wrappers_that_closed_open_state() {
|
||||
assert_eq!(remove_marks(&mut doc, &[0], MarkKind::Watermark).unwrap(), 1);
|
||||
assert_eq!(streams(&doc, 0), ["q 2 0 0 2 0 0 cm"]);
|
||||
}
|
||||
|
||||
/// #3087: flattening comments leaves alone what isn't a comment: links, and the non-markup
|
||||
/// multimedia and print-production annotations (3D, Screen, Movie, RichMedia, PrinterMark,
|
||||
/// TrapNet, Watermark), which it used to bake into the page and delete with their data.
|
||||
/// Flattening fields only leaves them alone too; real comments are still flattened.
|
||||
#[test]
|
||||
fn flattening_comments_keeps_annotations_that_are_not_comments() {
|
||||
let kept = ["Widget", "Link", "3D", "Screen", "Movie", "RichMedia", "PrinterMark", "TrapNet", "Watermark"];
|
||||
let flattened = ["Text", "Square", "FileAttachment"];
|
||||
let all: Vec<&str> = flattened.iter().chain(kept.iter()).copied().collect();
|
||||
let form = "<< /Type /XObject /Subtype /Form /BBox [0 0 40 20] /Length 26 >>\nstream\n0 0 1 rg 0 0 40 20 re f\nendstream";
|
||||
let annots: Vec<String> = all
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| {
|
||||
format!("<< /Type /Annot /Subtype /{s} /Rect [{x} 700 {r} 720] /AP << /N 4 0 R >> /Contents ({s}) >>", x = 20 + 50 * i, r = 60 + 50 * i)
|
||||
})
|
||||
.collect();
|
||||
let refs: Vec<String> = (0..all.len()).map(|i| format!("{} 0 R", 6 + i)).collect();
|
||||
let page = format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Resources << >> /Contents 5 0 R /Annots [{}] >>", refs.join(" "));
|
||||
let content = stream("", " ");
|
||||
let mut objs: Vec<&str> = vec!["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", &page, form, &content];
|
||||
objs.extend(annots.iter().map(String::as_str));
|
||||
let left = |doc: &Document| -> Vec<String> {
|
||||
let p = &pdfcraft_model::pages(doc)[0];
|
||||
let Some(a) = p.dict.get(b"Annots").map(|a| doc.resolve(a)) else { return Vec::new() };
|
||||
a.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|o| doc.resolve(o).as_dict().and_then(|d| d.name(b"Subtype")).map(|s| String::from_utf8_lossy(s).into_owned()))
|
||||
.collect()
|
||||
};
|
||||
let mut doc = build(&objs);
|
||||
assert_eq!(flatten(&mut doc, &[0], true, false).unwrap(), flattened.len());
|
||||
assert_eq!(left(&reopen(&doc)), kept, "only the comments were flattened");
|
||||
let mut doc = build(&objs);
|
||||
assert_eq!(flatten(&mut doc, &[0], false, true).unwrap(), 1, "the widget");
|
||||
let without_widget: Vec<&str> = all.iter().copied().filter(|s| *s != "Widget").collect();
|
||||
assert_eq!(left(&reopen(&doc)), without_widget, "flattening fields touches no annotation that isn't a widget");
|
||||
}
|
||||
|
||||
@@ -4208,8 +4208,8 @@ milestone = "M5"
|
||||
status = "shipped"
|
||||
commands = ["comment.flatten"]
|
||||
tools = ["doc_flatten"]
|
||||
evidence = ["crates/edit/src/tests.rs::flattening_draws_appearances_into_the_page_and_removes_the_comments", "crates/edit/src/tests.rs::flattening_keeps_an_undrawn_reply_to_a_flattened_comment", "crates/engine/src/tests.rs::flattening_keeps_the_look_and_drops_the_objects", "crates/automation/tests/automation.rs::flattening_through_tools"]
|
||||
notes = "appearances become page content; pop-ups and replies removed; a comment or reply whose appearance cannot be drawn (a cloudy border, an unsupported subtype) stays as it is instead of being deleted"
|
||||
evidence = ["crates/edit/src/tests.rs::flattening_draws_appearances_into_the_page_and_removes_the_comments", "crates/edit/src/tests.rs::flattening_keeps_an_undrawn_reply_to_a_flattened_comment", "crates/engine/src/tests.rs::flattening_keeps_the_look_and_drops_the_objects", "crates/automation/tests/automation.rs::flattening_through_tools", "crates/edit/src/tests.rs::flattening_comments_keeps_annotations_that_are_not_comments"]
|
||||
notes = "appearances become page content; pop-ups and replies removed; a comment or reply whose appearance cannot be drawn (a cloudy border, an unsupported subtype) stays as it is instead of being deleted; annotations that are not comments (links, 3D, Screen, Movie, RichMedia, PrinterMark, TrapNet, Watermark) are left in place (#3087)"
|
||||
|
||||
[[feature]]
|
||||
id = "comment.migrate"
|
||||
|
||||
Reference in New Issue
Block a user