From 3641801f131f4740f011cf4b6f63a1efca435b16 Mon Sep 17 00:00:00 2001 From: Combiler01 Date: Fri, 23 Oct 2026 14:49:01 +0530 Subject: [PATCH] Fixes: Refactored System not opening for Added/Pasted Images --- crates/engine/src/lib.rs | 111 +++++++++++++++-------------- crates/ui-egui/src/content_ui.rs | 47 +++++++++--- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/tests/it/editing.rs | 59 +++++++++++++++ 4 files changed, 158 insertions(+), 61 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 364467bbb1..5b6a026db3 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -2694,7 +2694,6 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ImageEdit::Arrange(order) => pdfcraft_edit::ImageChange::Arrange(*order), ImageEdit::RemoveCrop => pdfcraft_edit::ImageChange::RemoveCrop, }; - pdfcraft_edit::change_image(doc, *page, *index, &c)?; let page_obj = pdfcraft_model::pages(doc).into_iter().nth(*page); let target_stream_obj = page_obj.as_ref().and_then(|p| match p.dict.get(b"Contents") { None => None, @@ -2713,12 +2712,21 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> } else { None }; + + let mut ai_to_update = None; if let Some((added_idx, added_item)) = matched_added - && let AddedContent::Image(mut ai) = added_item.content.clone() + && let AddedContent::Image(ai) = added_item.content.clone() { - let added_idx = *added_idx; - let has_active_crop = img.clip_rect.is_some() || img.visible_rect.is_some_and(|v| v != img.rect); + let has_active_crop = ai.crop.iter().any(|v| v.abs() > 1e-6) + || img.visible_rect.is_some_and(|v| { + (v[0] - img.rect[0]).abs() > 1e-4 + || (v[1] - img.rect[1]).abs() > 1e-4 + || (v[2] - img.rect[2]).abs() > 1e-4 + || (v[3] - img.rect[3]).abs() > 1e-4 + }); + let rotated_transform = matches!(change, ImageEdit::Transform(t) if t[1].abs() >= 1e-6 || t[2].abs() >= 1e-6); let should_promote = has_active_crop + || rotated_transform || matches!( change, ImageEdit::Crop(_) | ImageEdit::RemoveCrop | ImageEdit::RotateBy(_) | ImageEdit::Arrange(_) | ImageEdit::Delete @@ -2730,56 +2738,55 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> d.remove(b"PCMark"); }); } else { - match change { - ImageEdit::Move(to) => { - ai.rect = *to; - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } - ImageEdit::Transform(t) => { - if t[1].abs() < 1e-6 && t[2].abs() < 1e-6 { - let apply = |x: f64, y: f64| (t[0] * x + t[2] * y + t[4], t[1] * x + t[3] * y + t[5]); - let pts = [ - apply(ai.rect[0], ai.rect[1]), - apply(ai.rect[2], ai.rect[1]), - apply(ai.rect[0], ai.rect[3]), - apply(ai.rect[2], ai.rect[3]), - ]; - let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN]; - for (x, y) in pts { - b = [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)]; - } - ai.rect = b; - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } else { - let _ = doc.update_dict(added_item.obj, |d| { - d.remove(b"PCAdded"); - d.remove(b"PCMark"); - }); - } - } - ImageEdit::Rotate(q) => { - let k = ((4 - q.rem_euclid(4)) % 4) as u8; - ai.rotation = (ai.rotation + k) % 4; - let r = ai.rect; - let (cx, cy) = ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); - if k % 2 == 1 { - let (w, h) = (r[2] - r[0], r[3] - r[1]); - ai.rect = [cx - h / 2.0, cy - w / 2.0, cx + h / 2.0, cy + w / 2.0]; - } - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } - ImageEdit::Flip { horizontal: true } => { - ai.flip_h = !ai.flip_h; - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } - ImageEdit::Flip { horizontal: false } => { - ai.flip_v = !ai.flip_v; - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } - _ => {} - } + ai_to_update = Some((*added_idx, ai)); } } + + if let Some((added_idx, mut ai)) = ai_to_update { + match change { + ImageEdit::Move(to) => { + ai.rect = *to; + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } + ImageEdit::Transform(t) => { + let apply = |x: f64, y: f64| (t[0] * x + t[2] * y + t[4], t[1] * x + t[3] * y + t[5]); + let pts = [ + apply(ai.rect[0], ai.rect[1]), + apply(ai.rect[2], ai.rect[1]), + apply(ai.rect[0], ai.rect[3]), + apply(ai.rect[2], ai.rect[3]), + ]; + let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN]; + for (x, y) in pts { + b = [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)]; + } + ai.rect = b; + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } + ImageEdit::Rotate(q) => { + let k = ((4 - q.rem_euclid(4)) % 4) as u8; + ai.rotation = (ai.rotation + k) % 4; + let r = ai.rect; + let (cx, cy) = ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); + if k % 2 == 1 { + let (w, h) = (r[2] - r[0], r[3] - r[1]); + ai.rect = [cx - h / 2.0, cy - w / 2.0, cx + h / 2.0, cy + w / 2.0]; + } + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } + ImageEdit::Flip { horizontal: true } => { + ai.flip_h = !ai.flip_h; + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } + ImageEdit::Flip { horizontal: false } => { + ai.flip_v = !ai.flip_v; + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } + _ => {} + } + } else { + pdfcraft_edit::change_image(doc, *page, *index, &c)?; + } } Edit::MoveObjects { page, objects, offset } => pdfcraft_edit::move_objects(doc, *page, objects, *offset)?, Edit::EditTextBlock { page, block, text, style } => { diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index 79482ce129..e56216b9ef 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -267,7 +267,10 @@ fn on_page(added: &[Added], page: usize) -> Vec<(usize, &Added)> { } pub(crate) fn hits_added(xf: &PageXform, info: &DocInfo, page: usize, added: &[Added], point: Pos2) -> bool { - on_page(added, page).iter().any(|(_, a)| screen_rect(xf, info, page, a.content.rect()).expand(2.0).contains(point)) + on_page(added, page) + .iter() + .filter(|(_, a)| matches!(a.content, AddedContent::Text(_))) + .any(|(_, a)| screen_rect(xf, info, page, a.content.rect()).expand(2.0).contains(point)) } fn handles(r: Rect) -> [(Pos2, bool, bool); 4] { @@ -310,7 +313,7 @@ pub(crate) fn select_grabs(ui: &egui::Ui, xf: &PageXform, page: usize, info: &Do let Some(p) = ui.input(|i| i.pointer.hover_pos().or(i.pointer.interact_pos())).filter(|p| xf.rect.contains(*p)) else { return false; }; - on_page(added, page).into_iter().any(|(index, item)| { + on_page(added, page).into_iter().filter(|(_, item)| matches!(item.content, AddedContent::Text(_))).any(|(index, item)| { let rect = screen_rect(xf, info, page, item.content.rect()); rect.expand(2.0).contains(p) || cv.selected == Some((page, index)) && handles(rect).into_iter().any(|(corner, _, _)| corner.distance(p) <= 6.0) @@ -344,11 +347,10 @@ pub(crate) fn page_input( let end = pointer.unwrap_or(start); if let Some((sp, si)) = cv.selected && sp == page - && let Some((_, a)) = on_page(added, page).into_iter().find(|(i, _)| *i == si) + && let Some((_, a)) = on_page(added, page).into_iter().find(|(i, a)| *i == si && matches!(a.content, AddedContent::Text(_))) && (end - start).length() >= 1.0 { - let keep = matches!(a.content, AddedContent::Image(_)); - let r = dragged(screen_rect(xf, info, page, a.content.rect()), grab, end - start, keep); + let r = dragged(screen_rect(xf, info, page, a.content.rect()), grab, end - start, false); let (p0, p1) = (to_display(xf, info, page, r.left_bottom()), to_display(xf, info, page, r.right_top())); let mut rect = [p0[0], p0[1], p1[0], p1[1]]; if let AddedContent::Text(t) = &a.content @@ -363,7 +365,7 @@ pub(crate) fn page_input( return consumed; } let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) else { return consumed }; - let items = on_page(added, page); + let items: Vec<(usize, &Added)> = on_page(added, page).into_iter().filter(|(_, a)| matches!(a.content, AddedContent::Text(_))).collect(); let hit = items.iter().rev().find(|(_, a)| screen_rect(xf, info, page, a.content.rect()).expand(2.0).contains(p)).map(|(i, a)| (*i, *a)); let selected_rect = cv .selected @@ -455,6 +457,9 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, xf: &PageXform, let cv = &view.content; let pointer = ui.input(|i| i.pointer.hover_pos()); for (i, a) in on_page(added, page) { + if matches!(a.content, AddedContent::Image(_)) { + continue; + } if cv.draft.as_ref().is_some_and(|d| d.page == page && d.index == Some(i)) { continue; } @@ -815,9 +820,22 @@ impl crate::PdfCraftApp { pub fn add_image(&mut self, name: String, bytes: Vec) { let Some((i, id)) = self.active_ids() else { return }; let page = self.views[i].current; - let before = self.session.get(id).map_or(0, |d| d.added().iter().filter(|a| a.page == page).count()); + let before_img = self.session.get(id).map_or(0, |d| d.page_images(page).len()); + let before_added = self.session.get(id).map_or(0, |d| d.added().iter().filter(|a| a.page == page).count()); if self.apply_edit(Edit::AddImage { page, rect: None, name, bytes: std::sync::Arc::new(bytes) }) { - self.views[i].content.select_added = Some((page, before)); + self.views[i].content.selected = Some((page, before_added)); + self.views[i].content.select_added = Some((page, before_added)); + self.views[i].image_selection = Some(crate::edit_text_ui::ImageSelection { + page, + index: before_img, + crop_mode: false, + crop_rect: None, + drag: None, + crop: None, + turning: false, + settled: None, + }); + self.quick_tool = crate::QuickTool::EditText; self.left = crate::LeftPanel::Tool("edit"); self.left_open = true; } @@ -830,9 +848,16 @@ impl crate::PdfCraftApp { pub(crate) fn paste_key_released(&mut self, ctx: &egui::Context) -> bool { let mut released = false; ctx.input(|inp| { + if inp.modifiers.command { + self.last_command_time = Some(inp.time); + } for e in &inp.events { if let egui::Event::Key { key: egui::Key::V, pressed, modifiers, .. } = e { - released |= !*pressed && !self.v_pressed && modifiers.command; + if modifiers.command { + self.last_command_time = Some(inp.time); + } + let command_active = modifiers.command || self.last_command_time.is_some(); + released |= !*pressed && !self.v_pressed && command_active; self.v_pressed = *pressed; } } @@ -1022,6 +1047,7 @@ impl crate::PdfCraftApp { } /// What the image tools ask for. +#[allow(dead_code)] pub(crate) enum ImageAction { Update(pdfcraft_engine::AddedContent), Replace, @@ -1029,10 +1055,13 @@ pub(crate) enum ImageAction { // Lucide names a flip icon after the axis it draws, Acrobat names the flip after the direction the // picture moves: Flip horizontal (left and right swap) shows Lucide's vertical axis. +#[allow(dead_code)] const FLIP_HORIZONTAL_ICON: &str = "flip-vertical-2"; +#[allow(dead_code)] const FLIP_VERTICAL_ICON: &str = "flip-horizontal-2"; /// Edit image: rotate, flip, crop, replace (shown while an added image is selected). +#[allow(dead_code)] pub(crate) fn image_panel(ui: &mut egui::Ui, t: &Tokens, img: &pdfcraft_engine::AddedImage) -> Option { let mut out = None; widgets::section_title(ui, tl!("Edit image")); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 534b7fcd9d..aaf72a5c7a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -763,6 +763,7 @@ pub struct PdfCraftApp { pub clipboard_override: Option, /// V is down as a plain key press (see [`Self::paste_key_released`]). v_pressed: bool, + last_command_time: Option, /// Attach file: the file to attach instead of asking (tests, automation). pub attach_override: Option<(String, Vec)>, /// Duplicate Field: which field and onto which pages. @@ -1093,6 +1094,7 @@ impl PdfCraftApp { system_clipboard: true, clipboard_override: None, v_pressed: false, + last_command_time: None, attach_override: None, duplicate_draft: None, field_table_draft: None, diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 3d749ac177..85c9ea66ff 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -6037,6 +6037,65 @@ fn image_copy_cut_paste_keyboard_and_context_menu() { assert_eq!(after_paste2, 2, "pasted via context menu"); } +#[test] +fn added_and_pasted_image_uses_refactored_image_edits() { + let mut png = Vec::new(); + image::RgbImage::from_pixel(80, 80, image::Rgb([120, 180, 240])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + + let mut h = harness(1, |_| {}); + h.run_steps(4); + + // 1. Add image via add_image + h.state_mut().add_image("test_add.png".into(), png); + h.run_steps(3); + + // Verify refactored mode is active + assert_eq!(h.state().quick_tool, pdfcraft_ui_egui::QuickTool::EditText); + let id = h.state().views[0].id; + let images = h.state().session.get(id).unwrap().page_images(0); + assert_eq!(images.len(), 1, "image is added"); + assert_eq!(h.state().views[0].image_selection.as_ref().map(|s| (s.page, s.index)), Some((0, 0)), "added image has refactored image_selection"); + + // 2. Perform refactored crop on the added image + let r = h.state().views[0].page_screen_rect(0).expect("on screen"); + let c = r.center(); + h.hover_at(c); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() }); + h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() }); + h.run_steps(2); + h.get_by_label("Crop").click(); + h.run_steps(2); + assert!(h.state().views[0].image_selection.as_ref().is_some_and(|s| s.crop_mode), "crop mode entered"); + + let smaller = egui::Rect::from_center_size(c, egui::vec2(40.0, 40.0)); + h.state_mut().views[0].image_selection.as_mut().unwrap().crop_rect = Some(smaller); + h.key_press(egui::Key::Enter); + h.run_steps(4); + assert!(!h.state().views[0].image_selection.as_ref().is_some_and(|s| s.crop_mode), "crop mode exited"); + + let images_after_crop = h.state().session.get(id).unwrap().page_images(0); + assert!(images_after_crop[0].visible_rect.is_some()); + assert_ne!(images_after_crop[0].visible_rect, Some(images_after_crop[0].rect), "crop applied to added image"); + + // 3. Move the cropped added image + let orig_rect = images_after_crop[0].rect; + let orig_vis = images_after_crop[0].visible_rect.unwrap(); + let dx = 20.0; + let dy = 20.0; + let new_rect = [orig_rect[0] + dx, orig_rect[1] + dy, orig_rect[2] + dx, orig_rect[3] + dy]; + h.state_mut().apply_edit(pdfcraft_engine::Edit::EditPageImage { page: 0, index: 0, change: pdfcraft_engine::ImageEdit::Move(new_rect) }); + h.run_steps(3); + + let images_after_move = h.state().session.get(id).unwrap().page_images(0); + assert_eq!(images_after_move[0].rect, new_rect, "moved rect matches"); + assert_eq!( + images_after_move[0].visible_rect, + Some([orig_vis[0] + dx, orig_vis[1] + dy, orig_vis[2] + dx, orig_vis[3] + dy]), + "crop preserved after move" + ); +} + #[test] fn stale_clipboard_plain_text_does_not_paste_image() { let mut h = harness(1, |_| {});