Fixes As Requested [Arranging works,Cropping fix, Small size issue,fonts,image override]

This commit is contained in:
Combiler01 committed 2026-10-14 20:40:13 +05:30
1 parent 9a5fec0caa
commit bf4c238eba
6 files changed
+233 -86

No files matched your search

+64 -31
View File
@@ -603,33 +603,72 @@ pub fn arrange_image(doc: &mut Document, page: usize, index: usize, order: Arran
};
let Some(target_unit) = units.get(target_idx) else { return Ok(()) };
let (scope_start, scope_end, placement) = image_transform_scope(&ops, target)?.unwrap_or((target, target, Matrix(img.draw_matrix)));
let has_other_paint = ops.iter().enumerate().any(|(i, op)| {
i >= target_unit.start
&& i < target_unit.end
&& i != target
&& matches!(
op.op.as_slice(),
b"Do" | b"Tj" | b"TJ" | b"'" | b"\"" | b"sh" | b"BI" | b"S" | b"s" | b"f" | b"F" | b"f*" | b"B" | b"B*" | b"b" | b"b*"
)
});
let unit_ops = if scope_start != scope_end && target_unit.start == scope_start && target_unit.end == scope_end.saturating_add(1) {
if placement == Matrix::IDENTITY {
ops.get(scope_start..=scope_end).unwrap_or_default().to_vec()
let (target_span, unit_ops) = if has_other_paint {
if let Ok(Some((sub_start, sub_end, sub_placement))) = image_exclusive_scope(&ops, target)
&& sub_start >= target_unit.start
&& sub_end < target_unit.end
{
let u_ops = if sub_placement == Matrix::IDENTITY {
ops.get(sub_start..=sub_end).unwrap_or_default().to_vec()
} else {
let mut out = Vec::new();
out.push(Op::new("q", vec![]));
out.push(Op::new("cm", sub_placement.0.map(pdfcraft_content::num).to_vec()));
if let Some(inner) = ops.get(sub_start.saturating_add(1)..=sub_end) {
out.extend_from_slice(inner);
}
out
};
let start_op = ops.get(sub_start).unwrap_or(&ops[target]);
let end_op = ops.get(sub_end).unwrap_or(&ops[target]);
(start_op.span.start..end_op.span.end, u_ops)
} else {
let mut out = Vec::new();
out.push(Op::new("q", vec![]));
out.push(Op::new("cm", placement.0.map(pdfcraft_content::num).to_vec()));
if let Some(inner) = ops.get(scope_start.saturating_add(1)..=scope_end) {
out.extend_from_slice(inner);
}
out
let u_ops = vec![
Op::new("q", vec![]),
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
Op::new("Q", vec![]),
];
let target_op = &ops[target];
(target_op.span.start..target_op.span.end, u_ops)
}
} else {
vec![
Op::new("q", vec![]),
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
Op::new("Q", vec![]),
]
let (scope_start, scope_end, placement) = image_transform_scope(&ops, target)?.unwrap_or((target, target, Matrix(img.draw_matrix)));
let u_ops = if scope_start != scope_end && target_unit.start == scope_start && target_unit.end == scope_end.saturating_add(1) {
if placement == Matrix::IDENTITY {
ops.get(scope_start..=scope_end).unwrap_or_default().to_vec()
} else {
let mut out = Vec::new();
out.push(Op::new("q", vec![]));
out.push(Op::new("cm", placement.0.map(pdfcraft_content::num).to_vec()));
if let Some(inner) = ops.get(scope_start.saturating_add(1)..=scope_end) {
out.extend_from_slice(inner);
}
out
}
} else {
vec![
Op::new("q", vec![]),
Op::new("cm", img.draw_matrix.map(pdfcraft_content::num).to_vec()),
ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)])),
Op::new("Q", vec![]),
]
};
let Some(first_op) = ops.get(target_unit.start) else { return Ok(()) };
let Some(last_op) = target_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
(first_op.span.start..last_op.span.end, u_ops)
};
let Some(first_op) = ops.get(target_unit.start) else { return Ok(()) };
let Some(last_op) = target_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
let target_span = first_op.span.start..last_op.span.end;
let dest_pos = if insert_after {
let Some(op) = dest_unit.end.checked_sub(1).and_then(|i| ops.get(i)) else { return Ok(()) };
op.span.end
@@ -851,21 +890,15 @@ pub fn crop_image(doc: &mut Document, page: usize, index: usize, crop_rect: [f64
(start, end, drawing_ops, scope_matrix)
} else {
let mut state = ImageState::new(Matrix::IDENTITY, p.crop(doc), Vec::new());
let mut scope_matrix = Matrix::IDENTITY;
let start = if target >= 1 && ops.get(target - 1).is_some_and(|o| o.is("cm")) {
target - 1
} else {
target
};
for (i, o) in ops.iter().enumerate() {
if i == start {
scope_matrix = state.placement.matrix;
if i == target {
break;
}
state.apply(o)?;
}
let drawing_ops = ops.get(start..=target).unwrap_or_default().to_vec();
(start, target, drawing_ops, scope_matrix)
let scope_matrix = state.placement.matrix;
let drawing_ops = vec![ops.get(target).cloned().unwrap_or_else(|| Op::new("Do", vec![Object::name(&img.name)]))];
(target, target, drawing_ops, scope_matrix)
};
let mut unit_ops = Vec::new();
+37
View File
@@ -1768,6 +1768,43 @@ fn image_arrange_stacking_order_and_remove_crop() {
assert_eq!(img_restored_moved.visible_rect, Some(img_restored_moved.rect));
}
#[test]
fn image_arrange_preserves_shared_group_drawings() {
let content = "q 100 0 0 100 100 100 cm /Im0 Do /Im0 Do Q BT /F1 12 Tf 72 700 Td (End Text) Tj ET";
let mut doc = image_page_with(content);
let initial = images::page_images(&doc, 0).unwrap();
assert_eq!(initial.len(), 2, "both images initially present in shared group");
// Bring image 0 to front: it must not delete image 1 that shares the same q...Q group!
images::arrange_image(&mut doc, 0, 0, images::ArrangeOrder::BringToFront).unwrap();
let doc = reopen(&doc);
let after = images::page_images(&doc, 0).unwrap();
assert_eq!(after.len(), 2, "both images must be preserved after BringToFront");
let bytes = page_content_bytes(&doc, 0);
let str_content = String::from_utf8_lossy(&bytes);
assert!(str_content.contains("End Text"), "text in page must be preserved");
}
#[test]
fn image_crop_preserves_outer_cm_for_subsequent_drawings() {
let content = "100 0 0 100 50 50 cm /Im0 Do /Im0 Do";
let mut doc = image_page_with(content);
let initial = images::page_images(&doc, 0).unwrap();
assert_eq!(initial.len(), 2);
assert_eq!(initial[0].rect, [50.0, 50.0, 150.0, 150.0]);
assert_eq!(initial[1].rect, [50.0, 50.0, 150.0, 150.0]);
// Crop image 0
images::crop_image(&mut doc, 0, 0, [60.0, 60.0, 120.0, 120.0]).unwrap();
let doc = reopen(&doc);
let after = images::page_images(&doc, 0).unwrap();
assert_eq!(after.len(), 2, "both images must remain");
// Image 0 is cropped:
assert!(close(after[0].visible_rect.unwrap(), [60.0, 60.0, 120.0, 120.0]));
// Crucial: Image 1 must still have its rect at [50, 50, 150, 150], proving outer cm was not destroyed!
assert_eq!(after[1].rect, [50.0, 50.0, 150.0, 150.0], "subsequent image must retain global cm");
}
#[test]
fn form_artwork_edits_only_the_selected_placement() {
+33 -41
View File
@@ -560,25 +560,6 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
ui.selectable_value(&mut s.family, f, f.label());
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(installed) = crate::files::installed_font_files(ui.ctx())
&& !installed.is_empty()
{
ui.separator();
ui.label(egui::RichText::new(tl!("Installed fonts")).weak().small());
for font in &installed {
let fam = if font.family.eq_ignore_ascii_case("times") || font.face.to_ascii_lowercase().contains("times") {
FontFamily::Times
} else if font.family.eq_ignore_ascii_case("courier") || font.face.to_ascii_lowercase().contains("courier") {
FontFamily::Courier
} else {
FontFamily::Helvetica
};
if ui.selectable_label(s.family == fam, &font.face).clicked() {
s.family = fam;
}
}
}
});
ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("font-size")
@@ -789,18 +770,16 @@ impl crate::PdfCraftApp {
/// Whether there is an image in the in-memory clipboard or system clipboard ready to paste.
pub fn can_paste_image(&self) -> bool {
if self.last_copied_image.is_some() || self.last_copied_file.is_some() {
return true;
}
#[cfg(not(target_arch = "wasm32"))]
{
if self.system_clipboard
&& let Ok(mut cb) = arboard::Clipboard::new()
&& cb.get_image().is_ok()
{
return cb.get_image().is_ok();
return true;
}
}
false
self.last_copied_image.is_some() || self.last_copied_file.is_some()
}
/// Paste an image from the clipboard onto `page`.
@@ -813,27 +792,40 @@ impl crate::PdfCraftApp {
return false;
}
let image_data: Option<(String, Vec<u8>)> = if let Some((name, bytes)) = self.last_copied_file.clone() {
#[cfg(not(target_arch = "wasm32"))]
let os_png = if self.system_clipboard {
if let Ok(mut cb) = arboard::Clipboard::new()
&& let Ok(img) = cb.get_image()
{
let matches_internal = self.last_copied_image.as_ref().is_some_and(|(w, h, rgba)| {
*w as usize == img.width && *h as usize == img.height && rgba.as_slice() == img.bytes.as_ref()
});
if !matches_internal {
crate::create_ui::png_from_rgba(img.width, img.height, &img.bytes).ok().map(|bytes| {
(img.width as u32, img.height as u32, img.bytes.to_vec(), bytes)
})
} else {
None
}
} else {
None
}
} else {
None
};
#[cfg(target_arch = "wasm32")]
let os_png: Option<(u32, u32, Vec<u8>, Vec<u8>)> = None;
let image_data: Option<(String, Vec<u8>)> = if let Some((w, h, rgba, png_bytes)) = os_png {
self.last_copied_image = Some((w, h, rgba));
self.last_copied_file = Some(("clipboard_image.png".to_string(), png_bytes.clone()));
self.last_copied_image_rect = None;
Some(("clipboard_image.png".to_string(), png_bytes))
} else if let Some((name, bytes)) = self.last_copied_file.clone() {
Some((name, bytes))
} else if let Some((w, h, rgba)) = self.last_copied_image.clone() {
crate::create_ui::png_from_rgba(w as usize, h as usize, &rgba).ok().map(|b| ("pasted_image.png".to_string(), b))
} else {
#[cfg(not(target_arch = "wasm32"))]
{
if self.system_clipboard {
arboard::Clipboard::new()
.ok()
.and_then(|mut cb| cb.get_image().ok())
.and_then(|img| {
crate::create_ui::png_from_rgba(img.width, img.height, &img.bytes)
.ok()
.map(|b| ("clipboard_image.png".to_string(), b))
})
} else {
None
}
}
#[cfg(target_arch = "wasm32")]
None
};
+44 -12
View File
@@ -975,41 +975,73 @@ pub(crate) fn image_input(
let initial = drag.initial_rect;
if is_crop {
let mut new_crop = initial;
let img_screen_w = b.width().max(0.0);
let img_screen_h = b.height().max(0.0);
let safe_min_w = 10.0_f32.min(img_screen_w).min(initial.width().max(0.0)).max(0.0);
let safe_min_h = 10.0_f32.min(img_screen_h).min(initial.height().max(0.0)).max(0.0);
match drag.target {
DragTarget::CropHandle(ImageHandle::TopLeft) => {
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
let min_x = b.left();
let max_x = (initial.right() - safe_min_w).max(min_x);
let x = (initial.left() + delta.x).clamp(min_x, max_x);
let min_y = b.top();
let max_y = (initial.bottom() - safe_min_h).max(min_y);
let y = (initial.top() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(Pos2::new(x, y), initial.max);
}
DragTarget::CropHandle(ImageHandle::TopRight) => {
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
let min_x = (initial.left() + safe_min_w).min(b.right());
let max_x = b.right().max(min_x);
let x = (initial.right() + delta.x).clamp(min_x, max_x);
let min_y = b.top();
let max_y = (initial.bottom() - safe_min_h).max(min_y);
let y = (initial.top() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(Pos2::new(initial.left(), y), Pos2::new(x, initial.bottom()));
}
DragTarget::CropHandle(ImageHandle::BottomLeft) => {
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
let min_x = b.left();
let max_x = (initial.right() - safe_min_w).max(min_x);
let x = (initial.left() + delta.x).clamp(min_x, max_x);
let min_y = (initial.top() + safe_min_h).min(b.bottom());
let max_y = b.bottom().max(min_y);
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(Pos2::new(x, initial.top()), Pos2::new(initial.right(), y));
}
DragTarget::CropHandle(ImageHandle::BottomRight) => {
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
let min_x = (initial.left() + safe_min_w).min(b.right());
let max_x = b.right().max(min_x);
let x = (initial.right() + delta.x).clamp(min_x, max_x);
let min_y = (initial.top() + safe_min_h).min(b.bottom());
let max_y = b.bottom().max(min_y);
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(initial.min, Pos2::new(x, y));
}
DragTarget::CropHandle(ImageHandle::TopCenter) => {
let y = (initial.top() + delta.y).clamp(b.top(), initial.bottom() - 10.0);
let min_y = b.top();
let max_y = (initial.bottom() - safe_min_h).max(min_y);
let y = (initial.top() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(Pos2::new(initial.left(), y), initial.max);
}
DragTarget::CropHandle(ImageHandle::BottomCenter) => {
let y = (initial.bottom() + delta.y).clamp(initial.top() + 10.0, b.bottom());
let min_y = (initial.top() + safe_min_h).min(b.bottom());
let max_y = b.bottom().max(min_y);
let y = (initial.bottom() + delta.y).clamp(min_y, max_y);
new_crop = Rect::from_min_max(initial.min, Pos2::new(initial.right(), y));
}
DragTarget::CropHandle(ImageHandle::LeftCenter) => {
let x = (initial.left() + delta.x).clamp(b.left(), initial.right() - 10.0);
let min_x = b.left();
let max_x = (initial.right() - safe_min_w).max(min_x);
let x = (initial.left() + delta.x).clamp(min_x, max_x);
new_crop = Rect::from_min_max(Pos2::new(x, initial.top()), initial.max);
}
DragTarget::CropHandle(ImageHandle::RightCenter) => {
let x = (initial.right() + delta.x).clamp(initial.left() + 10.0, b.right());
let min_x = (initial.left() + safe_min_w).min(b.right());
let max_x = b.right().max(min_x);
let x = (initial.right() + delta.x).clamp(min_x, max_x);
new_crop = Rect::from_min_max(initial.min, Pos2::new(x, initial.bottom()));
}
DragTarget::Move => {
+2 -1
View File
@@ -73,7 +73,8 @@ mod autoscroll;
mod bidi;
pub mod bulk_fields;
mod dialogs;
mod edit_text_ui;
pub mod edit_text_ui;
pub use edit_text_ui::{DragTarget, ImageDrag, ImageHandle, ImageSelection};
mod editing;
#[cfg(not(target_arch = "wasm32"))]
pub mod file_watch;
+53 -1
View File
@@ -2717,11 +2717,63 @@ fn image_edge_handle_stretch_and_crop_mode() {
h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
h.run_steps(2);
h.get_by_label("Remove crop").click();
h.run_steps(4);
let images = h.state().session.get(doc_id).unwrap().page_images(0);
assert_eq!(images[0].visible_rect, Some(images[0].rect), "crop is removed");
}
#[test]
fn tiny_image_crop_handle_drag_does_not_panic() {
let mut png = Vec::new();
image::RgbImage::from_pixel(5, 5, image::Rgb([100, 150, 200])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(800.0, 600.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("tiny.png", None, png.clone()).expect("opens");
app
});
h.run_steps(4);
assert!(h.state_mut().execute("edit.edit_text"));
h.run_steps(2);
// Enter crop mode on image 0
let view = &mut h.state_mut().views[0];
view.image_selection = Some(pdfcraft_ui_egui::ImageSelection {
page: 0,
index: 0,
drag: None,
crop_mode: true,
crop_rect: None,
});
h.run_steps(1);
// Simulate drag on TopLeft crop handle with an initial_rect of 5.0 x 5.0
let initial = egui::Rect::from_min_size(egui::pos2(100.0, 100.0), egui::vec2(5.0, 5.0));
let view = &mut h.state_mut().views[0];
if let Some(s) = view.image_selection.as_mut() {
s.drag = Some(pdfcraft_ui_egui::ImageDrag {
start: egui::pos2(100.0, 100.0),
target: pdfcraft_ui_egui::DragTarget::CropHandle(pdfcraft_ui_egui::ImageHandle::TopLeft),
initial_rect: initial,
});
}
// Hover/drag to a new position, triggering the clamp calculation
h.hover_at(egui::pos2(105.0, 105.0));
h.run_steps(2);
// Also test BottomRight handle
let view = &mut h.state_mut().views[0];
if let Some(s) = view.image_selection.as_mut() {
s.drag = Some(pdfcraft_ui_egui::ImageDrag {
start: egui::pos2(105.0, 105.0),
target: pdfcraft_ui_egui::DragTarget::CropHandle(pdfcraft_ui_egui::ImageHandle::BottomRight),
initial_rect: initial,
});
}
h.hover_at(egui::pos2(95.0, 95.0));
h.run_steps(2);
}
#[test]
fn text_selection_action_icons() {
let mut h = harness(1, |_| {});