diff --git a/crates/edit/src/images.rs b/crates/edit/src/images.rs index 6d15dcdc35..7ed2fc2393 100644 --- a/crates/edit/src/images.rs +++ b/crates/edit/src/images.rs @@ -55,7 +55,7 @@ pub struct PageImage { pub smask: Option, /// The content stream its `Do` (its first token) is in, and the `Do`'s index among the /// operators of the page's streams joined ([`Pieces`]). - stream: usize, + pub stream: usize, op: usize, /// CTM before `Do`, excluding a Form's own Matrix. draw_matrix: [f64; 6], @@ -148,24 +148,31 @@ struct ImageState { placement: Placement, stack: Vec, path: BezPath, + subpath_start: Option, pending_clip: Option, } impl ImageState { fn new(matrix: Matrix, clip: [f64; 4], clips: Vec) -> Self { - Self { placement: Placement { matrix, clip, clips }, stack: Vec::new(), path: BezPath::new(), pending_clip: None } + Self { placement: Placement { matrix, clip, clips }, stack: Vec::new(), path: BezPath::new(), subpath_start: None, pending_clip: None } + } + + fn current_point(&self) -> Option { + match self.path.elements().last() { + Some(kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p)) => Some(*p), + Some(kurbo::PathEl::QuadTo(_, p2) | kurbo::PathEl::CurveTo(_, _, p2)) => Some(*p2), + Some(kurbo::PathEl::ClosePath) => self.subpath_start, + None => None, + } } fn has_current_point(&self) -> bool { - matches!( - self.path.elements().last(), - Some(kurbo::PathEl::MoveTo(_) | kurbo::PathEl::LineTo(_) | kurbo::PathEl::QuadTo(_, _) | kurbo::PathEl::CurveTo(_, _, _)) - ) + self.current_point().is_some() } - fn point(&self, x: f64, y: f64) -> Point { + fn point(&self, x: f64, y: f64) -> Option { let (x, y) = self.placement.matrix.apply(x, y); - if x.is_finite() && y.is_finite() { Point::new(x, y) } else { Point::ZERO } + if x.is_finite() && y.is_finite() { Some(Point::new(x, y)) } else { None } } fn apply(&mut self, op: &Op) -> Result<(), EditError> { @@ -193,8 +200,11 @@ impl ImageState { } } b"m" => { - if let Some([x, y]) = op.nums::<2>() { - self.path.move_to(self.point(x, y)); + if let Some([x, y]) = op.nums::<2>() + && let Some(p) = self.point(x, y) + { + self.subpath_start = Some(p); + self.path.move_to(p); } } b"l" => { @@ -203,35 +213,33 @@ impl ImageState { // renderer, MuPDF and pdftotext do (#3581). if let Some([x, y]) = op.nums::<2>() && self.has_current_point() + && let Some(p) = self.point(x, y) { - self.path.line_to(self.point(x, y)); + self.path.line_to(p); } } b"c" => { if let Some([x1, y1, x2, y2, x3, y3]) = op.nums::<6>() && self.has_current_point() + && let (Some(p1), Some(p2), Some(p3)) = (self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)) { - self.path.curve_to(self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)); + self.path.curve_to(p1, p2, p3); } } b"v" => { - if let Some([x1, y1, x2, y2]) = op.nums::<4>() { - let p1 = self.point(x1, y1); - let p2 = self.point(x2, y2); - if let Some(current) = self.path.elements().last().and_then(|e| e.end_point()) { - self.path.curve_to(current, p1, p2); - } else { - self.path.move_to(p1); - self.path.curve_to(p1, p1, p2); - } + if let Some([x1, y1, x2, y2]) = op.nums::<4>() + && self.has_current_point() + && let (Some(p1), Some(p2)) = (self.point(x1, y1), self.point(x2, y2)) + && let Some(current) = self.current_point() + { + self.path.curve_to(current, p1, p2); } } b"y" => { if let Some([x1, y1, x2, y2]) = op.nums::<4>() && self.has_current_point() + && let (Some(p1), Some(end)) = (self.point(x1, y1), self.point(x2, y2)) { - let p1 = self.point(x1, y1); - let end = self.point(x2, y2); self.path.curve_to(p1, end, end); } } @@ -242,11 +250,15 @@ impl ImageState { } } b"re" => { - if let Some([x, y, w, h]) = op.nums::<4>() { - self.path.move_to(self.point(x, y)); - self.path.line_to(self.point(x + w, y)); - self.path.line_to(self.point(x + w, y + h)); - self.path.line_to(self.point(x, y + h)); + if let Some([x, y, w, h]) = op.nums::<4>() + && let (Some(p0), Some(p1), Some(p2), Some(p3)) = + (self.point(x, y), self.point(x + w, y), self.point(x + w, y + h), self.point(x, y + h)) + { + self.subpath_start = Some(p0); + self.path.move_to(p0); + self.path.line_to(p1); + self.path.line_to(p2); + self.path.line_to(p3); self.path.close_path(); } } @@ -261,6 +273,7 @@ impl ImageState { self.placement.clips.push(ClipPath { path: Arc::new(std::mem::take(&mut self.path)), even_odd }); } self.path = BezPath::new(); + self.subpath_start = None; } _ => {} } @@ -1659,26 +1672,9 @@ fn crop_image_impl( if x1 - x0 < 1e-6 || y1 - y0 < 1e-6 { return Err(EditError::Invalid("the crop box has no area".into())); } - let inv = Matrix(image.matrix).invert().ok_or_else(|| EditError::Invalid("the image has no area".into()))?; - let corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)].map(|(x, y)| inv.apply(x, y)); - let min_u = corners.iter().map(|(u, _)| *u).fold(f64::INFINITY, f64::min).clamp(0.0, 1.0); - let max_u = corners.iter().map(|(u, _)| *u).fold(f64::NEG_INFINITY, f64::max).clamp(0.0, 1.0); - let min_v = corners.iter().map(|(_, v)| *v).fold(f64::INFINITY, f64::min).clamp(0.0, 1.0); - let max_v = corners.iter().map(|(_, v)| *v).fold(f64::NEG_INFINITY, f64::max).clamp(0.0, 1.0); - if max_u - min_u < 1e-6 || max_v - min_v < 1e-6 { - return Err(EditError::Invalid("the crop box has no area".into())); + if intersection(image.rect, [x0, y0, x1, y1]).is_none() { + return Err(EditError::Invalid("the crop box does not intersect the image".into())); } - let (u0, v0, u1, v1) = (min_u, min_v, max_u, max_v); - - if u1 - u0 < 1e-6 || v1 - v0 < 1e-6 { - return Err(EditError::Invalid("the crop box has no area".into())); - } - - let m = Matrix(image.matrix); - let p00 = m.apply(u0, v0); - let p10 = m.apply(u1, v0); - let p11 = m.apply(u1, v1); - let p01 = m.apply(u0, v1); let pieces = joined(streams); let ops = pieces.parse().ops; @@ -1688,7 +1684,7 @@ fn crop_image_impl( } let (start, end, placement) = image_transform_scope(&ops, target)?.unwrap_or((target, target, Matrix(image.draw_matrix))); let inverse = placement.invert().ok_or_else(|| EditError::Invalid("the image has no area".into()))?; - let corners = [p00, p10, p11, p01].map(|(x, y)| inverse.apply(x, y)); + let corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)].map(|(x, y)| inverse.apply(x, y)); if !corners.iter().all(|(x, y)| x.is_finite() && y.is_finite() && x.abs() <= 1e12 && y.abs() <= 1e12) { return Err(EditError::Invalid("the crop box is too large for the image's placement".into())); } @@ -1841,32 +1837,3 @@ pub(crate) fn translate_images(doc: &mut Document, page: usize, indexes: &[usize doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), contents))?; Ok(()) } - -/// Post-multiplies the image's existing CTM `[a, b, c, d, e, f]` by the crop transform -/// `[u_min, v_min, u_max, v_max]` to shrink the footprint without stretching. -pub fn crop_ctm(ctm: [f64; 6], crop: [f64; 4]) -> [f64; 6] { - let [a, b, c, d, e, f] = ctm; - let [u_min, v_min, u_max, v_max] = crop; - let new_a = a * (u_max - u_min); - let new_b = b * (u_max - u_min); - let new_c = c * (v_max - v_min); - let new_d = d * (v_max - v_min); - let new_e = e + a * u_min + c * v_min; - let new_f = f + b * u_min + d * v_min; - [new_a, new_b, new_c, new_d, new_e, new_f] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_crop_ctm_standard_non_stretched() { - // Standard upright CTM: width=200, height=100 at origin (50, 100) - let ctm = [200.0, 0.0, 0.0, 100.0, 50.0, 100.0]; - let crop = [0.25, 0.25, 0.75, 0.75]; - let cropped = crop_ctm(ctm, crop); - let expected = [100.0, 0.0, 0.0, 50.0, 100.0, 125.0]; - assert_eq!(cropped, expected); - } -} diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 677f6a0133..62416b3721 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -2785,7 +2785,10 @@ fn crop_rotated_image_preserves_angle_and_in_place_geometry() { let bytes = page_content_bytes(&doc, 0); let str_content = String::from_utf8_lossy(&bytes); - assert!(str_content.contains("re") && str_content.contains("W\nn"), "{str_content}"); + assert!( + (str_content.contains("re") || (str_content.contains("m\n") && str_content.contains("l\n"))) && str_content.contains("W\nn"), + "{str_content}" + ); assert!(str_content.contains("/Im0 Do"), "{str_content}"); let mut doc = doc; @@ -7156,26 +7159,6 @@ fn reading_keeps_dotted_leader_entries_whole_and_in_order() { use proptest::prelude::*; proptest! { - #[test] - fn prop_crop_ctm_finite_for_valid_inputs( - a in -1000.0f64..1000.0, - b in -1000.0f64..1000.0, - c in -1000.0f64..1000.0, - d in -1000.0f64..1000.0, - e in -1000.0f64..1000.0, - f in -1000.0f64..1000.0, - x1 in -1000.0f64..1000.0, - y1 in -1000.0f64..1000.0, - w in 1.0f64..1000.0, - h in 1.0f64..1000.0, - ) { - let ctm = [a, b, c, d, e, f]; - let crop = [x1, y1, x1 + w, y1 + h]; - let res = images::crop_ctm(ctm, crop); - for v in res { - prop_assert!(v.is_finite()); - } - } #[test] fn prop_is_entire_page_wrapped_arbitrary_bytes(bytes in proptest::collection::vec(any::(), 0..2048)) { @@ -7304,3 +7287,18 @@ fn noop_arrange_is_not_ok_without_change() { let after = write_incremental(&doc, &SaveOptions::default()).unwrap(); assert!(r.is_err() || before != after, "Ok with no change"); } + +/// Cropping a turned image keeps the requested upright box in user space, rather than expanding +/// to the bounding box of the turned image coordinates. +#[test] +fn crop_turned_image_preserves_requested_area() { + let mut doc = regression_page("q 70.71 70.71 -70.71 70.71 300 100 cm /Im0 Do Q"); + let req = [270.0, 150.0, 330.0, 200.0]; + images::crop_image(&mut doc, 0, 0, req).expect("crop succeeds"); + let after = &images::page_images(&reopen(&doc), 0).unwrap()[0]; + let v = after.visible_rect.expect("visible rect exists"); + assert!((v[0] - req[0]).abs() < 1.0, "x0: expected {}, got {}", req[0], v[0]); + assert!((v[1] - req[1]).abs() < 1.0, "y0: expected {}, got {}", req[1], v[1]); + assert!((v[2] - req[2]).abs() < 1.0, "x1: expected {}, got {}", req[2], v[2]); + assert!((v[3] - req[3]).abs() < 1.0, "y1: expected {}, got {}", req[3], v[3]); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b6f5fd59ef..8c9d059d41 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -2691,18 +2691,84 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ImageEdit::RemoveCrop => pdfcraft_edit::ImageChange::RemoveCrop, }; pdfcraft_edit::change_image(doc, *page, *index, &c)?; - if let Some(target_ref) = img.object { - let added_items: Vec<(usize, Added)> = pdfcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).enumerate().collect(); - if let Some((added_idx, added_item)) = - added_items.into_iter().find(|(_, a)| matches!(&a.content, AddedContent::Image(ai) if ai.image == target_ref)) - && let AddedContent::Image(mut ai) = added_item.content - { - match change { - ImageEdit::Move(to) => { - ai.rect = *to; - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + 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, + Some(c) => match &*doc.resolve(c) { + pdfcraft_cos::Object::Array(a) => a.get(img.stream).and_then(|o| o.as_ref()), + _ if img.stream == 0 => c.as_ref(), + _ => None, + }, + }); + + let added_items: Vec<(usize, Added)> = pdfcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).enumerate().collect(); + let matched_added = if let Some(st_ref) = target_stream_obj { + added_items.iter().find(|(_, a)| a.obj == st_ref) + } else if let Some(target_ref) = img.object { + added_items.iter().find(|(_, a)| matches!(&a.content, AddedContent::Image(ai) if ai.image == target_ref)) + } else { + None + }; + if let Some((added_idx, added_item)) = matched_added + && let AddedContent::Image(mut ai) = added_item.content.clone() + { + let added_idx = *added_idx; + 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)]; } - ImageEdit::Transform(t) => { + ai.rect = b; + if t[1].abs() < 1e-6 && t[2].abs() < 1e-6 { + pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; + } else { + let _ = doc.update_dict(added_item.obj, |d| { + if let Some(pdfcraft_cos::Object::Dict(params)) = d.get_mut(b"PCAdded") { + params.set( + b"Rect".to_vec(), + pdfcraft_cos::Object::Array(ai.rect.iter().map(|x| pdfcraft_cos::Object::Real(*x)).collect()), + ); + } + }); + } + } + 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::RotateBy(degrees) => { + let deg = degrees.rem_euclid(360.0); + if (deg % 90.0).abs() < 1e-4 || (deg % 90.0 - 90.0).abs() < 1e-4 { + let quarters = ((deg + 45.0) / 90.0).floor() as i64; + let k = ((4 - quarters.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))?; + } else if let Some(t) = pdfcraft_edit::turn_by_degrees(img.visible_rect.unwrap_or(img.rect), *degrees) { 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]), @@ -2715,76 +2781,31 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> b = [b[0].min(x), b[1].min(y), b[2].max(x), b[3].max(y)]; } ai.rect = b; - if t[1].abs() < 1e-6 && t[2].abs() < 1e-6 { - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } else { - let _ = doc.update_dict(added_item.obj, |d| { - if let Some(pdfcraft_cos::Object::Dict(params)) = d.get_mut(b"PCAdded") { - params.set( - b"Rect".to_vec(), - pdfcraft_cos::Object::Array(ai.rect.iter().map(|x| pdfcraft_cos::Object::Real(*x)).collect()), - ); - } - }); - } - } - 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::RotateBy(degrees) => { - let deg = degrees.rem_euclid(360.0); - if (deg % 90.0).abs() < 1e-4 || (deg % 90.0 - 90.0).abs() < 1e-4 { - let quarters = ((deg + 45.0) / 90.0).floor() as i64; - let k = ((4 - quarters.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]; + let _ = doc.update_dict(added_item.obj, |d| { + if let Some(pdfcraft_cos::Object::Dict(params)) = d.get_mut(b"PCAdded") { + params.set( + b"Rect".to_vec(), + pdfcraft_cos::Object::Array(ai.rect.iter().map(|x| pdfcraft_cos::Object::Real(*x)).collect()), + ); } - pdfcraft_edit::update_content(doc, *page, added_idx, &AddedContent::Image(ai))?; - } else if let Some(t) = pdfcraft_edit::turn_by_degrees(img.visible_rect.unwrap_or(img.rect), *degrees) { - 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; - let _ = doc.update_dict(added_item.obj, |d| { - if let Some(pdfcraft_cos::Object::Dict(params)) = d.get_mut(b"PCAdded") { - params.set( - b"Rect".to_vec(), - pdfcraft_cos::Object::Array(ai.rect.iter().map(|x| pdfcraft_cos::Object::Real(*x)).collect()), - ); - } - }); - } + }); } - 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))?; - } - _ => {} } + 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))?; + } + ImageEdit::Arrange(_) | ImageEdit::Delete => { + let _ = doc.update_dict(added_item.obj, |d| { + d.remove(b"PCAdded"); + d.remove(b"PCMark"); + }); + } + _ => {} } } } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index cba800389e..93060a5c2b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -7304,3 +7304,23 @@ fn added_image_move_then_rotate_by_keeps_position() { let (a, b) = (c(moved), c(rotated)); assert!((a.0 - b.0).abs() < 1.0 && (a.1 - b.1).abs() < 1.0, "rotate_by moved the centre from {a:?} to {b:?}"); } + +/// Arranging an added image moves it into the document stream and converts it to an ordinary +/// page image (dropping the PCAdded marker), so no empty shell remains in list_added. +#[test] +fn added_image_arrange_converts_to_ordinary_image() { + let mut png = Vec::new(); + { + let mut enc = png::Encoder::new(&mut png, 2, 2); + enc.set_color(png::ColorType::Rgb); + enc.write_header().unwrap().write_image_data(&[255u8; 12]).unwrap(); + } + let (mut s, id) = session_with(1); + s.apply(id, Edit::AddImage { page: 0, rect: Some([20.0, 20.0, 60.0, 60.0]), name: "p1.png".into(), bytes: Arc::new(png.clone()) }).unwrap(); + s.apply(id, Edit::AddImage { page: 0, rect: Some([100.0, 100.0, 200.0, 200.0]), name: "p2.png".into(), bytes: Arc::new(png) }).unwrap(); + assert_eq!(s.get(id).unwrap().page_images(0).len(), 2); + assert_eq!(s.get(id).unwrap().added().len(), 2); + s.apply(id, Edit::EditPageImage { page: 0, index: 1, change: ImageEdit::Arrange(ArrangeOrder::SendToBack) }).unwrap(); + assert_eq!(s.get(id).unwrap().page_images(0).len(), 2); + assert_eq!(s.get(id).unwrap().added().len(), 1, "arranged image is converted and no longer an added-item shell"); +} diff --git a/crates/engine/src/text_fonts.rs b/crates/engine/src/text_fonts.rs index 79f358490b..ca55bffd32 100644 --- a/crates/engine/src/text_fonts.rs +++ b/crates/engine/src/text_fonts.rs @@ -54,13 +54,11 @@ pub fn discover(roots: &[PathBuf]) -> Vec { if meta.is_dir() && depth < 4 { if let Ok(children) = std::fs::read_dir(&path) { let remaining = 4096usize.saturating_sub(entries.saturating_add(pending.len())); - pending.extend(children.take(remaining).filter_map(Result::ok).map(|p| (p.path(), depth + 1))); + let mut sorted_children: Vec<_> = children.take(remaining).filter_map(Result::ok).map(|p| p.path()).collect(); + sorted_children.sort(); + pending.extend(sorted_children.into_iter().rev().map(|p| (p, depth + 1))); } } else if meta.is_file() && meta.len() <= (16 << 20) && font_extension(&path) { - if meta.len() > budget { - continue; - } - budget = budget.saturating_sub(meta.len()); let mut bytes = Vec::new(); let read = std::fs::File::open(&path).and_then(|f| f.take((16 << 20) + 1).read_to_end(&mut bytes)); if read.is_err() { @@ -69,6 +67,10 @@ pub fn discover(roots: &[PathBuf]) -> Vec { if pdfcraft_fonts::embedding::validate_document_input(&bytes).is_err() { continue; } + if meta.len() > budget { + continue; + } + budget = budget.saturating_sub(meta.len()); if let Ok((family, face)) = pdfcraft_fonts::embedding::names(&bytes) { let key = font_content_key(&bytes); if seen_face_content.insert((face.clone(), key)) { diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index f6b44a455d..43ebc6d9f2 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -888,15 +888,19 @@ impl crate::PdfCraftApp { Some(crate::create_ui::Clip::Text(_)) => None, None => { #[cfg(not(target_arch = "wasm32"))] - let sys_result = if self.system_clipboard { crate::os_clipboard::with(|cb| cb.get_image()).ok() } else { None }; + let sys_result = 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 + }; #[cfg(target_arch = "wasm32")] - let sys_result: Option> = None; + let sys_result: Option<(usize, usize, Vec)> = None; - if let Some(img) = sys_result { + if let Some((img_width, img_height, img_bytes)) = sys_result { let is_same_as_last = 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()); + .is_some_and(|(w, h, rgba)| *w as usize == img_width && *h as usize == img_height && rgba.as_slice() == img_bytes.as_slice()); if is_same_as_last && let Some((name, bytes)) = self.last_copied_file.clone() { Some((name, bytes)) } else if is_same_as_last && let Some((w, h, rgba)) = self.last_copied_image.clone() { @@ -904,9 +908,9 @@ impl crate::PdfCraftApp { } else { // System clipboard has a newer / external image! self.last_copied_file = None; - self.last_copied_image = Some((img.width as u32, img.height as u32, img.bytes.to_vec())); + self.last_copied_image = Some((img_width as u32, img_height as u32, img_bytes.clone())); self.last_copied_image_rect = None; - crate::create_ui::png_from_rgba(img.width, img.height, &img.bytes).ok().map(|b| ("clipboard_image.png".to_string(), b)) + crate::create_ui::png_from_rgba(img_width, img_height, &img_bytes).ok().map(|b| ("clipboard_image.png".to_string(), b)) } } else if let Some((name, bytes)) = self.last_copied_file.clone() { Some((name, bytes)) @@ -947,8 +951,12 @@ impl crate::PdfCraftApp { if let Some(r) = placed_rect { self.last_copied_image_rect = Some(r); } + 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) { v.current = page; + v.content.selected = Some((page, added_idx)); + v.content.select_added = Some((page, added_idx)); v.image_selection = Some(crate::edit_text_ui::ImageSelection { page, index: before, @@ -982,19 +990,23 @@ impl crate::PdfCraftApp { } } else { #[cfg(not(target_arch = "wasm32"))] - let sys_img = if self.system_clipboard { crate::os_clipboard::with(|cb| cb.get_image()).ok() } else { None }; + 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 + }; #[cfg(target_arch = "wasm32")] - let sys_img: Option> = None; + let sys_img: Option<(usize, usize, Vec)> = None; - if let Some(img) = sys_img { + 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_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) + (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]) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a94d987e59..24f663e599 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -3175,13 +3175,17 @@ impl PdfCraftApp { self.cut_page_image(sel.page, sel.index); } if paste { + let has_override_image = matches!(self.clipboard_override, Some(crate::create_ui::Clip::Image { .. })); #[cfg(not(target_arch = "wasm32"))] - let system_has_text = - if self.system_clipboard { crate::os_clipboard::with(|cb| cb.get_text()).map(|t| !t.is_empty()).unwrap_or(false) } else { false }; + let system_has_text = if !has_override_image && self.system_clipboard { + crate::os_clipboard::with(|cb| cb.get_text()).map(|t| !t.is_empty()).unwrap_or(false) + } else { + false + }; #[cfg(target_arch = "wasm32")] let system_has_text = false; - if paste_event_text.is_some() || system_has_text { + if !has_override_image && (paste_event_text.is_some() || system_has_text) { self.last_copied_image = None; self.last_copied_file = None; self.last_copied_image_rect = None;