M5.7: Fill & Sign tools pick up placed signatures to move and resize them (#822)

A signature placed with Fill & Sign could only be moved with the Select
tool. While a Fill & Sign tool was active (Sign, Initials, Add text,
Check, Date...), hovering the signature showed the placement crosshair,
a drag did nothing and a click stamped another mark on top of it.

Now, as in Acrobat, a Fill & Sign tool picks up the Fill & Sign marks
already on the page: hovering one shows the move cursor, a click selects
it, a drag moves it and the selected mark's handles resize it. Away from
them the tool places as before, including on other comments. Image
signatures keep their live move/resize preview.

Comments now carry a fill_sign flag (pdfcraft_annot::is_fill_sign for
the engine's list, the same rules in the viewer's inspector), so this
also works on reopened files.

Co-authored-by: lloyd <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-10 15:47:58 -04:00
1 parent c49148a290
commit 5ad2ecfef9
8 files changed
+164 -6

No files matched your search

+24
View File
@@ -264,6 +264,9 @@ pub struct Annotation {
pub locked: bool,
/// `/IT`, the intent: `FreeTextCallout`, `PolygonCloud`, `FreeTextTypeWriter`…
pub intent: Option<String>,
/// A Fill & Sign mark: typed text, a check, cross, dot or line, or a typed, drawn or picture
/// signature or initials (the rules of `pdfcraft_annot::is_fill_sign`).
pub fill_sign: bool,
}
impl Annotation {
@@ -553,6 +556,25 @@ impl<'a> Inspector<'a> {
self.resolve(d.get(key).ok()?).as_name().ok().map(|n| String::from_utf8_lossy(n).into_owned())
}
/// Whether an annotation is a Fill & Sign mark. Keep in step with
/// `pdfcraft_annot::is_fill_sign`, which the engine's comment list uses.
fn is_fill_sign(&self, d: &Dictionary, subtype: &str) -> bool {
let yes = |key: &[u8]| matches!(d.get(key).map(|o| self.resolve(o)), Ok(Object::Boolean(true)));
if yes(b"PCFillSign") {
return true;
}
match subtype {
"FreeText" => self.name(d, b"IT").as_deref() == Some("FreeTextTypeWriter"),
"Stamp" => match self.name(d, b"Name").as_deref() {
Some("PCCheck" | "PCCross" | "PCDot" | "PCLine" | "PCTypedSignature") => true,
Some("PCCustomSignature" | "PCCustomInitials") => yes(b"PCPictureImage"),
_ => false,
},
"Ink" => self.text(d, b"Subj").as_deref() == Some("Signature"),
_ => false,
}
}
fn page_of(&self, o: &Object) -> Option<usize> {
match o {
Object::Reference(id) => self.page_index.get(id).copied(),
@@ -871,6 +893,7 @@ impl<'a> Inspector<'a> {
_ => Vec::new(),
};
let in_reply_to = d.get(b"IRT").ok().and_then(|o| self.dict(o)).and_then(|p| self.text(p, b"NM"));
let fill_sign = self.is_fill_sign(d, &subtype);
info.annotations.push(Annotation {
page,
subtype,
@@ -886,6 +909,7 @@ impl<'a> Inspector<'a> {
quads,
locked: d.get(b"F").ok().and_then(|f| self.resolve(f).as_i64().ok()).unwrap_or(0) & 128 != 0,
intent: self.name(d, b"IT"),
fill_sign,
});
}
}