mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Place: PSD files placed without Show Import Options take the last PSD Import Options
With Show Import Options off, a PSD or PSB file placed on its own and embedded takes the conversion, hidden-layer and Import Slices choices made last in PSD Import Options, as text and DXF files take theirs. An importSlices or hiddenLayers the params give is kept. Files from bytes, which have no file to link to, are embedded even when the Place dialog's Link is on, alone and in the place cursor. With Show Import Options on, each PSD file of the place cursor asks for its options.
This commit is contained in:
@@ -96,6 +96,28 @@ pub fn queue_options(app: &VectorcraftApp, q: &mut Value) {
|
||||
q["importSlices"] = json!(slices);
|
||||
}
|
||||
|
||||
/// `file.place` params `p` placed without asking (Show Import Options off) that embed a PSD or PSB
|
||||
/// file and give no `layers` take the PSD Import Options chosen last; `hiddenLayers` and
|
||||
/// `importSlices` that the params give are kept.
|
||||
pub fn last_options(app: &VectorcraftApp, p: &mut Value) {
|
||||
let embedded = p.get("path").is_none() || p.get("link").and_then(Value::as_bool) == Some(false);
|
||||
let name = p.get("path").or_else(|| p.get("name")).and_then(Value::as_str).unwrap_or_default();
|
||||
if embedded && p.get("layers").is_none() && matches!(fileio::extension(name).as_str(), "psd" | "pdd" | "psb") {
|
||||
let (layers, hidden, slices) = last_choices(app);
|
||||
p["layers"] = json!(layers);
|
||||
if layers && hidden && p.get("hiddenLayers").is_none() {
|
||||
p["hiddenLayers"] = json!(true);
|
||||
}
|
||||
if p.get("importSlices").is_none() {
|
||||
p["importSlices"] = json!(slices);
|
||||
}
|
||||
if p.get("path").is_none() {
|
||||
// A file from bytes has no file to link to; it may carry the Place dialog's Link.
|
||||
p["link"] = json!(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OK: open or place the file with the choices.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let file = app.ui.dialog_file.clone().ok_or("the file is no longer loaded")?;
|
||||
@@ -451,6 +473,102 @@ mod tests {
|
||||
assert_eq!(q["importSlices"], json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placing_with_show_import_options_off_takes_the_last_choices() {
|
||||
let b64 = vectorcraft_format::base64_encode(&sliced(&layered()));
|
||||
let mut app = app();
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
// The file's bytes carry the Place dialog's Link, which they have no file for.
|
||||
let place = |app: &mut VectorcraftApp, slices: Option<bool>| {
|
||||
let mut p = json!({"name": "poster.psd", "dataBase64": b64, "link": true, "showImportOptions": false});
|
||||
if let Some(s) = slices {
|
||||
p["importSlices"] = json!(s);
|
||||
}
|
||||
crate::place::run_for_user(app, &p).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "placed without asking");
|
||||
let st = app.session.doc().unwrap();
|
||||
let [id] = st.selection.objects[..] else { panic!("one object is selected") };
|
||||
(st.doc.node(id).unwrap().kind.clone(), st.doc.slices.len())
|
||||
};
|
||||
// At first, Convert Layers to Objects with Import Slices.
|
||||
let (kind, slices) = place(&mut app, None);
|
||||
assert!(matches!(kind, NodeKind::Group { .. }));
|
||||
assert_eq!(slices, 1);
|
||||
// An `importSlices` the params give is kept.
|
||||
let (kind, slices) = place(&mut app, Some(false));
|
||||
assert!(matches!(kind, NodeKind::Group { .. }));
|
||||
assert_eq!(slices, 1, "no new slice");
|
||||
// Then the choices made last.
|
||||
app.ui.psd_import = json!({"layers": false, "importSlices": false});
|
||||
let (kind, slices) = place(&mut app, None);
|
||||
assert!(matches!(kind, NodeKind::Image(_)));
|
||||
assert_eq!(slices, 1, "no new slice");
|
||||
let (_, slices) = place(&mut app, Some(true));
|
||||
assert_eq!(slices, 2, "the given importSlices brings the slice in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_from_bytes_placed_with_link_take_the_last_choices() {
|
||||
let files: Vec<Value> = ["a.psd", "b.psd"].iter().map(|n| json!({ "name": n })).collect();
|
||||
let mut app = app();
|
||||
for n in ["a.psd", "b.psd"] {
|
||||
app.place.picked.push((n.to_string(), sliced(&layered())));
|
||||
}
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
// The dialog's Link is on, but files from bytes have no file to link to.
|
||||
let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": true, "template": false, "showImportOptions": false}));
|
||||
crate::place::confirm(&mut app, &d).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "the cursor loads without asking");
|
||||
let view = vectorcraft_engine::ViewInfo::default();
|
||||
for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] {
|
||||
for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() {
|
||||
if let vectorcraft_engine::UiRequest::Place(p) = r {
|
||||
crate::place::place_from_cursor(&mut app, &p);
|
||||
}
|
||||
}
|
||||
}
|
||||
let st = app.session.doc().unwrap();
|
||||
let [id] = st.selection.objects[..] else { panic!("one object is placed") };
|
||||
// At first, Convert Layers to Objects with Import Slices.
|
||||
assert!(matches!(st.doc.node(id).unwrap().kind, NodeKind::Group { .. }));
|
||||
assert_eq!(st.doc.slices.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cursor_loaded_with_show_import_options_on_asks_for_each_psd_file() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-psd-import-asks-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let paths: Vec<String> = ["a.psd", "b.psd"]
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let p = dir.join(n);
|
||||
std::fs::write(&p, layered()).unwrap();
|
||||
p.to_string_lossy().into_owned()
|
||||
})
|
||||
.collect();
|
||||
let files: Vec<Value> = paths.iter().map(|p| json!({ "path": p })).collect();
|
||||
let services = crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), ..Default::default() };
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
// The choices made last would place the layers without asking with Show Import Options off.
|
||||
app.ui.psd_import = json!({"layers": true, "importSlices": true});
|
||||
let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": false, "template": false, "showImportOptions": true}));
|
||||
crate::place::confirm(&mut app, &d).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "the cursor loads without asking");
|
||||
assert_eq!(app.session.tool_options()["count"], 2);
|
||||
// A click on the canvas: the file the cursor's request names asks.
|
||||
let view = vectorcraft_engine::ViewInfo::default();
|
||||
for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] {
|
||||
for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() {
|
||||
if let vectorcraft_engine::UiRequest::Place(p) = r {
|
||||
crate::place::place_from_cursor(&mut app, &p);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND), "the clicked file asks");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scripted_places_and_places_that_give_layers_dont_ask() {
|
||||
let b64 = vectorcraft_format::base64_encode(&layered());
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn is_document(name: &str) -> bool {
|
||||
#[derive(Default)]
|
||||
pub struct PlaceState {
|
||||
/// Web: the bytes of the files picked for the Place dialog, by name.
|
||||
picked: Vec<(String, Vec<u8>)>,
|
||||
pub(crate) picked: Vec<(String, Vec<u8>)>,
|
||||
/// The loaded cursor's thumbnails by file name.
|
||||
thumbs: Vec<(String, Thumb)>,
|
||||
/// The Control bar's `image.info`, for (document uid, revision, image id).
|
||||
@@ -113,10 +113,11 @@ fn engine(app: &mut VectorcraftApp, id: &str, p: &Value) -> Result<Value, String
|
||||
|
||||
/// File → Place… (`file.place`): with a file (`path` or `name` + `dataBase64`), place it, centred
|
||||
/// in the view unless `at`, `rect` or `replace` say otherwise (a text file without `text` options,
|
||||
/// a DXF drawing without `dxf` options and a PDF with several pages or a password and no `page`
|
||||
/// ask for them first, unless `showImportOptions: false` places them with the options confirmed
|
||||
/// last); with `paths`, the Place dialog for them; with neither, pick the files first. The file is
|
||||
/// read and placed at once (MCP, scripts).
|
||||
/// a DXF drawing without `dxf` options, a PSD file embedded without `layers` (when the user places
|
||||
/// it) and a PDF with several pages or a password and no `page` ask for them first, unless
|
||||
/// `showImportOptions: false` places them with the options confirmed last); with `paths`, the
|
||||
/// Place dialog for them; with neither, pick the files first. The file is read and placed at once
|
||||
/// (MCP, scripts).
|
||||
pub fn run(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
place_file(app, p, Read::AtOnce)
|
||||
}
|
||||
@@ -155,6 +156,7 @@ fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value,
|
||||
if let Some(dxf) = crate::dialogs::dxf_import::last_options(app, &p) {
|
||||
p["dxf"] = dxf;
|
||||
}
|
||||
crate::dialogs::psd_import::last_options(app, &mut p);
|
||||
}
|
||||
if p.get("text").is_none() && crate::dialogs::text_import::is_text(&p) {
|
||||
return Ok(crate::dialogs::text_import::open(app, &p));
|
||||
@@ -390,7 +392,9 @@ pub fn confirm(app: &mut VectorcraftApp, d: &crate::state::Dialog) -> Result<Val
|
||||
// confirmed last.
|
||||
q["text"] = crate::dialogs::text_import::last_options(app);
|
||||
q["dxf"] = crate::dialogs::dxf_import::remembered(app);
|
||||
if !link {
|
||||
if !link || paths.is_empty() {
|
||||
// Files from bytes have no file to link to; they may carry the dialog's Link.
|
||||
q["link"] = json!(false);
|
||||
crate::dialogs::psd_import::queue_options(app, &mut q);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -7023,10 +7023,12 @@ with `pointer_gesture` (a click places at 100% with the top-left corner there, a
|
||||
number, naming the key, and place nothing; null is the same as leaving the key out.
|
||||
|
||||
In the app, `file.place` asks for a file's import options before placing it: a text file without `text` (Text Import
|
||||
Options), a DXF drawing without `dxf` (DXF Import Options), and a PDF, `.ai` or `.ait` file with several pages or a
|
||||
password and no `page` (Place PDF). `showImportOptions: false` places such a file without asking, with the options
|
||||
last confirmed in its dialog: the text options, and the DXF fit, lineweight, centering and layer choices with the
|
||||
drawing at 1:1 in its own unit. A PDF is placed on page 1 (or `page`) and its crop box, and an encrypted PDF given no
|
||||
Options), a DXF drawing without `dxf` (DXF Import Options), a PDF, `.ai` or `.ait` file with several pages or a
|
||||
password and no `page` (Place PDF), and a PSD or PSB file embedded without `layers` that has two layers or more or
|
||||
slices (Place PSD). `showImportOptions: false` places such a file without asking, with the options last confirmed in
|
||||
its dialog: the text options, the DXF fit, lineweight, centering and layer choices with the drawing at 1:1 in its own
|
||||
unit, and the PSD conversion, hidden-layer and Import Slices choices. A PDF is placed on page 1 (or `page`) and its
|
||||
crop box, and an encrypted PDF given no
|
||||
`password` still asks for the password. A `showImportOptions` that is not true or false is refused, and nothing is placed. The
|
||||
Place dialog (File › Place, or `file.place {paths}`) has a Show Import Options checkbox after Link, Template and
|
||||
Replace ([Place multiple files in a document](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-other-file-types/place-multiple-files-in-a-document.html)).
|
||||
|
||||
@@ -1839,7 +1839,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- File › Open converts a PSD or PSB file's layers to objects, keeping the layer structure, merging clipped layers into one layer and leaving out hidden layers that would need a merge, as [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) describes for the option that converts layers to objects; hidden layers come in with `hiddenLayers`, as Import Hidden Layers brings them in, and `layers: false` opens the merged image. VectorCraft's own rules: each layer becomes a layer holding its pixels with its name, visibility, opacity and blend mode, groups become layers of sublayers, layers in Dissolve, Linear Burn, Linear Dodge, Vivid Light, Linear Light, Pin Light, Hard Mix, Subtract, Divide, Darker Color and Lighter Color are merged with the layers below, layer masks become opacity masks, fill opacity multiplies the opacity (in a layer that others are clipped to, it fades only that layer's own pixels), and adjustment layers, layer effects, knockouts and vector masks aren't applied and are counted in the warnings. 8-, 16- and 32-bit files, PSB, raw, PackBits and zipped channels (`crates/engine/src/cmd/fileio/tests_psdlayers.rs`).
|
||||
- A PSD or PSB file's clipping path, the saved path that image resource 2999 names in the [PSD file format specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), clips its merged image on File › Place and on File › Open of the merged image, as [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) describes for a file imported as one flattened image. The image comes in a clip group whose clipping path has no paint and uses the even-odd rule, and the path can be edited or released. `links.update` and `links.relink` keep the clip group, and files that name no clipping path come in as before (`crates/engine/src/cmd/fileio/tests_psdread.rs`).
|
||||
- File › Open of a PSD or PSB file with two or more layers, and File › Place of one without Link, show PSD Import Options: Convert Layers to Objects with Import Hidden Layers, or Flatten Layers to a Single Image, the options [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) describes for a file that is opened or embedded. A file placed with Link shows its flat image and isn't asked about. `file.place` takes `layers` and `hiddenLayers`, and `document.psdInfo` counts a file's layers without opening it. VectorCraft's own rules: placed layers come in as one group named after the file, a layer as an image and a layer group as a group, at the place the merged image would take and clipped to the canvas when layers reach past it; the dialog asks from two layers up and starts from the choices made last. Layer comps aren't read (`crates/engine/src/cmd/fileio/tests_psdlayers.rs`, `crates/ui-egui/src/dialogs/psd_import.rs`).
|
||||
- Import Slices: File › Open of a PSD or PSB file with slices, and File › Place of one without Link, bring its slices in as user slices, as [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) describes for a file that is opened or embedded; PSD Import Options offers Import Slices for such a file. `document.open` and `file.place` take `importSlices`, and `document.psdInfo` counts the slices. The slices are read from image resource 1050 in version 6 and in the descriptor of versions 7 and 8, as the [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) describes it. VectorCraft's own rules: Import Slices applies to Convert Layers to Objects and to Flatten Layers to a Single Image alike, and makes the dialog ask for a file with fewer than two layers; placed slices move and scale with the art in the same undo step; each slice keeps its name, type, link, target, message, alternative text, cell text and background color; slices the file generated automatically, and slices wholly outside the canvas, are left out; slices that can't be read are left out with a warning while the art comes in (`crates/engine/src/cmd/fileio/tests_psdslices.rs`, `crates/ui-egui/src/dialogs/psd_import.rs`).
|
||||
- Import Slices: File › Open of a PSD or PSB file with slices, and File › Place of one without Link, bring its slices in as user slices, as [PSD import options](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html) describes for a file that is opened or embedded; PSD Import Options offers Import Slices for such a file. `document.open` and `file.place` take `importSlices`, and `document.psdInfo` counts the slices. The slices are read from image resource 1050 in version 6 and in the descriptor of versions 7 and 8, as the [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) describes it. VectorCraft's own rules: Import Slices applies to Convert Layers to Objects and to Flatten Layers to a Single Image alike, and makes the dialog ask for a file with fewer than two layers; placed slices move and scale with the art in the same undo step; each slice keeps its name, type, link, target, message, alternative text, cell text and background color; slices the file generated automatically, and slices wholly outside the canvas, are left out; slices that can't be read are left out with a warning while the art comes in; with Show Import Options off, a PSD file is placed with the PSD Import Options chosen last, and on, each PSD file of the place cursor asks for them (`crates/engine/src/cmd/fileio/tests_psdslices.rs`, `crates/ui-egui/src/dialogs/psd_import.rs`).
|
||||
- PCX files (`.pcx`) open and place, from the ZSoft PCX Technical Reference Manual: 1 bit in the header palette's first two colors (black and white when the version has no palette or both colors are the same), 1 bit in two to four planes and 4 bits with up to 16 colors from the header's palette (the EGA colors for versions 0 and 3), 2 bits in the CGA colors that the header's CGA palette settings pick (background, color burst, palette and intensity), 8 bits with the 256-color palette after the image data (gray levels without one) and 24-bit color in three planes, run-length encoded. The header's HDpi and VDpi size the image. A file of another depth or plane count, one cut short, and one whose pixels as RGBA pass the pixel allowance are refused with a message before its pixels are decoded. Open and Place recognize a PCX file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a PCX file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_pcxread.rs`).
|
||||
- Pixar picture files (`.pxr`) open, place and paste, from "The Format of Stored Pictures" in the Pixar Image Computer manuals: any set of the R, G, B and A channels in 8 or 12 bits, dumped or run-length encoded with all four packet kinds and block ends, in tiles of any size (pixels of border tiles past the picture are dropped). R alone is gray, a missing color channel is 0 and a missing A opaque; 12-bit samples are clamped to 0.0 to 1.0; color matted to black becomes straight color; a null tile is transparent when the picture has A and black otherwise. The offsets aren't read, and the image takes the default resolution. Version 2 headers read like version 0. A file cut short, one with a tile whose writing wasn't finished, one with header values or packets the format doesn't define, and one whose pixels as RGBA pass the pixel allowance are refused with a message. Every reader recognizes a Pixar file by its magic number. Tested on synthetic files only (`crates/engine/src/cmd/fileio/tests_pxrread.rs`).
|
||||
- JPEG 2000 JP2 and raw codestream images open and place from `.jpf`, `.jpx`, `.jp2`, `.j2k`, `.j2c` and `.jpc`. Each axis uses the valid display resolution (`resd`), else the valid capture resolution (`resc`), whichever box comes first ([ITU-T T.800](https://www.itu.int/rec/T-REC-T.800/en), I.5.3.7.2); an axis with none uses 72 ppi. Multiple codestreams, JPX composition or layers, and unsupported color spaces are refused with a message. The decoder checks image size before allocating (`crates/engine/src/cmd/fileio/tests_jp2read.rs`).
|
||||
|
||||
Reference in New Issue
Block a user