Fixes: Refactored System not opening for Added/Pasted Images

This commit is contained in:
Combiler01 committed 2026-10-23 14:49:01 +05:30
1 parent e4476b007d
commit 3641801f13
4 files changed
+158 -61

No files matched your search

+59 -52
View File
@@ -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 } => {
+38 -9
View File
@@ -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<u8>) {
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<ImageAction> {
let mut out = None;
widgets::section_title(ui, tl!("Edit image"));
+2
View File
@@ -763,6 +763,7 @@ pub struct PdfCraftApp {
pub clipboard_override: Option<create_ui::Clip>,
/// V is down as a plain key press (see [`Self::paste_key_released`]).
v_pressed: bool,
last_command_time: Option<f64>,
/// Attach file: the file to attach instead of asking (tests, automation).
pub attach_override: Option<(String, Vec<u8>)>,
/// 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,
+59
View File
@@ -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, |_| {});