mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 21:11:52 +00:00
Fixes: Refactored System not opening for Added/Pasted Images
This commit is contained in:
1 parent
e4476b007d
commit
3641801f13
4 files changed
+158
-61
No files matched your search
+59
-52
@@ -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 } => {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
Reference in new issue
Block a user