diff --git a/crates/engine/src/cmd/fileio/tests_psdread.rs b/crates/engine/src/cmd/fileio/tests_psdread.rs index 4fe5f1e49b..6d66b8da4e 100644 --- a/crates/engine/src/cmd/fileio/tests_psdread.rs +++ b/crates/engine/src/cmd/fileio/tests_psdread.rs @@ -386,10 +386,32 @@ fn replacing_a_clipped_psd_keeps_its_scale() { assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}"); let (anchors, _, _) = clip_group(&s.doc().unwrap().doc, NodeId(r["ids"][0].as_u64().unwrap())); assert_eq!(anchors, [[200.0, 150.0], [300.0, 250.0], [100.0, 250.0]]); - // Other art replacing the clip group takes its centre only, as before. + // A plain image replacing the clip group keeps its scale too. let plain = vectorcraft_format::base64_encode(&red_psd(&[])); let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap(); - assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(100.0), Some(50.0)), "{r}"); + assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}"); +} + +/// A plain image replacing a clipped PSD file takes the clipped image's scale and rotation. +#[test] +fn a_plain_image_replacing_a_clipped_psd_keeps_its_rotation() { + let data = vectorcraft_format::base64_encode(&red_psd(&triangle_resources())); + let plain = vectorcraft_format::base64_encode(&red_psd(&[])); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let r = s.execute("file.place", &json!({"name": "cut.psd", "dataBase64": data, "at": [200, 200]})).unwrap(); + let group = NodeId(r["ids"][0].as_u64().unwrap()); + s.execute("object.scale", &json!({"sx": 200, "sy": 150})).unwrap(); + s.execute("object.rotate", &json!({"angle": 30})).unwrap(); + let xf = |s: &Session, id: NodeId| match s.doc().unwrap().doc.node(id).map(|n| &n.kind) { + Some(NodeKind::Image(im)) => im.xf.as_coeffs(), + other => panic!("not an image: {other:?}"), + }; + let (_, _, image) = clip_group(&s.doc().unwrap().doc, group); + let before = xf(&s, image); + let r = s.execute("file.place", &json!({"name": "plain.psd", "dataBase64": plain, "replace": true})).unwrap(); + let after = xf(&s, NodeId(r["ids"][0].as_u64().unwrap())); + assert!(after.iter().zip(before).all(|(a, b)| (a - b).abs() < 1e-9), "{after:?} != {before:?}"); } #[test] diff --git a/crates/engine/src/cmd/place.rs b/crates/engine/src/cmd/place.rs index 114f5b4f61..46231dcd0d 100644 --- a/crates/engine/src/cmd/place.rs +++ b/crates/engine/src/cmd/place.rs @@ -495,14 +495,14 @@ fn at_param(p: &Value) -> Result> { /// The transform that puts new art with the natural box `natural` where `old` is: an image's scale /// and rotation relative to its 100% size (its resolution's) and its centre are kept (with -/// `image_xf`), also for an image alone in a clip group when the new art is an image clipped by -/// its file's clipping path (`clipped`); other objects keep their centre. -fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, clipped: bool) -> Affine { +/// `image_xf`), also for an image alone in a clip group when the new art is an image, clipped by +/// its file's clipping path or not (`new_image`); other objects keep their centre. +fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool, new_image: bool) -> Affine { // An image alone in a clip group (as a PSD or PSB file's clipping path places it, - // [`fileio::file_clipped`]) is measured as the image when the new art brings its own clip - // group; a DCS file's plates are not. + // [`fileio::file_clipped`]) is measured as the image when the new art is an image; a DCS + // file's plates are not. let image = match &old.kind { - NodeKind::Group { children, clip: true } if clipped => match &children[..] { + NodeKind::Group { children, clip: true } if new_image => match &children[..] { [path, image] if matches!(path.kind, NodeKind::Path { clipping: true, .. }) => match &image.kind { NodeKind::Image(im) if d.images.get(&im.key).is_none_or(|b| b.separated().is_none()) => image, _ => old, @@ -681,7 +681,7 @@ fn apply(s: &mut Session, read: PlaceRead) -> Result { } let natural = loaded.natural; let m = match (old_node, rect) { - (Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(_, Some(_)))), + (Some(o), _) => replace_xf(&st.doc, o, natural, !is_text, matches!(loaded.art, Art::Image(..))), (None, Some(r)) => fit(natural, r), // Its artboard's bottom-left corner on the target's. (None, None) if let (Some(src), Some(to)) = (loaded.board, target) => Affine::translate((to.x0 - src.x0, to.y1 - src.y1)), diff --git a/docs/mcp.md b/docs/mcp.md index 494b4f4ef6..06e215a9a4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -8455,7 +8455,8 @@ that can be edited or released. `file.place` answers `image`, the id of the imag [import options help page](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) says that a file imported as one flattened image keeps the document's clipping path when it has one. `links.update` and `links.relink` read the file's pixels again and leave the clip group and its clipping path as they are. Files that name no clipping path, or name a path -they don't have, come in as a plain image. +they don't have, come in as a plain image. `file.place` with `replace` on such a clip group puts a new image, clipped or +not, at the scale and rotation of the image in the group. `document.open` converts a PSD or PSB file's layers to objects. In [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html),