mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Flatten Fill & Sign marks when saving (#464)
* M5: flatten Fill & Sign marks when saving Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay. * M4: zoom the Organize page grid The page grid draws its pages at 50% to 300%: toolbar buttons with a percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the hidden page view's zoom while the grid shows). Thumbnails are re-rendered sharper in a few steps as the grid grows, capped so one document's thumbnails stay within 256 MB. The page under the pointer (or the first one in view) stays where it is across a zoom. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M4: zoomed grid pages are rendered at the size drawn A zoomed page grid stretched 132 pt thumbnails, and the stepped detail that was meant to help was capped by a memory budget, so a long document stayed blurry at any zoom. Pages drawn larger than their thumbnails now get a render at exactly the size drawn while they are in view; the thumbnail stands in until it arrives. Only pages in view keep one, so memory no longer depends on the page count, and thumbnails go back to their one fixed size. Co-Authored-By: Claude Opus 5.5 <[email protected]> * View: the middle button pans instead of drawing with markup tools egui's drag responses accept every pointer button, so on Windows and macOS a middle-button drag reached the page tools: with Draw (ink) selected it drew a stroke, and other tools cropped, selected or moved. Linux was unaffected because its middle-click auto-scroll already blocks page input. Outside Linux a middle-button drag that starts in the viewport (document or Organize Pages) now pans 1:1 while the button is held, with a Grabbing cursor, and page input is blocked from the press through the release. Linux keeps its latched auto-scroll. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M3: a short page gone to from the Pages panel stays current The current page was whichever page showed most of itself. Going to a short page (a cheque) above a tall one scrolls it to the top, where the tall page below shows more, so the current page and the Pages panel's highlight moved to the tall page instead of the one clicked. The page navigated to now stays current while it is wholly on screen. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M4: page operations from the Pages panel: context menu and drag to reorder Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on the selection or on the page clicked (which becomes the selection), as the organize grid does. Dragging thumbnails moves the selection, or the page grabbed, to the gap it is dropped on: a bar marks the gap, the page count follows the pointer, and the panel scrolls near its edges. The move is one undoable step; the moved pages stay selected and the current page follows its page. Documents that forbid assembly keep click-only thumbnails. Parity: view.thumbnail-context-ops is shipped, with what it still lacks. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped * M5: translate Fill & Sign flatten labels into Ukrainian The coverage test failed because the new preference strings had no Ukrainian entries. * Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591) #516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one rendering test. Both crates are layer 3, so `cargo xtask layers` (run in CI) rejects the sideways edge and main went red. The test now lives in crates/engine/tests/form_rotation.rs, where both crates are already dependencies, and forms drops the dev-dependency. The parity evidence points at the new location. Co-authored-by: Claude Opus 5.5 <[email protected]> * uk: the author's wording for the flatten setting --------- Co-authored-by: samehelhawary <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Cody Vogt <[email protected]> Co-authored-by: William Max & Co. <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
24 files changed
+503
-22
No files matched your search
@@ -1100,6 +1100,116 @@ fn flattening_keeps_the_look_and_drops_the_objects() {
|
||||
assert_eq!(s.get(id).unwrap().form.len(), 2);
|
||||
}
|
||||
|
||||
fn fill_item(shape: Shape, contents: &str) -> Edit {
|
||||
let style = Style::default_for(&shape);
|
||||
Edit::AddAnnotation(NewAnnotation { page: 0, shape, style, contents: contents.into(), author: "Ada".into() })
|
||||
}
|
||||
|
||||
fn comment_text(s: &Session, id: DocId) -> Vec<String> {
|
||||
let mut t: Vec<String> = s.get(id).unwrap().info.annotations.iter().map(|a| a.contents.clone().unwrap_or_default()).collect();
|
||||
t.sort();
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_keeps_other_comments_fields_and_undoes() {
|
||||
let (mut s, id) = session_with(1);
|
||||
s.apply(id, fill_item(Shape::Typewriter { rect: [20.0, 200.0, 120.0, 220.0], font_size: 10.0 }, "Hello")).unwrap();
|
||||
s.apply(id, fill_item(Shape::Mark { rect: [140.0, 200.0, 160.0, 220.0], mark: FillMark::Check }, "")).unwrap();
|
||||
s.apply(id, fill_item(Shape::Signature { strokes: vec![vec![[10.0, 40.0], [40.0, 70.0], [70.0, 40.0]]] }, "")).unwrap();
|
||||
s.apply(id, fill_item(Shape::TypedSignature { rect: [90.0, 40.0, 150.0, 70.0], contours: vec![vec![[0.1, 0.2], [0.5, 0.9], [0.9, 0.2]]] }, ""))
|
||||
.unwrap();
|
||||
let mut png = Vec::new();
|
||||
{
|
||||
let mut enc = png::Encoder::new(&mut png, 8, 8);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
let pixel = [40u8, 60, 80, 200];
|
||||
enc.write_header().unwrap().write_image_data(&pixel.repeat(64)).unwrap();
|
||||
}
|
||||
let image = SignatureImage::from_bytes(&png).unwrap();
|
||||
let info = s.get(id).unwrap().info.pages[0].clone();
|
||||
s.apply(id, image.edit(0, &info, [20.0, 120.0], false, "Ada").unwrap()).unwrap();
|
||||
s.apply(id, fill_item(Shape::TextMarkup { kind: Markup::Highlight, quads: vec![[20.0, 280.0, 80.0, 280.0, 20.0, 270.0, 80.0, 270.0]] }, "keep"))
|
||||
.unwrap();
|
||||
s.apply(id, fill_item(Shape::TextBox { rect: [20.0, 150.0, 120.0, 180.0], font_size: 12.0 }, "Note")).unwrap();
|
||||
s.apply(id, Edit::AddField { page: 0, rect: [20.0, 230.0, 120.0, 250.0], kind: NewField::Text { multiline: false }, name: Some("email".into()) })
|
||||
.unwrap();
|
||||
|
||||
s.apply(id, Edit::FlattenFillSign).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().can_undo(), Some("Flatten Fill & Sign"));
|
||||
assert_eq!(comment_text(&s, id), ["Note", "keep"]);
|
||||
assert_eq!(s.get(id).unwrap().form.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["email"]);
|
||||
s.undo(id).unwrap();
|
||||
assert!(comment_text(&s, id).contains(&"Hello".to_string()), "undo restores the typewriter");
|
||||
|
||||
s.apply(id, Edit::FlattenFillSign).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
s.mark_saved(id, saved.clone(), None).unwrap();
|
||||
let mut again = Session::new();
|
||||
let id2 = again.open("saved.pdf", None, saved, None).unwrap();
|
||||
assert_eq!(comment_text(&again, id2), ["Note", "keep"]);
|
||||
assert_eq!(again.get(id2).unwrap().form.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["email"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_does_nothing_when_there_are_none() {
|
||||
let (mut s, id) = session_with(1);
|
||||
s.apply(id, fill_item(Shape::TextMarkup { kind: Markup::Highlight, quads: vec![[20.0, 200.0, 80.0, 200.0, 20.0, 190.0, 80.0, 190.0]] }, "keep"))
|
||||
.unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
s.mark_saved(id, saved, None).unwrap();
|
||||
assert!(!s.get(id).unwrap().dirty);
|
||||
s.apply(id, Edit::FlattenFillSign).unwrap();
|
||||
assert!(!s.get(id).unwrap().dirty, "nothing was baked in, so the document stays saved");
|
||||
assert_eq!(s.get(id).unwrap().can_undo(), Some("Add highlight"), "no undo step for a flatten that changed nothing");
|
||||
assert_eq!(comment_text(&s, id), ["keep"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_is_refused_without_permission_to_change() {
|
||||
let (mut s, id) = session_with(1);
|
||||
s.apply(id, fill_item(Shape::Typewriter { rect: [20.0, 200.0, 120.0, 220.0], font_size: 10.0 }, "Hello")).unwrap();
|
||||
let p = Protection { printing: Printing::Low, changes: Changes::CommentFillSign, copy: false, ..protection(None, Some("boss")) };
|
||||
s.apply(id, Edit::Protect(p)).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
let mut other = Session::new();
|
||||
let id2 = other.open("locked.pdf", None, saved, None).unwrap();
|
||||
let before = other.get(id2).unwrap().bytes.clone();
|
||||
assert_eq!(other.apply(id2, Edit::FlattenFillSign), Err(EditError::NotPermitted("changes to the document")));
|
||||
assert_eq!(other.get(id2).unwrap().bytes, before);
|
||||
assert!(!other.get(id2).unwrap().dirty);
|
||||
assert!(comment_text(&other, id2).contains(&"Hello".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_on_a_signed_file_stays_incremental() {
|
||||
let p12 = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap();
|
||||
let digital_id = sign::pkcs12::open(&p12, "test").unwrap();
|
||||
let mut s = Session::new().with_clock(|| 1_800_000_000);
|
||||
let id = s.open("fixture.pdf", None, Arc::new(fixture(1)), None).unwrap();
|
||||
let opts = SignOptions { page: 0, rect: Some([20.0, 20.0, 160.0, 50.0]), ..SignOptions::default() };
|
||||
let signed = s.sign(id, &digital_id, opts).unwrap();
|
||||
s.mark_signed(id, signed.clone(), None).unwrap();
|
||||
s.apply(id, fill_item(Shape::Typewriter { rect: [20.0, 200.0, 120.0, 220.0], font_size: 10.0 }, "Hello")).unwrap();
|
||||
s.apply(id, Edit::FlattenFillSign).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
assert!(saved.starts_with(signed.as_ref()), "the signature's bytes stay an incremental update");
|
||||
assert!(!comment_text(&s, id).contains(&"Hello".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_fill_and_sign_reopens_an_encrypted_file() {
|
||||
let (mut s, id) = session_with(1);
|
||||
s.apply(id, fill_item(Shape::Typewriter { rect: [20.0, 200.0, 120.0, 220.0], font_size: 10.0 }, "Hello")).unwrap();
|
||||
s.apply(id, Edit::Protect(protection(Some("secret"), None))).unwrap();
|
||||
s.apply(id, Edit::FlattenFillSign).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
let mut again = Session::new();
|
||||
assert!(again.open("p.pdf", None, saved.clone(), None).is_err());
|
||||
let id2 = again.open("p.pdf", None, saved, Some("secret")).unwrap();
|
||||
assert!(!comment_text(&again, id2).contains(&"Hello".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_export_as_png_jpeg_and_tiff() {
|
||||
use crate::export::{Exporter, ImageFormat};
|
||||
|
||||
Reference in new issue
Block a user