LaTeX `animate` PDFs store each frame as a read-only push button and drive them from a Screen annotation. PdfCraft showed "This document contains N interactive form fields." because the bar counted any field, and listed the Screen annotation in the Comments panel because only Link, Widget and Popup were filtered out. - The form-fields bar now needs at least one field that isn't a push button (something to fill in). - Non-markup annotations (ISO 32000-2 §12.5.6: Screen, Movie, RichMedia, 3D, PrinterMark, TrapNet, Watermark) are no longer comments, both when a document is inspected and when the comment list is rebuilt after an edit (new pdfcraft_annot::is_comment_subtype). Playing the animation itself (page actions, timers) is out of scope here. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
90bb2f14ad
commit
275cbcf323
|
|
@ -1793,7 +1793,14 @@ fn text_value(doc: &Document, d: &Dict, key: &[u8]) -> Option<String> {
|
|||
(!s.is_empty()).then_some(s)
|
||||
}
|
||||
|
||||
/// Every comment (not links, form widgets or pop-ups), ordered by page and then top edge.
|
||||
/// Whether an annotation subtype is a comment. Links, form widgets, pop-ups and the non-markup
|
||||
/// multimedia and print-production annotations (ISO 32000-2 §12.5.6: Screen, Movie, RichMedia,
|
||||
/// 3D, PrinterMark, TrapNet, Watermark) are not.
|
||||
pub fn is_comment_subtype(subtype: &str) -> bool {
|
||||
!matches!(subtype, "Link" | "Widget" | "Popup" | "Screen" | "Movie" | "RichMedia" | "3D" | "PrinterMark" | "TrapNet" | "Watermark")
|
||||
}
|
||||
|
||||
/// Every comment (see [`is_comment_subtype`]), ordered by page and then top edge.
|
||||
pub fn summaries(doc: &Document) -> Vec<Summary> {
|
||||
let mut out = Vec::new();
|
||||
let Ok(pages) = page_refs(doc) else { return out };
|
||||
|
|
@ -1802,7 +1809,7 @@ pub fn summaries(doc: &Document) -> Vec<Summary> {
|
|||
let obj = doc.resolve(entry);
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
let Some(subtype) = d.name(b"Subtype").map(|s| String::from_utf8_lossy(s).into_owned()) else { continue };
|
||||
if matches!(subtype.as_str(), "Link" | "Widget" | "Popup") {
|
||||
if !is_comment_subtype(&subtype) {
|
||||
continue;
|
||||
}
|
||||
let nums = |k: &[u8]| -> Vec<f32> {
|
||||
|
|
|
|||
|
|
@ -919,3 +919,14 @@ fn redraw_keeps_shared_annotation_appearances_and_drops_stale_alternates() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_markup_annotations_are_not_comments() {
|
||||
// #169: a LaTeX `animate` player's Screen annotation showed up in the Comments list.
|
||||
for s in ["Link", "Widget", "Popup", "Screen", "Movie", "RichMedia", "3D", "PrinterMark", "TrapNet", "Watermark"] {
|
||||
assert!(!is_comment_subtype(s), "{s}");
|
||||
}
|
||||
for s in ["Text", "FreeText", "Highlight", "Ink", "Stamp", "FileAttachment", "Redact", "Sound"] {
|
||||
assert!(is_comment_subtype(s), "{s}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -815,7 +815,11 @@ impl<'a> Inspector<'a> {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if matches!(subtype.as_str(), "Widget" | "Popup") {
|
||||
// Not comments: form widgets, pop-ups and non-markup annotations such as the
|
||||
// Screen annotation that drives a LaTeX `animate` player (keep in step with
|
||||
// `pdfcraft_annot::is_comment_subtype`).
|
||||
if matches!(subtype.as_str(), "Widget" | "Popup" | "Screen" | "Movie" | "RichMedia" | "3D" | "PrinterMark" | "TrapNet" | "Watermark")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let rect = rect4(self.resolve(d.get(b"Rect").unwrap_or(&Object::Null)));
|
||||
|
|
|
|||
|
|
@ -2878,7 +2878,8 @@ fn notices(
|
|||
tl!("XFA form: its fields and its XFA data are kept in step, so other viewers show what you fill in.").to_string(),
|
||||
true,
|
||||
))
|
||||
} else if !info.fields.is_empty() {
|
||||
} else if info.fields.iter().any(|f| f.kind != pdfcraft_render::FieldKind::PushButton) {
|
||||
// Push buttons alone (LaTeX `animate` frames, navigation buttons) leave nothing to fill in.
|
||||
Some((
|
||||
"text-cursor-input",
|
||||
crate::i18n::fmt(tl!("This document contains {n} interactive form fields."), &[("n", &info.fields.len().to_string())]),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
//! LaTeX `animate` PDFs (#169): every field is a push button holding a frame and a Screen
|
||||
//! annotation drives the player. Nothing there can be filled in, so the form-fields bar stays
|
||||
//! away, and the Screen annotation is not a comment.
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
/// A one-page PDF whose page has `/Annots [6 0 R …]` for each of `annots` (objects from 6 on)
|
||||
/// and whose AcroForm lists the widgets among them.
|
||||
fn pdf(annots: &[&str]) -> Vec<u8> {
|
||||
let refs: Vec<String> = (0..annots.len()).map(|i| format!("{} 0 R", i + 6)).collect();
|
||||
let fields: Vec<String> = annots.iter().enumerate().filter(|(_, a)| a.contains("/Widget")).map(|(i, _)| format!("{} 0 R", i + 6)).collect();
|
||||
let mut objs: Vec<String> = vec![
|
||||
format!("<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [{}] >> >>", fields.join(" ")),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Annots [{}] >>", refs.join(" ")),
|
||||
"<< /Length 0 >>\nstream\n\nendstream".into(),
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Length 0 >>\nstream\n\nendstream".into(),
|
||||
];
|
||||
objs.extend(annots.iter().map(|o| o.to_string()));
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn open(bytes: Vec<u8>) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 800.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("animate.pdf", None, bytes).expect("opens");
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
/// A frame of the player: a read-only push button (`/Ff 65537`), hidden unless `shown`.
|
||||
fn frame(name: &str, shown: bool) -> String {
|
||||
let hidden = if shown { 4 } else { 6 };
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65537 /T ({name}) /F {hidden} /Rect [50 50 250 150] /P 3 0 R /AP << /N 5 0 R >> >>")
|
||||
}
|
||||
|
||||
const SCREEN: &str = "<< /Type /Annot /Subtype /Screen /Rect [50 50 250 150] /P 3 0 R \
|
||||
/AA << /PO << /S /JavaScript /JS (app.setInterval\\('a0_gotoNext\\(\\)', 500\\);) >> >> >>";
|
||||
|
||||
#[test]
|
||||
fn push_buttons_alone_show_no_form_bar_and_a_screen_annotation_is_not_a_comment() {
|
||||
let (a, b, c) = (frame("0.0", true), frame("0.1", false), frame("0.2", false));
|
||||
let h = open(pdf(&[&a, &b, &c, SCREEN]));
|
||||
{
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(doc.info.fields.len(), 3, "the frames are still fields");
|
||||
assert!(doc.info.fields.iter().all(|f| f.kind == pdfcraft_render::FieldKind::PushButton));
|
||||
assert!(doc.info.annotations.is_empty(), "no comments: {:?}", doc.info.annotations);
|
||||
}
|
||||
assert!(h.query_by_label_contains("interactive form fields").is_none(), "nothing to fill in, so no form bar");
|
||||
assert!(h.query_by_label("Highlight fields").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fillable_field_beside_push_buttons_still_shows_the_form_bar() {
|
||||
let button = frame("0.0", true);
|
||||
let text = "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [20 160 200 180] /P 3 0 R >>";
|
||||
let h = open(pdf(&[&button, text, SCREEN]));
|
||||
h.get_by_label_contains("This document contains 2 interactive form fields.");
|
||||
h.get_by_label("Highlight fields");
|
||||
}
|
||||
Loading…
Reference in New Issue