From 5ad2ecfef9791bf3e7a91550d29435d55ace3a7f Mon Sep 17 00:00:00 2001 From: LloydNicholson <34131158+LloydNicholson@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:47:58 +0200 Subject: [PATCH] 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 Co-authored-by: Claude Opus 5.5 --- crates/annot/src/lib.rs | 3 + crates/engine/src/lib.rs | 1 + crates/render/src/inspect.rs | 24 +++++++ crates/ui-egui/src/canvas.rs | 5 +- crates/ui-egui/src/comments.rs | 30 +++++++- crates/ui-egui/src/signature_drag.rs | 2 +- crates/ui-egui/tests/fill_sign.rs | 101 +++++++++++++++++++++++++++ parity/acrobat-features.toml | 4 +- 8 files changed, 164 insertions(+), 6 deletions(-) diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 4e8aceb2..12107679 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -1784,6 +1784,8 @@ pub struct Summary { pub quads: Vec<[f32; 8]>, pub locked: bool, pub intent: Option, + /// A Fill & Sign mark ([`is_fill_sign`]). + pub fill_sign: bool, } fn text_value(doc: &Document, d: &Dict, key: &[u8]) -> Option { @@ -1854,6 +1856,7 @@ pub fn summaries(doc: &Document) -> Vec { quads, locked: annotation_flags(doc, d) & FLAG_LOCKED != 0, intent: d.get(b"IT").and_then(|o| doc.resolve(o).as_name().map(|n| String::from_utf8_lossy(n).into_owned())), + fill_sign: is_fill_sign(doc, d), }); } } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 1e2979d2..5ff685db 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1772,6 +1772,7 @@ fn comment_list(doc: &pdfcraft_cos::Document) -> Vec, + /// 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 { 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, }); } } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 5605292d..e5eaec2d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2179,8 +2179,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { stamp_placed = true; } } + // Over a mark already placed, the Fill & Sign tool picks it up (comments, below). if let QuickTool::Fill(ft) = tool && allowed + && !comments::fill_grabs(ui, &pcx, view) { match crate::fill_sign::page_input( ui, @@ -2268,7 +2270,8 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let on_link = tool == QuickTool::Link && can_modify && crate::link_ui::page_input(ui, &resp, &xf, i, info, &doc_links, view); let consumed = on_edit_text || on_link || on_content || boxing || on_field || comments::page_input(ui, &resp, &pcx, view); - let preview_target = (tool == QuickTool::Select && !comments_hidden).then_some(view.comments.selected).flatten(); + let preview_target = + (matches!(tool, QuickTool::Select | QuickTool::Fill(_)) && !comments_hidden).then_some(view.comments.selected).flatten(); view.signature_drag.prepare(ui.ctx(), doc, preview_target, scale); view.signature_drag.paint(painter, &pcx, &view.comments, view.pending_edit.as_ref()); diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index 1f195784..4de1e2c0 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -576,6 +576,7 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, let page_rect = cx.xf.rect; let pressed_here = origin.is_some_and(|o| page_rect.contains(o)); let over_page = pointer.is_some_and(|p| page_rect.contains(p)); + let fill_grab = fill_grabs(ui, cx, view); let cv = &mut view.comments; if cv.gesture.is_some() && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { cv.gesture = None; @@ -768,10 +769,35 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, true } QuickTool::Select => select_input(ui, resp, cx, view, pointer, origin, pressed_here), + QuickTool::Fill(_) if fill_grab => select_input(ui, resp, cx, view, pointer, origin, pressed_here), _ => false, } } +/// With a Fill & Sign tool, the Fill & Sign marks already on the page are picked up as with the +/// Select tool, as in Acrobat: hovering one shows the move cursor, a click selects it, a drag +/// moves it and the selected one's handles resize it. Elsewhere the tool places its mark. While +/// the button is down the press decides; otherwise the pointer (egui clears the press origin on +/// release, so a click is located by the pointer). +pub(crate) fn fill_grabs(ui: &egui::Ui, cx: &PageCx<'_>, view: &DocView) -> bool { + if !matches!(cx.tool, QuickTool::Fill(_)) || cx.hidden { + return false; + } + let cv = &view.comments; + if matches!(cv.gesture, Some(Gesture::Move { page, .. } | Gesture::Resize { page, .. }) if page == cx.page) { + return true; + } + let (pointer, origin, down) = ui.input(|i| (i.pointer.hover_pos(), i.pointer.press_origin(), i.pointer.any_down())); + let Some(p) = (if down { origin } else { pointer }).filter(|p| cx.xf.rect.contains(*p)) else { return false }; + let on_handle = cv + .selected + .filter(|(page, _)| *page == cx.page) + .and_then(|(_, i)| cx.get(i)) + .filter(|a| a.fill_sign && cx.allowed && resizable(a)) + .is_some_and(|a| HANDLES.into_iter().any(|h| handle_pos(cx.screen_rect(a), h).distance(p) <= 7.0)); + on_handle || cx.hit(p).is_some_and(|a| a.fill_sign) +} + fn clamp_to(r: Rect, p: Pos2) -> Pos2 { pos2(p.x.clamp(r.left(), r.right()), p.y.clamp(r.top(), r.bottom())) } @@ -917,9 +943,9 @@ pub(crate) fn page_after_text(resp: &egui::Response, cx: &PageCx<'_>, view: &mut pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>, view: &DocView) { let cv = &view.comments; let pointer = ui.input(|i| i.pointer.hover_pos()); - if cx.tool == QuickTool::Select - && cv.gesture.is_none() + if cv.gesture.is_none() && let Some(a) = pointer.and_then(|p| cx.hit(p)) + && (cx.tool == QuickTool::Select || matches!(cx.tool, QuickTool::Fill(_)) && a.fill_sign) && cv.selected != Some((cx.page, a.index)) { for r in cx.screen_rects(a) { diff --git a/crates/ui-egui/src/signature_drag.rs b/crates/ui-egui/src/signature_drag.rs index b65d6cc6..fb683f1f 100644 --- a/crates/ui-egui/src/signature_drag.rs +++ b/crates/ui-egui/src/signature_drag.rs @@ -124,7 +124,7 @@ impl SignatureDrag { pub fn paint(&self, painter: &egui::Painter, cx: &PageCx<'_>, cv: &CommentView, pending: Option<&Edit>) { let (Some((_, page, index)), Some(layers)) = (self.key, &self.layers) else { return }; - if page != cx.page || cx.tool != QuickTool::Select || cx.hidden { + if page != cx.page || !matches!(cx.tool, QuickTool::Select | QuickTool::Fill(_)) || cx.hidden { return; } let dragging = matches!(cv.gesture, Some(Gesture::Move { page: p, index: i, .. } | Gesture::Resize { page: p, index: i, .. }) if (p, i) == (page, index)); diff --git a/crates/ui-egui/tests/fill_sign.rs b/crates/ui-egui/tests/fill_sign.rs index 96df83af..6f25b13c 100644 --- a/crates/ui-egui/tests/fill_sign.rs +++ b/crates/ui-egui/tests/fill_sign.rs @@ -1016,3 +1016,104 @@ fn drawn_and_typed_signatures_stay_upright_on_rotated_pages() { assert!(differ * 10 < ink, "rotation {rotation}: {differ} of {ink} ink pixels differ from the unturned page"); } } + +/// Press, move in steps and release, from one user-space point to another. +fn drag(h: &mut Harness<'static, PdfCraftApp>, from: (f32, f32), to: (f32, f32)) { + let (a, b) = (at(h, from.0, from.1), at(h, to.0, to.1)); + h.hover_at(a); + h.run_steps(1); + h.drag_at(a); + h.run_steps(1); + for k in 1..=5 { + h.hover_at(a + (b - a) * (k as f32 / 5.0)); + h.run_steps(1); + } + h.drop_at(b); + h.run_steps(4); +} + +fn rects(h: &Harness<'static, PdfCraftApp>) -> Vec<[f32; 4]> { + let s = h.state(); + s.session.get(s.views[0].id).unwrap().info.annotations.iter().map(|a| a.rect).collect() +} + +/// A placed signature moves while a Fill & Sign tool is still active (as in Acrobat), instead of +/// the drag doing nothing and a click stamping another mark on top of it. Elsewhere on the page +/// the tool still places its mark. +#[test] +fn fill_and_sign_tools_pick_up_placed_marks_to_move_and_resize_them() { + for tool in ["sign.fill.signature", "sign.fill.initials", "sign.fill.text", "sign.fill.check", "sign.fill.date"] { + let mut h = harness(); + h.state_mut().signature = Some(SavedSig::Typed("Ada Lovelace".into())); + h.state_mut().initials = Some(SavedSig::Typed("AL".into())); + h.state_mut().execute("sign.fill.signature"); + click(&mut h, 40.0, 250.0); + let [placed] = rects(&h)[..] else { panic!("one signature: {:?}", rects(&h)) }; + assert!(h.state_mut().execute(tool)); + h.run_steps(2); + let mid = ((placed[0] + placed[2]) / 2.0, (placed[1] + placed[3]) / 2.0); + h.hover_at(at(&h, mid.0, mid.1)); + h.run_steps(2); + assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Move, "{tool}: a placed mark can be picked up"); + + drag(&mut h, mid, (mid.0 + 40.0, mid.1 - 60.0)); + assert!(matches!(h.state().quick_tool, QuickTool::Fill(_)), "{tool}: the tool stays"); + let moved = [placed[0] + 40.0, placed[1] - 60.0, placed[2] + 40.0, placed[3] - 60.0]; + let [now] = rects(&h)[..] else { panic!("{tool}: still one mark: {:?}", rects(&h)) }; + assert_rect(now, moved); + let doc = h.state().session.get(h.state().views[0].id).unwrap(); + assert_eq!(doc.can_undo(), Some("Move comment"), "{tool}"); + assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "{tool}"); + + // The selected signature's handle resizes it. + let corner = at(&h, moved[2], moved[1]); + h.hover_at(corner); + h.run_steps(2); + h.drag_at(corner); + h.run_steps(1); + h.hover_at(corner + egui::vec2(30.0, 10.0)); + h.run_steps(2); + h.drop_at(corner + egui::vec2(30.0, 10.0)); + h.run_steps(3); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Resize comment"), "{tool}"); + assert_eq!(rects(&h).len(), 1, "{tool}"); + + // A click on it selects it rather than placing another mark on top. + h.state_mut().views[0].comments.selected = None; + let r = rects(&h)[0]; + click(&mut h, (r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); + assert_eq!(rects(&h).len(), 1, "{tool}: no mark stamped on the signature"); + assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "{tool}"); + assert!(h.state().views[0].fill_text.is_none(), "{tool}: no text box opened on the signature"); + } + // A drawn signature in a saved and reopened file (read by the viewer, not the editor). + let mut h = harness(); + h.state_mut().signature = Some(SavedSig::Drawn(vec![vec![[0.05, 0.1], [0.3, 0.2], [0.55, 0.05], [0.95, 0.15]]])); + h.state_mut().execute("sign.fill.signature"); + click(&mut h, 40.0, 250.0); + let saved = h.state().session.save_bytes(h.state().views[0].id).unwrap(); + let mut h = harness_bytes(&saved); + let [placed] = rects(&h)[..] else { panic!("one signature: {:?}", rects(&h)) }; + h.state_mut().execute("sign.fill.check"); + let mid = ((placed[0] + placed[2]) / 2.0, (placed[1] + placed[3]) / 2.0); + drag(&mut h, mid, (mid.0 + 40.0, mid.1 - 60.0)); + assert_rect(rects(&h)[0], [placed[0] + 40.0, placed[1] - 60.0, placed[2] + 40.0, placed[3] - 60.0]); + + // Away from Fill & Sign marks, the tool still places (checks stay repeatable), also on other + // comments. + let mut h = harness(); + h.state_mut().signature = Some(SavedSig::Typed("Ada Lovelace".into())); + h.state_mut().execute("sign.fill.signature"); + click(&mut h, 40.0, 250.0); + let id = h.state().views[0].id; + let square = pdfcraft_engine::Shape::Rectangle { rect: [100.0, 80.0, 200.0, 140.0] }; + let style = pdfcraft_engine::Style::default_for(&square); + let new = pdfcraft_engine::NewAnnotation { page: 0, shape: square, style, contents: String::new(), author: "Ada".into() }; + h.state_mut().session.apply(id, pdfcraft_engine::Edit::AddAnnotation(new)).unwrap(); + h.run_steps(2); + h.state_mut().execute("sign.fill.check"); + click(&mut h, 60.0, 100.0); + click(&mut h, 150.0, 110.0); + assert_eq!(items(&h).len(), 4, "{:?}", items(&h)); + assert_eq!(h.state().quick_tool, QuickTool::Fill(FillTool::Check)); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e55bff06..f95e15a5 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5226,8 +5226,8 @@ milestone = "M5" status = "shipped" commands = ["sign.fill.signature", "sign.fill.initials", "sign.fill.signature.change", "sign.fill.initials.change", "sign.fill.signature.remove", "sign.fill.initials.remove"] tools = ["fill_sign_add", "comment_edit", "comment_image_preview"] -evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save"] -notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 4 megapixels and 4096 pixels per side" +evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save", "crates/ui-egui/tests/fill_sign.rs::fill_and_sign_tools_pick_up_placed_marks_to_move_and_resize_them"] +notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; while a Fill & Sign tool is active, marks already placed are picked up as with Select (hover shows the move cursor, a click selects, a drag moves, the selected mark's handles resize) and the tool places only away from them; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 4 megapixels and 4096 pixels per side" [[feature]] id = "form.fill-sign-flatten"