Merge pull request #7746 from dotCooCoo/fix/replace-clipped-psd-keeps-transform

Place: a plain image replacing a clipped PSD keeps its scale and rotation
This commit is contained in:
BFlat
2026-11-15 13:49:22 +07:00
committed by GitHub
3 changed files with 33 additions and 10 deletions
+24 -2
View File
@@ -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]
+7 -7
View File
@@ -495,14 +495,14 @@ fn at_param(p: &Value) -> Result<Option<Point>> {
/// 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<Value> {
}
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)),
+2 -1
View File
@@ -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),