Place: Show Import Options asks for each file's import options or uses the last ones

The Place dialog gets a Show Import Options checkbox after Link, Template
and Replace. On (the default, saved with the preferences), each PDF, DXF
or text file asks for its import options before it is placed, also each
file of the place cursor when it is clicked or dragged. Off, each file is
placed with the options last confirmed for its format: the Text Import
Options, the DXF fit, lineweight, centering and layer choices, a PDF's
first page and crop box. An encrypted PDF still asks for its password.

file.place takes showImportOptions (true or false; anything else is an
error). Text Import Options now remembers its options and starts from
them.
This commit is contained in:
dotCooCoo
2026-11-14 18:08:06 -08:00
parent dafd993395
commit 098e819a04
39 changed files with 463 additions and 41 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Place…",
["File"],
Some("Cmd+Shift+P"),
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, unless password or textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark, or without one by dataset.import's rule: an even length, at least half of its two-byte units holding a NUL byte, the byte order that reads a line break, valid UTF-16 with no NUL character; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect; refused while an object of a symbol instance's art is selected), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings, image?: the id of the image of a DCS file placed in a clip group, which the Links commands take}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, HP-GL (.plt, .hpgl, .hgl, .hpg), PDF/.ai, EMF/WMF and PICT (.pct, .pic, .pict; clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns, swatches and brushes they use. A DCS file (an EPS file with one program per printing plate, DCS 1.0 or 2.0) is placed only from its path with link (else an error, as are document.open and links.embed): one linked image of its plates mixed into CMYK, clipped by the clipping path its composite is drawn through, its spot plates added as spot swatches in the process equivalents the file gives (a swatch of the same name is kept, with a warning); print separations and Separations Preview show each plate on its own ink, and plate files of their own are read from its folder. One undo step; selects what it placed (unless on a template layer); never touches the clipboard. A PNG, JPEG, GIF or WebP image whose pixels can't be read (a file cut short) is an error, and nothing is placed",
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, unless password or textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark, or without one by dataset.import's rule: an even length, at least half of its two-byte units holding a NUL byte, the byte order that reads a line break, valid UTF-16 with no NUL character; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect; refused while an object of a symbol instance's art is selected), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at), showImportOptions?: true (in the app, a text file without text, a DXF drawing without dxf and a PDF with several pages or a password and no page ask for those options first; false places them with the options last confirmed, a PDF on page 1 (or page), and asks only an encrypted PDF's password)} → {ids, name, format, linked, width, height, warnings, image?: the id of the image of a DCS file placed in a clip group, which the Links commands take}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, HP-GL (.plt, .hpgl, .hgl, .hpg), PDF/.ai, EMF/WMF and PICT (.pct, .pic, .pict; clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns, swatches and brushes they use. A DCS file (an EPS file with one program per printing plate, DCS 1.0 or 2.0) is placed only from its path with link (else an error, as are document.open and links.embed): one linked image of its plates mixed into CMYK, clipped by the clipping path its composite is drawn through, its spot plates added as spot swatches in the process equivalents the file gives (a swatch of the same name is kept, with a warning); print separations and Separations Preview show each plate on its own ink, and plate files of their own are read from its folder. One undo step; selects what it placed (unless on a template layer); never touches the clipboard. A PNG, JPEG, GIF or WebP image whose pixels can't be read (a file cut short) is an error, and nothing is placed",
has_doc,
place
),
+18
View File
@@ -84,6 +84,24 @@ pub fn offer_place(app: &mut VectorcraftApp, p: &Value) -> bool {
offer(app, &name, &bytes, path, Some(p))
}
/// The `dxf` options a drawing `file.place` params `p` name is placed with when Show Import Options
/// is off: the choices confirmed last (fit, lineweights, centring, layers), the drawing at 1:1 in
/// its own unit. `None` when `p` names no `.dxf` drawing or already gives `dxf` options.
pub fn last_options(app: &VectorcraftApp, p: &Value) -> Option<Value> {
let named = p.get("path").or_else(|| p.get("name")).and_then(Value::as_str).map(fileio::extension);
if p.get("dxf").is_some() || named.as_deref() != Some("dxf") {
return None;
}
Some(remembered(app))
}
/// The `dxf` options of the choices confirmed last (fit, lineweights, centring, layers; `{}` when
/// none was).
pub fn remembered(app: &VectorcraftApp) -> Value {
let last = app.ui.dxf_import.as_object();
Value::Object(REMEMBERED.iter().filter_map(|k| last?.get(*k).filter(|v| v.is_boolean()).map(|v| (k.to_string(), v.clone()))).collect())
}
/// The `dxf` options of `document.open` and `file.place` the fields set.
fn options(d: &Dialog) -> Value {
let fit = d.bool("fit");
+38 -4
View File
@@ -4,7 +4,8 @@
//! Fields: `mode` (`open` | `place`), `name`, `path?`, `pageCount`, `page` (the page shown, and the
//! one placed; 1-based), `allPages`, `range` and `linkPages` (open: Import PDF Pages as Links, for
//! a file read from a path and opened without a password), `cropTo` and `password`. `__locked` is set
//! until the password is accepted: OK then unlocks the file. `__place` keeps the other `file.place`
//! until the password is accepted: OK then unlocks the file, and with `__passwordOnly` (Place with
//! Show Import Options off) places its first page at once. `__place` keeps the other `file.place`
//! params (Link, Template, `at`…). The file's bytes stay out of the fields, in
//! [`crate::state::UiState::dialog_file`].
@@ -168,6 +169,33 @@ pub fn offer_place(app: &mut VectorcraftApp, p: &Value) -> bool {
offer(app, &name, &bytes, path, Some(p))
}
/// [`offer_place`] when Show Import Options is off: asks (true) only for the password of an
/// encrypted PDF given no `password`, which nothing else opens, and places its first page (or
/// `page`) and crop box once it is unlocked; any other PDF is placed that way without asking.
pub fn offer_place_password(app: &mut VectorcraftApp, p: &Value) -> bool {
// A `page` that isn't a page number is left for `file.place` to refuse.
if p.get("password").is_some() || p.get("page").is_some_and(|v| v.as_u64().is_none_or(|n| n == 0)) {
return false;
}
let Some((name, bytes, path)) = place_source(app, p, |ext| matches!(ext, "pdf" | "ai" | "ait")) else { return false };
if !matches!(vectorcraft_pdf::open_pdf(&bytes, None).map(|_| ()), Err(PdfError::NeedsPassword)) {
return false;
}
let offered = offer(app, &name, &bytes, path, Some(p));
if let (true, Some(d)) = (offered, app.ui.dialog.as_mut()) {
d.fields.insert("__passwordOnly".into(), json!(true));
// A `page` and `crop` given are placed as given, for `file.place` to check.
if let Some(place) = d.fields.get_mut("__place").and_then(Value::as_object_mut) {
for k in ["page", "crop"] {
if let Some(v) = p.get(k) {
place.insert(k.into(), v.clone());
}
}
}
}
offered
}
fn page_count(d: &Dialog) -> usize {
d.fields.get("pageCount").and_then(Value::as_u64).unwrap_or(0) as usize
}
@@ -204,7 +232,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let info = vectorcraft_pdf::info(&file.bytes, password(&d).as_deref()).map_err(|e| e.to_string())?;
d.fields.insert("__locked".into(), json!(false));
d.fields.insert("pageCount".into(), json!(info.pages.len()));
if info.pages.len() > 1 {
if info.pages.len() > 1 && !d.bool("__passwordOnly") {
// Unlocked: now pick the pages.
app.ui.dialog = Some(d);
return Ok(Value::Null);
@@ -214,8 +242,14 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let path = d.fields.get("path").and_then(Value::as_str).map(str::to_string);
let r = if d.str("mode") == "place" {
let mut p = with_file(d.fields.get("__place"), &d, &file);
p["page"] = json!(page(&d));
p["crop"] = json!(crop(&d).id());
// Asked only for the password: a `page` and `crop` given with the file stand.
let [page_given, crop_given] = ["page", "crop"].map(|k| d.bool("__passwordOnly") && p.get(k).is_some());
if !page_given {
p["page"] = json!(page(&d));
}
if !crop_given {
p["crop"] = json!(crop(&d).id());
}
if let Some(pw) = password(&d) {
p["password"] = json!(pw);
}
+11 -5
View File
@@ -1,8 +1,9 @@
//! File → Place: the files picked, one line each about what they are, and the Place options row
//! (Link, Template, Replace). OK places one file centred in the view (or in place of the selected
//! object) or loads the place cursor with several ([`crate::place::confirm`]).
//! (Link, Template, Replace, Show Import Options). OK places one file centred in the view (or in
//! place of the selected object) or loads the place cursor with several ([`crate::place::confirm`]).
//!
//! Fields: `files` (`[{path} | {name}]`), `link`, `template`, `replace`, and `__replace` (one file
//! Fields: `files` (`[{path} | {name}]`), `link`, `template`, `replace`, `showImportOptions` (each
//! PDF, DXF or text file asks for its import options before it is placed), and `__replace` (one file
//! and one selected object: Replace applies), `__documents` and `__others` (VectorCraft documents
//! from files are picked, other files are: what Link's tooltip explains), `__info` (a line per
//! file; on the desktop the lines are read off the UI thread and are empty until they are read,
@@ -29,10 +30,15 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
};
/// The options row: (field, label, tooltip).
const OPTIONS: [(&str, &str, &str); 3] = [
const OPTIONS: [(&str, &str, &str); 4] = [
("link", "Link", "Keep a link to an image file instead of only embedding it"),
("template", "Template", "Place onto a new locked, dimmed template layer below the current layer"),
("replace", "Replace", "Swap the selected object for the file, keeping its place and transform"),
(
"showImportOptions",
"Show Import Options",
"Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last",
),
];
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
@@ -73,7 +79,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
}
ui.add_space(10.0);
// The options row.
ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 18.0;
for (key, label, tip) in OPTIONS {
let on = d.bool(key);
+29 -4
View File
@@ -1,6 +1,7 @@
//! Text Import Options: placing a text file asks how to read it (platform and character set) and
//! what to clean up (line breaks inside paragraphs, blank lines between them, runs of spaces). OK
//! places it as area type (`file.place` with `text`).
//! places it as area type (`file.place` with `text`) and remembers the options: the next text file
//! starts from them, and is placed with them when Show Import Options is off.
//!
//! Fields: `platform` (`windows` | `mac`), `characterSet` (`unicode` | `ansi`),
//! `removeLineReturns`, `removeParagraphReturns`, `replaceSpaces` (on or off), `spaces` (how many
@@ -27,9 +28,10 @@ pub fn is_text(p: &Value) -> bool {
vectorcraft_engine::cmd::fileio::TEXT_EXTS.contains(&vectorcraft_engine::cmd::fileio::extension(name).as_str())
}
/// Ask how to place the text file `params` (`file.place` params) names.
/// Ask how to place the text file `params` (`file.place` params) names, starting from the options
/// confirmed last.
pub fn open(app: &mut VectorcraftApp, params: &Value) -> Value {
let fields = json!({
let mut fields = json!({
"platform": if cfg!(target_os = "macos") { "mac" } else { "windows" },
"characterSet": "unicode",
"removeLineReturns": false,
@@ -38,10 +40,31 @@ pub fn open(app: &mut VectorcraftApp, params: &Value) -> Value {
"spaces": 3,
"__params": params,
});
let last = &app.ui.text_import;
for (k, wanted) in [("platform", ["windows", "mac"]), ("characterSet", ["unicode", "ansi"])] {
if let Some(v) = last.get(k).and_then(Value::as_str).filter(|v| wanted.contains(v)) {
fields[k] = json!(v);
}
}
for k in ["removeLineReturns", "removeParagraphReturns"] {
if let Some(v) = last.get(k).and_then(Value::as_bool) {
fields[k] = json!(v);
}
}
if let Some(n) = last.get("replaceSpaces").and_then(Value::as_u64).filter(|n| (2..=100).contains(n)) {
fields["replaceSpaces"] = json!(true);
fields["spaces"] = json!(n);
}
app.ui.dialog = Some(Dialog::new(KIND, fields));
json!({ "dialog": KIND })
}
/// The `text` options a text file is placed with when Show Import Options is off: those confirmed
/// last, else `file.place`'s defaults.
pub fn last_options(app: &VectorcraftApp) -> Value {
if app.ui.text_import.is_object() { app.ui.text_import.clone() } else { json!({}) }
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
widgets::subheader(ui, tl!("Encoding"));
form::choice(ui, d, "platform", tl!("Platform:"), (LABEL, 180.0), &[("windows", "Windows"), ("mac", "Mac")]);
@@ -87,10 +110,12 @@ fn options(d: &Dialog) -> Value {
o
}
/// Places the file with the options; closes first, so an error shows in the status bar.
/// Places the file with the options and remembers them for the next text file; closes first, so an
/// error shows in the status bar.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let mut params = d.fields.get("__params").cloned().unwrap_or_else(|| json!({}));
params["text"] = options(d);
app.ui.text_import = params["text"].clone();
app.ui.dialog = None;
crate::place::run_for_user(app, &params)
}
+2
View File
@@ -3515,6 +3515,8 @@ digits Malayalam Малаялам
Swap Fill and Stroke (Shift+X) Размяна на запълването и щриха (Shift+X)
Swap start and end arrowheads Размяна на началната и крайната стрелка
Swap the selected object for the file, keeping its place and transform Замяна на избрания обект с файла, като се запазват мястото и трансформациите му
Show Import Options Показване на опциите за импортиране
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Питане за опциите за импортиране на всеки PDF, DXF или текстов файл преди поставянето му; изключено: използват се последно избраните опции
Stylistic Alternates Стилистични алтернативи
Set {n} Набор {n}
Swash Декоративен елемент
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2749,6 +2749,8 @@ digits Malayalam Malajalam
Keep a link to an image file instead of only embedding it Zadrži vezu na slikovnu datoteku umjesto samo ugrađivanja
Place onto a new locked, dimmed template layer below the current layer Postavi na novi zaključani, zatamnjeni sloj predloška ispod trenutnog sloja
Swap the selected object for the file, keeping its place and transform Zamijeni odabrani objekat datotekom, zadržavajući njegovo mjesto i transformaciju
Show Import Options Prikaži opcije uvoza
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Pitaj za opcije uvoza svake PDF, DXF ili tekstualne datoteke prije postavljanja; isključeno: koriste se posljednje odabrane opcije
On: one locked object showing the document, updated when it changes. Uključeno: jedan zaključani objekat koji prikazuje dokument, ažuriran kad se on promijeni.
Off: an editable copy of its art, embedded in this document. Isključeno: kopija njegovog crteža koja se može uređivati, ugrađena u ovaj dokument.
Images: link to the file instead of only embedding it. Slike: poveži s datotekom umjesto samo ugrađivanja.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4587,6 +4587,8 @@ digits Malayalam Malaiàlam
Swap Fill and Stroke (Shift+X) Intercanvia el farciment i el traç (Maj+X)
Swap start and end arrowheads Intercanvia les puntes de fletxa inicial i final
Swap the selected object for the file, keeping its place and transform Substitueix l’objecte seleccionat pel fitxer, mantenint-ne la posició i la transformació
Show Import Options Mostra les opcions d’importació
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Demana les opcions d’importació de cada fitxer PDF, DXF o de text abans de col·locar-lo; desactivat: s’usen les últimes opcions triades
Stylistic Alternates Alternatives estilístiques
Swash Volada
Swatch Conflict Conflicte de mostres
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2784,6 +2784,8 @@ digits Malayalam Malajálamské
Keep a link to an image file instead of only embedding it Ponechat propojení na soubor obrázku, ne jen ho vložit
Place onto a new locked, dimmed template layer below the current layer Umístit do nové zamčené, ztlumené vrstvy šablony pod aktuální vrstvou
Swap the selected object for the file, keeping its place and transform Nahradit vybraný objekt souborem se zachováním jeho polohy a transformace
Show Import Options Zobrazit možnosti importu
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Před umístěním se zeptat na možnosti importu každého souboru PDF, DXF nebo textového souboru; vypnuto: použijí se naposledy zvolené možnosti
On: one locked object showing the document, updated when it changes. Zapnuto: jeden zamčený objekt zobrazující dokument, aktualizovaný při jeho změně.
Off: an editable copy of its art, embedded in this document. Vypnuto: upravitelná kopie jeho kresby vložená do tohoto dokumentu.
Images: link to the file instead of only embedding it. Obrázky: propojit se souborem místo pouhého vložení.
Can't render this file because it is too large.
+2
View File
@@ -3175,6 +3175,8 @@ digits Malayalam Malayalam
Keep a link to an image file instead of only embedding it Behold et link til en billedfil i stedet for kun at indlejre den
Place onto a new locked, dimmed template layer below the current layer Placér på et nyt låst, nedtonet skabelonlag under det aktuelle lag
Swap the selected object for the file, keeping its place and transform Udskift det markerede objekt med filen, mens placering og transformering bevares
Show Import Options Vis importindstillinger
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Spørg om importindstillinger for hver PDF-, DXF- eller tekstfil, før den placeres; fra: brug de senest valgte indstillinger
On: one locked object showing the document, updated when it changes. Til: ét låst objekt, der viser dokumentet, og som opdateres, når det ændres.
Off: an editable copy of its art, embedded in this document. Fra: en redigerbar kopi af dens grafik, indlejret i dette dokument.
Images: link to the file instead of only embedding it. Billeder: link til filen i stedet for kun at indlejre den.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3578,6 +3578,8 @@ effect Points Punkte
Swap Fill and Stroke (Shift+X) Fläche und Kontur vertauschen (Shift+X)
Swap start and end arrowheads Pfeilspitzen an Anfang und Ende vertauschen
Swap the selected object for the file, keeping its place and transform Das ausgewählte Objekt durch die Datei ersetzen; Position und Transformation bleiben erhalten
Show Import Options Importoptionen anzeigen
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Vor dem Platzieren nach den Importoptionen jeder PDF-, DXF- oder Textdatei fragen; aus: die zuletzt gewählten Optionen verwenden
Stylistic Alternates Stilistische Varianten
Swash Schwungschrift
Swatch Conflict Farbfeldkonflikt
Can't render this file because it is too large.
+2
View File
@@ -4102,6 +4102,8 @@ digits Malayalam Μαλαγιάλαμ
Swap Fill and Stroke (Shift+X) Εναλλαγή γεμίσματος και περιγράμματος (Shift+X)
Swap start and end arrowheads Εναλλαγή αρχικής και τελικής αιχμής βέλους
Swap the selected object for the file, keeping its place and transform Αντικατάσταση επιλεγμένου αντικειμένου με το αρχείο, διατηρώντας θέση και μετασχηματισμό
Show Import Options Εμφάνιση επιλογών εισαγωγής
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Ερώτηση για τις επιλογές εισαγωγής κάθε αρχείου PDF, DXF ή κειμένου πριν από την τοποθέτηση· απενεργοποιημένο: χρήση των τελευταίων επιλογών
Swatch Conflict Διένεξη δείγματος χρώματος
Swatch Conflicts Διενέξεις δειγμάτων χρωμάτων
Swatch exchange files keep solid colors only. Τα αρχεία ανταλλαγής δειγμάτων διατηρούν μόνο συμπαγή χρώματα.
Can't render this file because it is too large.
+2
View File
@@ -3607,6 +3607,8 @@ digits Malayalam Malayálam
Swap Fill and Stroke (Shift+X) Intercambiar relleno y trazo (Shift+X)
Swap start and end arrowheads Intercambiar puntas de flecha inicial y final
Swap the selected object for the file, keeping its place and transform Sustituir el objeto seleccionado por el archivo, conservando su posición y transformación
Show Import Options Mostrar opciones de importación
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Preguntar las opciones de importación de cada archivo PDF, DXF o de texto antes de colocarlo; desactivado: usar las últimas opciones elegidas
Stylistic Alternates Alternativas estilísticas
Swash Decorativa
Swatch Conflict Conflicto de muestras
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2728,6 +2728,8 @@ digits Malayalam Malajalam
Keep a link to an image file instead of only embedding it Säilytä linkki kuvatiedostoon sen sijaan, että kuva vain upotetaan
Place onto a new locked, dimmed template layer below the current layer Sijoita uudelle lukitulle, himmennetylle mallitasolle nykyisen tason alapuolelle
Swap the selected object for the file, keeping its place and transform Korvaa valittu objekti tiedostolla säilyttäen sen paikan ja muunnoksen
Show Import Options Näytä tuontiasetukset
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Kysy kunkin PDF-, DXF- tai tekstitiedoston tuontiasetukset ennen sijoittamista; pois: käytä viimeksi valittuja asetuksia
On: one locked object showing the document, updated when it changes. Päällä: yksi lukittu objekti, joka näyttää asiakirjan ja päivittyy sen muuttuessa.
Off: an editable copy of its art, embedded in this document. Pois: muokattava kopio sen kuvituksesta upotettuna tähän asiakirjaan.
Images: link to the file instead of only embedding it. Kuvat: linkitä tiedostoon sen sijaan, että se vain upotetaan.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3599,6 +3599,8 @@ digits Malayalam Malayalam
Swap Fill and Stroke (Shift+X) Permuter le fond et le contour (Maj+X)
Swap start and end arrowheads Permuter les pointes de flèche de début et de fin
Swap the selected object for the file, keeping its place and transform Remplacer l’objet sélectionné par le fichier, en conservant sa position et sa transformation
Show Import Options Afficher les options d’importation
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Demander les options d’importation de chaque fichier PDF, DXF ou texte avant de le placer, sinon utiliser les dernières options choisies
Stylistic Alternates Variantes stylistiques
Swash Lettres ornées
Swatch Conflict Conflit de nuances
Can't render this file because it is too large.
+2
View File
@@ -4968,6 +4968,8 @@ antiAlias Sharp शार्प
Swap Objects and Sizes स्वैप ऑब्जेक्ट्स और साइज
Swap start and end arrowheads स्वैप शुरू और अंत तीरहेड्स
Swap the selected object for the file, keeping its place and transform फ़ाइल के लिए सिलेक्टेड ऑब्जेक्ट को स्वैप करें, अपनी जगह को बनाए रखें और परिवर्तित करें
Show Import Options आयात विकल्प दिखाएँ
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last रखने से पहले हर PDF, DXF या टेक्स्ट फ़ाइल के आयात विकल्प पूछें; बंद होने पर पिछली बार चुने गए विकल्प इस्तेमाल करें
Swash स्वॉश
Swatch Conflict स्वॉच विरोध
Swatch Conflicts स्वॉच विरोध
Can't render this file because it is too large.
+2
View File
@@ -4295,6 +4295,8 @@ mark-style Japanese Japán
Swap Fill and Stroke (Shift+X) Kitöltés és körvonal cseréje (Shift+X)
Swap start and end arrowheads Kezdő és záró nyílhegy cseréje
Swap the selected object for the file, keeping its place and transform A kijelölt objektum cseréje a fájlra, a helye és az átalakítása megtartásával
Show Import Options Importálási beállítások megjelenítése
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Elhelyezés előtt rákérdez minden PDF-, DXF- vagy szövegfájl importálási beállításaira; kikapcsolva a legutóbb választott beállításokat használja
Stylistic Alternates Stilisztikai változatok
Swash Cifra változatok
Swatch Conflict Színminta-ütközés
Can't render this file because it is too large.
+2
View File
@@ -2566,6 +2566,8 @@ digits Malayalam Malayalam
Keep a link to an image file instead of only embedding it Pertahankan tautan ke file gambar alih-alih hanya menyematkannya
Place onto a new locked, dimmed template layer below the current layer Tempatkan pada lapisan templat baru yang terkunci dan diredupkan di bawah lapisan saat ini
Swap the selected object for the file, keeping its place and transform Tukar objek terpilih dengan file, mempertahankan tempat dan transformasinya
Show Import Options Tampilkan Opsi Impor
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Tanyakan opsi impor setiap file PDF, DXF, atau teks sebelum menempatkannya; nonaktif: gunakan opsi yang terakhir dipilih
On: one locked object showing the document, updated when it changes. Aktif: satu objek terkunci yang menampilkan dokumen, diperbarui saat dokumen berubah.
Off: an editable copy of its art, embedded in this document. Nonaktif: salinan karya seninya yang dapat diedit, tersemat dalam dokumen ini.
Images: link to the file instead of only embedding it. Gambar: tautkan ke file alih-alih hanya menyematkannya.
Can't render this file because it contains an unexpected character in line 2451 and column 5.
+2
View File
@@ -3653,6 +3653,8 @@ digits Malayalam Malayalam
Swap Fill and Stroke (Shift+X) Inverti riempimento e traccia (Shift+X)
Swap start and end arrowheads Inverti punte di freccia iniziale e finale
Swap the selected object for the file, keeping its place and transform Sostituisci l'oggetto selezionato con il file, mantenendone posizione e trasformazione
Show Import Options Mostra opzioni di importazione
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Chiedi le opzioni di importazione di ogni file PDF, DXF o di testo prima di inserirlo; disattivato: usa le ultime opzioni scelte
Stylistic Alternates Alternative stilistiche
Swash Ornamentale
Swatch Conflict Conflitto campioni
Can't render this file because it is too large.
+2
View File
@@ -4054,6 +4054,8 @@ file-filter Templates テンプレート
Superscript 上付き文字
Swap Fill and Stroke (Shift+X) 塗りと線を入れ替え (Shift+X)
Swap the selected object for the file, keeping its place and transform 選択したオブジェクトを、位置と変形を保ったままファイルに置き換え
Show Import Options 読み込みオプションを表示
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last 配置する前に PDF、DXF、テキストファイルごとの読み込みオプションを確認。オフのときは前回選んだオプションを使用
Swatch Libraries Menu スウォッチライブラリメニュー
System Layout システムレイアウト
Tabular Lining 等幅ライニング
Can't render this file because it is too large.
+2
View File
@@ -2488,6 +2488,8 @@ digits Malayalam მალაიალამური
Keep a link to an image file instead of only embedding it გამოსახულების ფაილის ბმულის შენარჩუნება მხოლოდ ჩაშენების ნაცვლად
Place onto a new locked, dimmed template layer below the current layer ახალ ჩაკეტილ, ჩამქრალ შაბლონის შრეზე განთავსება მიმდინარე შრის ქვემოთ
Swap the selected object for the file, keeping its place and transform მონიშნული ობიექტის ფაილით ჩანაცვლება მისი ადგილისა და გარდაქმნის შენარჩუნებით
Show Import Options იმპორტის პარამეტრების ჩვენება
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last განთავსებამდე თითოეული PDF, DXF ან ტექსტური ფაილის იმპორტის პარამეტრების მოთხოვნა; გამორთულისას ბოლოს არჩეული პარამეტრები გამოიყენება
On: one locked object showing the document, updated when it changes. ჩართული: ერთი ჩაკეტილი ობიექტი, რომელიც დოკუმენტს აჩვენებს და მისი ცვლილებისას ახლდება.
Off: an editable copy of its art, embedded in this document. გამორთული: მისი ნახატის რედაქტირებადი ასლი, ჩაშენებული ამ დოკუმენტში.
Images: link to the file instead of only embedding it. გამოსახულებები: ფაილის ბმული მხოლოდ ჩაშენების ნაცვლად.
Can't render this file because it is too large.
+2
View File
@@ -4042,6 +4042,8 @@ file-filter Templates 템플릿
Superscript 위 첨자
Swap Fill and Stroke (Shift+X) 칠과 선 바꾸기 (Shift+X)
Swap the selected object for the file, keeping its place and transform 선택한 개체를 파일로 바꾸되 위치와 변형은 유지합니다
Show Import Options 가져오기 옵션 표시
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last 배치하기 전에 각 PDF, DXF 또는 텍스트 파일의 가져오기 옵션을 묻습니다. 끄면 마지막으로 선택한 옵션을 사용합니다
Swatch Libraries Menu 견본 라이브러리 메뉴
System Layout 시스템 레이아웃
Tabular Lining 고정폭 라이닝
Can't render this file because it is too large.
+2
View File
@@ -3084,6 +3084,8 @@ digits Malayalam Malayalam
Keep a link to an image file instead of only embedding it Een koppeling met een afbeeldingsbestand behouden in plaats van het alleen in te sluiten
Place onto a new locked, dimmed template layer below the current layer Plaatsen op een nieuwe, vergrendelde, gedimde sjabloonlaag onder de huidige laag
Swap the selected object for the file, keeping its place and transform Het geselecteerde object vervangen door het bestand, met behoud van positie en transformatie
Show Import Options Importopties tonen
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Vóór het plaatsen naar de importopties van elk PDF-, DXF- of tekstbestand vragen; uit: de laatst gekozen opties gebruiken
On: one locked object showing the document, updated when it changes. Aan: één vergrendeld object dat het document weergeeft, bijgewerkt wanneer het verandert.
Off: an editable copy of its art, embedded in this document. Uit: een bewerkbare kopie van de artwork, ingesloten in dit document.
Images: link to the file instead of only embedding it. Afbeeldingen: koppel aan het bestand in plaats van het alleen in te sluiten.
Can't render this file because it is too large.
+2
View File
@@ -2697,6 +2697,8 @@ digits Malayalam Malajalam
Keep a link to an image file instead of only embedding it Zachowaj łącze do pliku obrazu zamiast tylko go osadzać
Place onto a new locked, dimmed template layer below the current layer Umieść na nowej zablokowanej, przyciemnionej warstwie szablonu pod bieżącą warstwą
Swap the selected object for the file, keeping its place and transform Zamień zaznaczony obiekt na plik, zachowując jego miejsce i przekształcenie
Show Import Options Pokaż opcje importu
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Pytaj o opcje importu każdego pliku PDF, DXF lub tekstowego przed umieszczeniem; wył.: używaj ostatnio wybranych opcji
On: one locked object showing the document, updated when it changes. Wł.: jeden zablokowany obiekt pokazujący dokument, aktualizowany przy jego zmianie.
Off: an editable copy of its art, embedded in this document. Wył.: edytowalna kopia jego kompozycji, osadzona w tym dokumencie.
Images: link to the file instead of only embedding it. Obrazy: połącz z plikiem zamiast tylko go osadzać.
Can't render this file because it is too large.
+2
View File
@@ -5233,6 +5233,8 @@ axis Both Ambos
Summary Resumo
Superscript Sobrescrito
Swap the selected object for the file, keeping its place and transform Troca o objeto selecionado pelo arquivo, mantendo sua posição e transformação
Show Import Options Mostrar Opções de Importação
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Pergunta as opções de importação de cada arquivo PDF, DXF ou de texto antes de colocá-lo; desligado: usa as últimas opções escolhidas
Swatch Libraries Menu Menu de Bibliotecas de Amostras
Create New Library… Criar Nova Biblioteca…
Rename Library… Renomear Biblioteca…
Can't render this file because it is too large.
+2
View File
@@ -3639,6 +3639,8 @@ digits Malayalam Малаялам
Swap Fill and Stroke (Shift+X) Поменять заливку и обводку (Shift+X)
Swap start and end arrowheads Поменять начальную и конечную стрелки
Swap the selected object for the file, keeping its place and transform Заменить выделенный объект файлом, сохранив его положение и преобразование
Show Import Options Показывать параметры импорта
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Запрашивать параметры импорта каждого файла PDF, DXF или текстового файла перед помещением; если выключено, используются последние выбранные параметры
Stylistic Alternates Стилистические альтернативы
Swash Росчерк
Swatch Conflict Конфликт образцов
Can't render this file because it is too large.
+2
View File
@@ -3529,6 +3529,8 @@ digits Malayalam Malayalam
Swap Fill and Stroke (Shift+X) Byt plats på fyllning och kontur (Shift+X)
Swap start and end arrowheads Byt plats på start- och slutpilspets
Swap the selected object for the file, keeping its place and transform Ersätt det markerade objektet med filen och behåll dess placering och transformering
Show Import Options Visa importalternativ
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Fråga efter importalternativ för varje PDF-, DXF- eller textfil innan den placeras; av: använd de senast valda alternativen
Stylistic Alternates Stilistiska alternativ
Swash Swash
Swatch Conflict Färgrutekonflikt
Can't render this file because it is too large.
+2
View File
@@ -3393,6 +3393,8 @@ digits Malayalam Malayalam
Swap Fill and Stroke (Shift+X) Dolgu ve Konturu Değiştir (Shift+X)
Swap start and end arrowheads Başlangıç ve bitiş ok uçlarını değiştirin
Swap the selected object for the file, keeping its place and transform Seçilen nesneyi, yerini koruyarak dosyayla değiştirin ve dönüştürün
Show Import Options İçe Aktarma Seçeneklerini Göster
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Her PDF, DXF veya metin dosyasını yerleştirmeden önce içe aktarma seçeneklerini sor; kapalıyken son seçilen seçenekleri kullan
Stylistic Alternates Stilistik Alternatifler
Swash çalkalama
Swatch Conflict Renk Örneği Çatışması
Can't render this file because it is too large.
+2
View File
@@ -3646,6 +3646,8 @@ effect Points Точки
Swap Fill and Stroke (Shift+X) Змінити заливку та обведення (Shift+X)
Swap start and end arrowheads Поміняти початкову та кінцеву стрілки
Swap the selected object for the file, keeping its place and transform Замінити виділений об’єкт файлом, зберігши його положення та перетворення
Show Import Options Показувати параметри імпорту
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Запитувати параметри імпорту кожного файлу PDF, DXF чи текстового файлу перед розміщенням; якщо вимкнено, використовуються останні вибрані параметри
Stylistic Alternates Стилістичні альтернативи
Swash Розчерк
Swatch Conflict Конфлікт зразків
Can't render this file because it is too large.
+2
View File
@@ -4020,6 +4020,8 @@ mark-style Japanese Kiểu Nhật
Swap Fill and Stroke (Shift+X) Đổi màu tô và nét (Shift+X)
Swap start and end arrowheads Đổi đầu mũi tên bắt đầu và kết thúc
Swap the selected object for the file, keeping its place and transform Thay đối tượng đã chọn bằng tệp, giữ nguyên vị trí và biến đổi
Show Import Options Hiện tùy chọn nhập
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last Hỏi tùy chọn nhập của từng tệp PDF, DXF hoặc văn bản trước khi đặt; khi tắt, dùng các tùy chọn đã chọn gần nhất
Stylistic Alternates Ký tự thay thế theo phong cách
Swash Swash
Swatch Conflict Xung đột mẫu màu
Can't render this file because it contains an unexpected character in line 204 and column 5.
+2
View File
@@ -3468,6 +3468,8 @@ digits Malayalam 马拉雅拉姆数字
Swap Fill and Stroke (Shift+X) 切换填色和描边 (Shift+X)
Swap start and end arrowheads 对调起点与终点箭头
Swap the selected object for the file, keeping its place and transform 以文件替换选择的对象,并保留其位置与变形
Show Import Options 显示导入选项
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last 置入前询问每个 PDF、DXF 或文本文件的导入选项;关闭时使用上次选择的选项
Stylistic Alternates 风格替代字
Swash 花饰字
Swatch Conflict 色板冲突
Can't render this file because it is too large.
+2
View File
@@ -4726,6 +4726,8 @@ digits Malayalam 馬來亞拉姆數字
Swap Fill and Stroke (Shift+X) 對調填色與筆畫 (Shift+X)
Swap start and end arrowheads 對調起點與終點箭頭
Swap the selected object for the file, keeping its place and transform 以檔案取代選取的物件,並保留其位置與變形
Show Import Options 顯示讀入選項
Ask for each PDF, DXF or text file's import options before placing it; off, use the options chosen last 置入前詢問每個 PDF、DXF 或文字檔案的讀入選項;關閉時使用上次選擇的選項
Stylistic Alternates 風格替代字
Swash 花體字
Swatch Conflict 色票衝突
Can't render this file because it is too large.
+99 -20
View File
@@ -69,6 +69,11 @@ pub struct PlaceState {
/// The Place dialog's lines about its files while they are read off the UI thread: the
/// dialog's files, and the lines once they are read.
reading_lines: Option<(Value, std::sync::mpsc::Receiver<Vec<String>>)>,
/// The files of the loaded place cursor ask for their import options when clicked or dragged
/// (the Place dialog loaded it with Show Import Options on).
queue_asks: bool,
/// The loaded cursor's files by name, with the size a click places them at.
queue_sizes: Vec<(String, f64, f64)>,
}
/// A place cursor thumbnail: decoded when the cursor is loaded, uploaded when first drawn (and
@@ -107,9 +112,11 @@ fn engine(app: &mut VectorcraftApp, id: &str, p: &Value) -> Result<Value, String
// ---------- File → Place… ----------
/// 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
/// asks for them first); with `paths`, the Place dialog for them; with neither, pick the files
/// first. The file is read and placed at once (MCP, scripts).
/// 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).
pub fn run(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
place_file(app, p, Read::AtOnce)
}
@@ -130,8 +137,27 @@ enum Read {
}
fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value, String> {
if p.get("text").is_none() && crate::dialogs::text_import::is_text(p) {
return Ok(crate::dialogs::text_import::open(app, p));
let show = match p.get("showImportOptions") {
None | Some(Value::Null) => None,
Some(Value::Bool(b)) => Some(*b),
Some(_) => return Err("`showImportOptions` must be true or false".into()),
};
let ask = show.unwrap_or(true);
let mut p = p.clone();
if let Some(o) = p.as_object_mut() {
o.remove("showImportOptions");
}
if !ask {
// Placed with the options confirmed last for the format.
if p.get("text").is_none() && crate::dialogs::text_import::is_text(&p) {
p["text"] = crate::dialogs::text_import::last_options(app);
}
if let Some(dxf) = crate::dialogs::dxf_import::last_options(app, &p) {
p["dxf"] = dxf;
}
}
if p.get("text").is_none() && crate::dialogs::text_import::is_text(&p) {
return Ok(crate::dialogs::text_import::open(app, &p));
}
if p.get("path").is_none() && p.get("dataBase64").is_none() {
let files: Vec<Value> = match p.get("paths").and_then(Value::as_array) {
@@ -142,17 +168,21 @@ fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value,
return Err("paths: give at least one file".into());
}
open_dialog(app, files);
if let (Some(show), Some(d)) = (show, app.ui.dialog.as_mut()) {
d.fields.insert("showImportOptions".into(), json!(show));
}
return Ok(Value::Null);
}
// A PDF with several pages or a password asks for the page, box and password first.
if crate::dialogs::import_pdf::offer_place(app, p) {
// A PDF with several pages or a password asks for the page, box and password first; with
// Show Import Options off, only an encrypted one asks, for its password.
let offered = if ask { crate::dialogs::import_pdf::offer_place(app, &p) } else { crate::dialogs::import_pdf::offer_place_password(app, &p) };
if offered {
return Ok(Value::Null);
}
// A DXF drawing asks for its import options.
if crate::dialogs::dxf_import::offer_place(app, p) {
if ask && crate::dialogs::dxf_import::offer_place(app, &p) {
return Ok(json!({ "dialog": crate::dialogs::dxf_import::KIND }));
}
let mut p = p.clone();
if ["at", "rect", "replace"].iter().all(|k| p.get(k).is_none())
&& let Some(v) = app.view()
{
@@ -203,6 +233,7 @@ fn open_dialog(app: &mut VectorcraftApp, files: Vec<Value>) {
"link": app.ui.place_link,
"template": false,
"replace": false,
"showImportOptions": app.ui.place_import_options,
"__replace": files.len() == 1 && one_object,
"__documents": linked.contains(&true),
"__others": linked.contains(&false),
@@ -319,15 +350,18 @@ fn fmt_ppi(v: f64) -> String {
}
/// Place (the dialog's OK): one file is placed centred in the view (or replaces the selection);
/// several load the place cursor.
/// several load the place cursor. With Show Import Options on, each file that has import options
/// asks for them before it is placed (the cursor's files when clicked or dragged); off, each is
/// placed with the options confirmed last for its format.
pub fn confirm(app: &mut VectorcraftApp, d: &crate::state::Dialog) -> Result<Value, String> {
let files: Vec<Value> = d.fields.get("files").and_then(Value::as_array).cloned().unwrap_or_default();
let sources: Vec<Value> = files.iter().filter_map(|f| source(app, f)).collect();
if sources.is_empty() {
return Err("no files to place".into());
}
let (link, template) = (d.bool("link"), d.bool("template"));
let (link, template, ask) = (d.bool("link"), d.bool("template"), d.bool("showImportOptions"));
app.ui.place_link = link;
app.ui.place_import_options = ask;
app.ui.dialog = None;
app.place.reading_lines = None;
let r = match &sources[..] {
@@ -335,6 +369,7 @@ pub fn confirm(app: &mut VectorcraftApp, d: &crate::state::Dialog) -> Result<Val
let mut p = one.clone();
p["link"] = json!(link);
p["template"] = json!(template);
p["showImportOptions"] = json!(ask);
if d.bool("replace") && d.bool("__replace") {
p["replace"] = json!(true);
}
@@ -343,7 +378,13 @@ pub fn confirm(app: &mut VectorcraftApp, d: &crate::state::Dialog) -> Result<Val
many => {
let paths: Vec<&Value> = many.iter().filter_map(|s| s.get("path")).collect();
let files: Vec<&Value> = many.iter().filter(|s| s.get("path").is_none()).collect();
queue_files(app, &json!({ "paths": paths, "files": files, "link": link, "template": template }), Read::ForUser { dropped: false })
let mut q = json!({ "paths": paths, "files": files, "link": link, "template": template });
if !ask {
// Each text file and DXF drawing in the cursor takes the options confirmed last.
q["text"] = crate::dialogs::text_import::last_options(app);
q["dxf"] = crate::dialogs::dxf_import::remembered(app);
}
queue_files(app, &q, Read::ForUser { dropped: false }, ask)
}
};
app.place.picked.clear();
@@ -355,35 +396,41 @@ pub fn confirm(app: &mut VectorcraftApp, d: &crate::state::Dialog) -> Result<Val
/// `file.place.queue` with thumbnails for the cursor (unless `thumbnail` is given), the files read
/// at once (MCP, scripts).
pub fn queue(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
queue_files(app, p, Read::AtOnce)
queue_files(app, p, Read::AtOnce, false)
}
fn queue_files(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value, String> {
/// `file.place.queue` with `p`; `asks`: the cursor's files ask for their import options when placed.
fn queue_files(app: &mut VectorcraftApp, p: &Value, read: Read, asks: bool) -> Result<Value, String> {
let mut p = p.clone();
if p.get("thumbnail").is_none() {
p["thumbnail"] = json!(THUMB);
}
if matches!(read, Read::ForUser { .. }) && crate::placing::available(app) {
let r = crate::placing::start_queue(app, &p);
let r = crate::placing::start_queue(app, &p, asks);
if let Err(e) = &r {
app.status(e.clone());
}
return r;
}
let r = engine(app, "file.place.queue", &p)?;
show_thumbnails(app, &r);
show_thumbnails(app, &r, asks);
Ok(r)
}
/// The file a click or drag of the place cursor places (`file.place` with `p`, its `rect` set):
/// read off the UI thread where the files the user places are ([`crate::placing::start`]), else at
/// once. An error shows in the status bar.
/// once. When the Place dialog loaded the cursor with Show Import Options on, a file that has
/// import options asks for them first (Cancel leaves it unplaced). An error shows in the status bar.
pub(crate) fn place_from_cursor(app: &mut VectorcraftApp, p: &Value) {
// Read within the automation roots of whoever loaded the cursor, even on a click of the person
// at the keyboard: an agent's queue reaches no file it wasn't given.
let roots = app.session.place_queue_roots();
vectorcraft_engine::file_access::confine(roots.as_ref(), || {
if crate::placing::available(app) {
if app.place.queue_asks {
// Placing shows its own errors in the status bar.
let p = clicked_at_full_size(app, p);
let _ = place_file(app, &p, Read::ForUser { dropped: false });
} else if crate::placing::available(app) {
if let Err(e) = crate::placing::start(app, p, false) {
app.status(e);
}
@@ -393,8 +440,40 @@ pub(crate) fn place_from_cursor(app: &mut VectorcraftApp, p: &Value) {
});
}
/// The cursor's thumbnails from `file.place.queue`'s reply `r`.
pub(crate) fn show_thumbnails(app: &mut VectorcraftApp, r: &Value) {
/// The params of a cursor click (`rect` at the file's queued size) on a PDF or DXF file, whose
/// import options may change its size (another page, another scale): placed as its options place
/// it at the click (`at`, the center of the box the click gave), as File › Place of that one file
/// places it, instead of fitted into the box of the first page or the default scale. A drag keeps
/// its `rect`; one to exactly the queued size gives the same `rect` as a click, and is taken as one.
fn clicked_at_full_size(app: &VectorcraftApp, p: &Value) -> Value {
let mut p = p.clone();
let Some(name) = p.get("path").or_else(|| p.get("name")).and_then(Value::as_str).map(fileio::file_name) else { return p };
if !matches!(fileio::extension(&name).as_str(), "pdf" | "ai" | "ait" | "dxf") {
return p;
}
let r: Vec<f64> = p.get("rect").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
let [x, y, w, h] = r[..] else { return p };
// Files of the same name in the cursor must all have that size: the place tool's request names
// the file, not its place in the cursor.
let mut same_name = app.place.queue_sizes.iter().filter(|(n, ..)| *n == name).peekable();
let queued = same_name.peek().is_some() && same_name.all(|(_, qw, qh)| (qw - w).abs() < 1e-6 && (qh - h).abs() < 1e-6);
if queued && let Some(o) = p.as_object_mut() {
o.remove("rect");
o.insert("at".into(), json!([x + w / 2.0, y + h / 2.0]));
}
p
}
/// The cursor's thumbnails and its files' sizes from `file.place.queue`'s reply `r` once the cursor
/// is loaded; `asks`: its files ask for their import options when placed.
pub(crate) fn show_thumbnails(app: &mut VectorcraftApp, r: &Value, asks: bool) {
app.place.queue_asks = asks;
app.place.queue_sizes = r["files"]
.as_array()
.into_iter()
.flatten()
.filter_map(|f| Some((f["name"].as_str()?.to_string(), f["width"].as_f64()?, f["height"].as_f64()?)))
.collect();
app.place.thumbs = r["files"]
.as_array()
.into_iter()
+10 -6
View File
@@ -21,10 +21,11 @@ use vectorcraft_engine::file_access::ScopeHandle;
use crate::VectorcraftApp;
/// What was read: one file to place, the files to load the place cursor with, or a bitmap to paste.
/// What was read: one file to place, the files to load the place cursor with (and whether they ask
/// for their import options when placed), or a bitmap to paste.
enum Read {
Place(Box<PlaceRead>),
Queue(QueueRead),
Queue(QueueRead, bool),
Paste(Box<Paste>),
}
@@ -236,17 +237,18 @@ pub fn start(app: &mut VectorcraftApp, p: &Value, dropped: bool) -> Result<Value
/// `file.place.queue` with `p` for the active document, its files read and their thumbnails made off
/// the UI thread → `{background: true, status}`; the place cursor is loaded once they are ready. When no thread could start, the cursor is loaded at once → `file.place.queue`'s reply.
pub fn start_queue(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
/// `asks`: the cursor's files ask for their import options when placed ([`crate::place::show_thumbnails`]).
pub fn start_queue(app: &mut VectorcraftApp, p: &Value, asks: bool) -> Result<Value, String> {
let job = app.session.prepare_place_queue(p).map_err(|e| e.to_string())?;
let names = job.names().join(", ");
let started = spawn(app, names.clone(), names, false, move || job.read().map(Read::Queue).map_err(|e| e.to_string()))?;
let started = spawn(app, names.clone(), names, false, move || job.read().map(|r| Read::Queue(r, asks)).map_err(|e| e.to_string()))?;
match started {
Some(r) => Ok(r),
None => {
let r = app.session.execute("file.place.queue", p).map_err(|e| e.to_string());
app.sync_views();
let r = r?;
crate::place::show_thumbnails(app, &r);
crate::place::show_thumbnails(app, &r, asks);
Ok(r)
}
}
@@ -485,7 +487,9 @@ fn finish(app: &mut VectorcraftApp, p: &Pending, r: Result<Read, String>) {
app.session.set_active(i);
let r = match r {
Ok(Read::Place(read)) => app.session.place_read(*read).map(|_| ()).map_err(|e| e.to_string()),
Ok(Read::Queue(read)) => app.session.place_queue_read(read).map(|r| crate::place::show_thumbnails(app, &r)).map_err(|e| e.to_string()),
Ok(Read::Queue(read, asks)) => {
app.session.place_queue_read(read).map(|r| crate::place::show_thumbnails(app, &r, asks)).map_err(|e| e.to_string())
}
Ok(Read::Paste(paste)) => return finish_paste(app, *paste),
Err(e) => Err(e),
};
+1 -1
View File
@@ -1173,7 +1173,7 @@ fn a_file_whose_document_was_reverted_meanwhile_is_not_placed() {
for kind in ["place", "drop", "queue"] {
let (mut app, _) = saved_and_changed(app(true), &format!("reverted-{kind}"));
let read = |app: &mut VectorcraftApp| match kind {
"queue" => start_queue(app, &json!({"paths": [tif]})),
"queue" => start_queue(app, &json!({"paths": [tif]}), false),
_ => start(app, &json!({"path": tif}), kind == "drop"),
};
read(&mut app).unwrap();
+10
View File
@@ -749,6 +749,14 @@ pub struct UiState {
/// File → Place: Link is on (the Place dialog remembers it).
#[serde(default = "yes")]
pub place_link: bool,
/// File → Place: Show Import Options is on (the Place dialog remembers it). Each placed file
/// with import options (a PDF's page and box, a DXF drawing, a text file) asks for them first;
/// off, it is placed with the options last confirmed for its format.
#[serde(default = "yes")]
pub place_import_options: bool,
/// The Text Import Options confirmed last (`file.place`'s `text` options; null: never used).
#[serde(default)]
pub text_import: Value,
/// The file the open dialog reads, kept out of its JSON fields (Import PDF); dropped when no
/// dialog is open.
#[serde(skip)]
@@ -1219,6 +1227,8 @@ impl Default for UiState {
swatches_show_recent_colors: true,
svg_options: Value::Null,
place_link: true,
place_import_options: true,
text_import: Value::Null,
dialog_file: None,
made_text_style: vec![],
pending_fonts: vec![],
+174
View File
@@ -7,6 +7,7 @@ use egui::{Pos2, Shape, vec2};
use serde_json::{Value, json};
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::Session;
use vectorcraft_testkit::pdf::{PdfPage, pdf};
use crate::VectorcraftApp;
use crate::canvas::Xf;
@@ -897,6 +898,179 @@ fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64
/// while the window sits idle in the background, follows Update Links: Automatically: the app
/// wakes itself to see the new link before the change, so the change isn't taken for the file as
/// first seen.
/// The text of the one selected type object.
fn selected_text(app: &VectorcraftApp) -> String {
let st = app.session.active().unwrap();
let NodeKind::Text(t) = &st.doc.node(st.selection.objects[0]).unwrap().kind else { panic!("not type") };
t.plain_text()
}
/// The kind of the open dialog, if any.
fn dialog_kind(app: &VectorcraftApp) -> Option<&str> {
app.ui.dialog.as_ref().map(|d| d.kind.as_str())
}
const NOTES: &[u8] = b"one\r\ntwo\r\n\r\nthree\r\n";
/// A DXF drawing of one 100 × 50 line.
const LINE_DXF: &[u8] = b"0\nSECTION\n2\nENTITIES\n0\nLINE\n8\n0\n10\n0\n20\n0\n11\n100\n21\n50\n0\nENDSEC\n0\nEOF\n";
#[test]
fn show_import_options_is_a_place_option_saved_with_the_preferences() {
let mut app = app();
let dir = Folder::new("ui-place-show-import-options");
let notes = dir.file("notes.txt", NOTES);
app.run("file.place", json!({"paths": [notes]})).unwrap();
let d = app.ui.dialog.as_ref().expect("the Place dialog");
assert!(d.bool("showImportOptions"), "on by default");
let text = crate::tests_labels::painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
assert!(text.contains("Show Import Options"), "{text}");
// On: the text file asks for its import options first.
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
app.ui.dialog = None;
// Off: it is placed at once, and the next Place dialog starts off.
app.run("file.place", json!({"paths": [notes]})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("showImportOptions".into(), json!(false));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(dialog_kind(&app), None);
assert!(selected_text(&app).contains("three"));
assert!(!app.ui.place_import_options);
app.run("file.place", json!({"paths": [notes]})).unwrap();
assert!(!app.ui.dialog.as_ref().unwrap().bool("showImportOptions"), "remembered");
// `showImportOptions` given with `paths` sets the dialog's checkbox.
app.run("file.place", json!({"paths": [notes], "showImportOptions": true})).unwrap();
assert!(app.ui.dialog.as_ref().unwrap().bool("showImportOptions"));
// Saved with the preferences; older preferences without it start on.
let saved = serde_json::to_value(&app.ui).unwrap();
assert_eq!(saved["place_import_options"], false);
let old: crate::UiState = serde_json::from_value(json!({"place_link": false})).unwrap();
assert!(old.place_import_options && old.text_import.is_null());
}
#[test]
fn with_show_import_options_off_a_file_takes_the_options_confirmed_last() {
let mut app = app();
let b64 = vectorcraft_format::base64_encode;
// Nothing confirmed yet: file.place's defaults.
app.run("file.place", json!({"name": "a.txt", "dataBase64": b64(NOTES), "showImportOptions": false})).unwrap();
assert_eq!(dialog_kind(&app), None);
let plain = selected_text(&app);
// Text Import Options remembers what it confirmed, and the next text file starts from it.
app.run("file.place", json!({"name": "b.txt", "dataBase64": b64(NOTES)})).unwrap();
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
for k in ["removeLineReturns", "removeParagraphReturns", "replaceSpaces"] {
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), json!(true));
}
app.ui.dialog.as_mut().unwrap().fields.insert("spaces".into(), json!(4));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(selected_text(&app), "one two\nthree");
assert_ne!(plain, "one two\nthree");
app.run("file.place", json!({"name": "c.txt", "dataBase64": b64(NOTES)})).unwrap();
let d = app.ui.dialog.as_ref().unwrap();
assert!(d.bool("removeLineReturns") && d.bool("replaceSpaces"));
assert_eq!(d.f64("spaces", 0.0), 4.0);
app.ui.dialog = None;
app.run("file.place", json!({"name": "d.txt", "dataBase64": b64(NOTES), "showImportOptions": false})).unwrap();
assert_eq!((dialog_kind(&app), selected_text(&app).as_str()), (None, "one two\nthree"));
// A DXF drawing: placed without asking, with the fit chosen last.
let width = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
st.doc.node(st.selection.objects[0]).unwrap().geometric_bounds().unwrap().width()
};
app.run("file.place", json!({"name": "line.dxf", "dataBase64": b64(LINE_DXF), "showImportOptions": false})).unwrap();
assert_eq!(dialog_kind(&app), None);
let unfitted = width(&app);
app.ui.dxf_import = json!({"fit": true});
app.run("file.place", json!({"name": "line.dxf", "dataBase64": b64(LINE_DXF), "showImportOptions": false})).unwrap();
assert_eq!(dialog_kind(&app), None);
assert!(width(&app) > unfitted + 1.0, "fitted: {} vs {unfitted}", width(&app));
app.run("file.place", json!({"name": "line.dxf", "dataBase64": b64(LINE_DXF)})).unwrap();
assert_eq!(dialog_kind(&app), Some(crate::dialogs::dxf_import::KIND), "on: it asks");
app.ui.dialog = None;
// A PDF with several pages: its first page, without asking.
let pages = [PdfPage::new(100.0, 100.0, "0 g 0 0 10 10 re f"), PdfPage::new(300.0, 100.0, "1 0 0 rg 0 0 30 30 re f")];
let two = pdf(&pages, None);
app.run("file.place", json!({"name": "two.pdf", "dataBase64": b64(&two), "showImportOptions": false})).unwrap();
assert_eq!(dialog_kind(&app), None);
assert!((width(&app) - 100.0).abs() < 0.01, "page 1: {}", width(&app));
// An encrypted PDF still asks for its password, and then places its first page.
let locked = pdf(&pages, Some("pw"));
app.run("file.place", json!({"name": "locked.pdf", "dataBase64": b64(&locked), "showImportOptions": false})).unwrap();
assert_eq!(app.ui.dialog.as_ref().map(|d| d.bool("__locked")), Some(true));
app.ui.dialog.as_mut().unwrap().fields.insert("password".into(), json!("pw"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(dialog_kind(&app), None, "no page to pick");
assert!((width(&app) - 100.0).abs() < 0.01, "page 1: {}", width(&app));
// Given `page`, it asks for the password and places that page.
app.run("file.place", json!({"name": "locked.pdf", "dataBase64": b64(&locked), "page": 2, "showImportOptions": false})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("password".into(), json!("pw"));
crate::dialogs::confirm(&mut app).unwrap();
assert!((width(&app) - 300.0).abs() < 0.01, "page 2: {}", width(&app));
// A page past the last one is refused once the file is unlocked, not placed as another page.
app.run("file.place", json!({"name": "locked.pdf", "dataBase64": b64(&locked), "page": 9, "showImportOptions": false})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("password".into(), json!("pw"));
assert!(crate::dialogs::confirm(&mut app).is_err());
app.ui.dialog = None;
// A value that is not true or false is refused, and nothing is placed.
let before = app.session.active().unwrap().history.undo.len();
for bad in [json!("no"), json!(0)] {
let e = app.run("file.place", json!({"name": "e.txt", "dataBase64": b64(NOTES), "showImportOptions": bad})).unwrap_err();
assert!(e.contains("`showImportOptions` must be true or false"), "{e}");
}
assert_eq!((dialog_kind(&app), app.session.active().unwrap().history.undo.len()), (None, before));
}
#[test]
fn the_place_cursors_files_ask_for_their_import_options_when_show_import_options_is_on() {
let mut app = app();
let dir = Folder::new("ui-place-cursor-import-options");
let a = dir.file("a.png", &png(10, 10, 72.0));
let notes = dir.file("notes.txt", NOTES);
let click = json!({"path": notes, "rect": [10, 10, 200, 100]});
// On: a click on the text file asks for its import options, and OK places it in the rect.
app.run("file.place", json!({"paths": [a, notes]})).unwrap();
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.session.tool_options()["count"], 2);
crate::place::place_from_cursor(&mut app, &click);
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["__params"]["rect"], json!([10, 10, 200, 100]));
crate::dialogs::confirm(&mut app).unwrap();
assert!(selected_text(&app).contains("three"));
// A click on a PDF places the page chosen at its own size, centered where the click put the
// first page; a drag keeps its box.
let pages = [PdfPage::new(100.0, 100.0, "0 g 0 0 10 10 re f"), PdfPage::new(300.0, 100.0, "1 0 0 rg 0 0 30 30 re f")];
let two = dir.file("two.pdf", &pdf(&pages, None));
app.run("file.place", json!({"paths": [a, two]})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("link".into(), json!(false));
crate::dialogs::confirm(&mut app).unwrap();
crate::place::place_from_cursor(&mut app, &json!({"path": two, "link": false, "rect": [10, 10, 100, 100]}));
assert_eq!(dialog_kind(&app), Some("importPdf"));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["__place"], json!({"link": false, "at": [60.0, 60.0]}));
app.ui.dialog.as_mut().unwrap().fields.insert("page".into(), json!(2));
crate::dialogs::confirm(&mut app).unwrap();
let st = app.session.active().unwrap();
let b = st.doc.node(st.selection.objects[0]).unwrap().geometric_bounds().unwrap();
assert_eq!((b.width().round(), b.center()), (300.0, vectorcraft_geom::Point::new(60.0, 60.0)));
crate::place::place_from_cursor(&mut app, &json!({"path": two, "link": false, "rect": [10, 10, 50, 50]}));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["__place"]["rect"], json!([10, 10, 50, 50]));
app.ui.dialog = None;
// Off: the cursor's files are placed at once, with the options confirmed last.
app.run("file.place", json!({"paths": [a, notes]})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("showImportOptions".into(), json!(false));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.session.tool_options()["count"], 2);
crate::place::place_from_cursor(&mut app, &click);
assert_eq!(dialog_kind(&app), None);
assert!(selected_text(&app).contains("three"));
// A cursor loaded by file.place.queue places at once, as before.
app.run("file.place", json!({"paths": [a, notes]})).unwrap();
crate::dialogs::confirm(&mut app).unwrap();
app.run("file.place.queue", json!({"paths": [a, notes]})).unwrap();
crate::place::place_from_cursor(&mut app, &click);
assert_eq!(dialog_kind(&app), None);
}
#[test]
fn a_file_placed_while_idle_follows_its_changes() {
let mut app = app();
+17
View File
@@ -6826,6 +6826,23 @@ with `pointer_gesture` (a click places at 100% with the top-left corner there, a
`file.place.info` refuse, of the keys each one takes, a `link`, `replace` or `template` that is not true or false and a `thumbnail` that is not 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
`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)).
It is on by default and saved with the preferences. On, each file that has import options asks for them before it is
placed, also each file of the place cursor when it is clicked or dragged (Cancel leaves that file unplaced). A click on
a PDF or DXF file places it as its options place it with `at` at the center of the box the click gave (a PDF's page at
its own size, a DXF drawing at its scale, or on the artboard there when fitted or not centered), and a drag to any other
size fits it into the dragged box (a drag to exactly the file's queued size gives the place tool the same box as a click and
is placed as one). Off, each
file is placed with the options last confirmed for its format. The default, and the options a file is placed with
when the checkbox is off, are VectorCraft's own choices.
```json
{"name":"run_command","arguments":{"command":"file.place","params":{"path":"/tmp/photo.jpg","at":[300,200]}}}
{"name":"run_command","arguments":{"command":"file.place","params":{"name":"logo.svg","dataBase64":"PHN2Zy…","rect":[0,0,100,100]}}}
+1
View File
@@ -1835,6 +1835,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Placed documents' bitmaps on screen: when a bitmap made in the background is ready, the desktop app renders the canvas again only where the objects that show its file are, when the frame shown was rendered on the UI thread and that part is at most half the canvas. Bitmaps drawn or made since a frame began are kept for that frame past the 512 MiB budget, up to 2 GiB, so a view showing more than the budget does not make them again in every frame. A bitmap that would take them past 2 GiB is not kept, and its file draws exactly at that zoom. A bitmap that can't be made (the file can't be read and there is no preview) shows the light placeholder and is not asked for again. Each ready bitmap used to render the whole canvas again, and bitmaps past the budget and bitmaps that could not be made were asked for again in every frame (`crates/render/src/tests/placed_document.rs`).
- Linked PDF and `.ai` pages: File › Place with Link (the default) places a page of a PDF, `.ai` or `.ait` file read as PDF pages as a placed document, as it links an image, the page and its box (`crop`) kept with the link: Links check, update and relink it, Break Link and Object › Expand give the group placing it without Link gives, every output draws the page's vectors, and Package copies the file. Without Link, or with a password or `textAs: "outlines"`, the page is embedded. EPS and PostScript `.ai` files are still embedded (`crates/engine/src/tests/placed_document.rs`).
- File › Place with Bounding Box (`file.place {crop: "bounding"}`) measures the art that shows, with its strokes and effects, as an export does: hidden objects, guides and template layers do not count, a group whose only part on the page or artboard is hidden is not placed, and symbol instances are measured by their art, for `at`, `rect`, the place cursor and `file.place.info` (`crates/engine/src/tests/place.rs`).
- The Place dialog 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)). On (VectorCraft's default, saved with the preferences), each PDF, DXF or text file asks for its import options before it is placed, also in the place cursor; off (`file.place {showImportOptions: false}`), it is placed with the options last confirmed for its format, and only an encrypted PDF asks, for its password. Text Import Options now starts from the options confirmed last (`crates/ui-egui/src/tests/place.rs`).
- Separations Preview shows placed and embedded raster images on the visible plates, as it shows vector art (#4153): an RGB image's pixels are separated into process inks with the proof setup's intent, a CMYK image shows its own inks, a hidden plate's ink leaves the image, and with one plate visible the image shows that plate's ink as a grey. Raster images carry no spot ink, so a spot plate shown alone shows them as paper; a colorized image shows its color's plates. Emoji and other bitmap glyphs follow the same rule. The plate-filtered pixels and their reduced-size copies are kept apart from the image's own pixels, for each set of visible plates, proof setup and color settings, and with the preview off nothing changes. Evidence: `tests::colorize::separations_preview_shows_an_rgb_images_process_inks`, `separations_preview_shows_a_cmyk_images_own_inks`, `separations_preview_keeps_an_images_pixels_per_plate_set`. Print preview fidelity remains the open gap, so scores and hours are unchanged.
- Desktop print transport uses `craft-print` for CUPS and modern Windows. Sheet composition, copies, separations and colour management remain in VectorCraft. The Print dialog's Media list also carries the Japanese papers — JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 128 × 182 mm beside the ISO B4/B5 (250 × 353 and 176 × 250 mm, kept so settings and presets saved before them keep their paper), and はがき 100 × 148 mm, 往復はがき 148 × 200 mm, 封筒 長形3号 120 × 235 mm, 長形4号 90 × 225 mm, 角形2号 240 × 332 mm, 角形3号 216 × 277 mm and 四六判 127 × 188 mm; the JSON id or the dialog label names a paper, any case and spacing (`B5 (JIS)` or `b5jis`) (`crates/pdf/src/print/settings.rs`, `crates/pdf/src/tests/print.rs`). The print-preview and physical-device fidelity gaps remain, so print scores and hours are unchanged.
- A PDF, PDF-compatible `.ai` or `.ait` opened from its pages or its editing copy opens with the File Info that its XMP packet and its document information dictionary give: the title (`dc:title`, its `x-default` item first, else Info `Title`), the author, description, keywords, copyright status, notice and URL, and the created and modified dates, the XMP packet's value where both give one (`crates/pdf/src/tests/file_info.rs`, `crates/engine/src/tests/fileinfo.rs`). An SVG opens with the title of its root `<title>`. The title is File Info's alone (`DocMetadata::title`): the document's name stays the file's name with its extension, a file that gives no title leaves File Info with that name, and exports (PDF, SVG, PNG, EPS, `.ai`) and native saves carry it; changing the title in File Info sets the document's name as before (#3626).