mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 15:05:02 -06:00
Fixes suggested by Reviewers 2
This commit is contained in:
+44
-77
@@ -55,7 +55,7 @@ pub struct PageImage {
|
||||
pub smask: Option<MaskIdentity>,
|
||||
/// 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<Placement>,
|
||||
path: BezPath,
|
||||
subpath_start: Option<Point>,
|
||||
pending_clip: Option<bool>,
|
||||
}
|
||||
|
||||
impl ImageState {
|
||||
fn new(matrix: Matrix, clip: [f64; 4], clips: Vec<ClipPath>) -> 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<Point> {
|
||||
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<Point> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-21
@@ -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::<u8>(), 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]);
|
||||
}
|
||||
|
||||
+99
-78
@@ -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");
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -54,13 +54,11 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
|
||||
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<FontFile> {
|
||||
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)) {
|
||||
|
||||
@@ -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<arboard::ImageData<'static>> = None;
|
||||
let sys_result: Option<(usize, usize, Vec<u8>)> = 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<arboard::ImageData<'static>> = None;
|
||||
let sys_img: Option<(usize, usize, Vec<u8>)> = 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])
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user