diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index e56216b9ef..46f1c5c2b3 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -718,7 +718,7 @@ impl crate::PdfCraftApp { } /// Copy Image: image `index` on `page`, at its own resolution, to the clipboard. - pub(crate) fn copy_page_image(&mut self, page: usize, index: usize) -> bool { + pub fn copy_page_image(&mut self, page: usize, index: usize) -> bool { let Some((_, id)) = self.active_ids() else { return false }; if !self.may_copy(id) { return false; @@ -735,7 +735,7 @@ impl crate::PdfCraftApp { return Err("the web version can't put images on the clipboard yet; use Save Image As".to_string()); } self.last_copied_image = Some((w, h, rgba)); - self.last_copied_image_rect = self.page_image_box(page, index); + self.last_copied_image_rect = None; Ok((w, h)) }); match copied { @@ -952,30 +952,11 @@ impl crate::PdfCraftApp { return false; }; - let target_rect = match rect { - Some(r) => Some(r), - None => { - if let Some(src_rect) = self.last_copied_image_rect { - let w = src_rect[2] - src_rect[0]; - let h = src_rect[3] - src_rect[1]; - let offset_x = 20.0; - let offset_y = -20.0; - let new_r = [src_rect[0] + offset_x, src_rect[1] + offset_y, src_rect[0] + offset_x + w, src_rect[1] + offset_y + h]; - self.last_copied_image_rect = Some(new_r); - Some(new_r) - } else { - None - } - } - }; + let target_rect = rect; let before = self.session.get(id).map_or(0, |d| d.page_images(page).len()); if self.apply_edit(pdfcraft_engine::Edit::AddImage { page, rect: target_rect, name, bytes: std::sync::Arc::new(bytes) }) { - let placed_rect = - target_rect.or_else(|| self.session.get(id).and_then(|d| d.page_images(page).into_iter().nth(before).map(|im| im.rect))); - if let Some(r) = placed_rect { - self.last_copied_image_rect = Some(r); - } + self.last_copied_image_rect = None; let added_count = self.session.get(id).map_or(0, |d| d.added().iter().filter(|a| a.page == page).count()); let added_idx = added_count.saturating_sub(1); if let Some(v) = self.views.get_mut(i) { @@ -1003,44 +984,31 @@ impl crate::PdfCraftApp { /// Paste an image centered at context menu point `at: [f64; 2]` on `page`. pub fn paste_image_at(&mut self, page: usize, at: [f64; 2]) -> bool { - let (w, h) = if let Some(crate::create_ui::Clip::Image { width, height, rgba }) = self.clipboard_override.as_ref() { - let is_same = self - .last_copied_image - .as_ref() - .is_some_and(|(w, h, bytes)| *w as usize == *width && *h as usize == *height && bytes.as_slice() == rgba.as_slice()); - if is_same && let Some(src_r) = self.last_copied_image_rect { - (src_r[2] - src_r[0], src_r[3] - src_r[1]) - } else { - (*width as f64, *height as f64) - } + let (raw_w, raw_h) = if let Some(crate::create_ui::Clip::Image { width, height, .. }) = self.clipboard_override.as_ref() { + (*width as f64, *height as f64) } else { #[cfg(not(target_arch = "wasm32"))] - let sys_img = if self.system_clipboard { - crate::os_clipboard::with(|cb| cb.get_image()).ok().map(|img| (img.width, img.height, img.bytes.into_owned())) - } else { - None - }; + let sys_img = + if self.system_clipboard { crate::os_clipboard::with(|cb| cb.get_image()).ok().map(|img| (img.width, img.height)) } else { None }; #[cfg(target_arch = "wasm32")] - let sys_img: Option<(usize, usize, Vec)> = None; + let sys_img: Option<(usize, usize)> = None; - if let Some((img_width, img_height, img_bytes)) = sys_img { - let is_same = self - .last_copied_image - .as_ref() - .is_some_and(|(w, h, bytes)| *w as usize == img_width && *h as usize == img_height && bytes.as_slice() == img_bytes.as_slice()); - if is_same && let Some(src_r) = self.last_copied_image_rect { - (src_r[2] - src_r[0], src_r[3] - src_r[1]) - } else { - (img_width as f64, img_height as f64) - } - } else if let Some(src_r) = self.last_copied_image_rect { - (src_r[2] - src_r[0], src_r[3] - src_r[1]) + if let Some((img_width, img_height)) = sys_img { + (img_width as f64, img_height as f64) } else if let Some((w, h, _)) = self.last_copied_image { (w as f64, h as f64) } else { (100.0, 100.0) } }; + + let (_, id) = match self.active_ids() { + Some(ids) => ids, + None => return false, + }; + let (pw, ph) = self.session.get(id).and_then(|d| d.info.pages.get(page)).map(|p| (p.width as f64, p.height as f64)).unwrap_or((612.0, 792.0)); + let k = ((pw * 0.8) / raw_w.max(1.0)).min((ph * 0.8) / raw_h.max(1.0)).min(1.0); + let (w, h) = (raw_w * k, raw_h * k); let rect = Some([at[0] - w * 0.5, at[1] - h * 0.5, at[0] + w * 0.5, at[1] + h * 0.5]); self.paste_image(page, rect) } diff --git a/crates/ui-egui/src/image_pick.rs b/crates/ui-egui/src/image_pick.rs index babde8227a..fad22f299b 100644 --- a/crates/ui-egui/src/image_pick.rs +++ b/crates/ui-egui/src/image_pick.rs @@ -191,7 +191,7 @@ impl PdfCraftApp { /// Copy Image in Original Format: image `index` on `page` as a file on the clipboard (JPEG /// as stored in the PDF, otherwise PNG). - pub(crate) fn copy_page_image_file(&mut self, page: usize, index: usize) { + pub fn copy_page_image_file(&mut self, page: usize, index: usize) { let Some((_, id)) = self.active_ids() else { return }; if !self.may_copy(id) { return; @@ -203,7 +203,7 @@ impl PdfCraftApp { let name = format!("{stem} p{} img{}.{ext}", page.saturating_add(1), index.saturating_add(1)); self.put_file_on_clipboard(&name, &bytes)?; self.last_copied_file = Some((name, bytes)); - self.last_copied_image_rect = self.page_image_box(page, index); + self.last_copied_image_rect = None; Ok(ext) }); match copied { diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 85c9ea66ff..233b572153 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -7093,3 +7093,49 @@ fn mode_tabs_keep_the_hand_over_a_paragraph_scrolled_under_them() { click_at(&mut h, convert); assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the paragraph's editor"); } + +#[test] +fn copy_pasted_image_discards_coordinates_and_moves_without_offset() { + let mut png = Vec::new(); + image::RgbImage::from_pixel(60, 60, image::Rgb([100, 150, 200])).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 to page 0 + h.state_mut().add_image("test_img.png".into(), png); + h.run_steps(3); + + let id = h.state().views[0].id; + let images_before = h.state().session.get(id).unwrap().page_images(0); + assert_eq!(images_before.len(), 1); + + // 2. Copy the image from page 0 + assert!(h.state_mut().copy_page_image(0, 0)); + // Verify coordinates data was discarded during copy! + assert!(h.state().last_copied_image_rect.is_none(), "coordinates data must be discarded during copy"); + assert!(h.state().last_copied_image.is_some(), "image pixel data is kept"); + + // 3. Paste the image via Ctrl+V (paste_image with rect: None) + assert!(h.state_mut().paste_image(0, None)); + h.run_steps(3); + + // Verify after paste that coordinates data remains None + assert!(h.state().last_copied_image_rect.is_none()); + + let images_after_paste = h.state().session.get(id).unwrap().page_images(0); + assert_eq!(images_after_paste.len(), 2, "second image pasted"); + + // The pasted image should be centered on the page, not offset by (+20, -20) from the first image + let img1 = &images_after_paste[0]; + let img2 = &images_after_paste[1]; + assert_eq!(img1.rect, img2.rect, "pasted image placed using natural page centering"); + + // 4. Move the pasted image by an explicit Move change + let target = [50.0, 50.0, 110.0, 110.0]; + h.state_mut().apply_edit(pdfcraft_engine::Edit::EditPageImage { page: 0, index: 1, change: pdfcraft_engine::ImageEdit::Move(target) }); + h.run_steps(3); + + let images_after_move = h.state().session.get(id).unwrap().page_images(0); + assert_eq!(images_after_move[1].rect, target, "moved image rect matches target exactly without offset"); +}