Export for Screens: <filename> takes the saved document's file name

In a file name pattern, <filename> is documented as the document's name
without its extension, but it expanded to the File Info title whenever the
document had one, so a saved my-logo.vectorcraft titled "Logo: final
version" exported as Logo--final-version__Artboard 1.png. The Full
Document file and the .zip took the same title.

Export for Screens and the dialog's Example line now take <filename> from
the document's file, as Save names it (save.rs), and fall back to the
title only for a document that has not been saved.
This commit is contained in:
dotCooCoo
2026-10-24 09:53:53 -07:00
parent c7d65ae7e7
commit 0dcb52c7cd
3 changed files with 43 additions and 6 deletions
+7 -4
View File
@@ -382,7 +382,7 @@ struct Naming {
}
impl Naming {
/// The naming of params `o` for a document titled `title`.
/// The naming of params `o` for a document named `title`: its file's path, or its title before it is saved.
fn of(o: &ScreenParams, title: &str) -> Result<Self> {
let folders = Folders::of(o)?;
// A prefix names files, not folders.
@@ -669,7 +669,9 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
let o = ScreenParams::of(p)?;
let rows = rows_of(s, p, o.preset.as_deref())?;
o.one_source()?;
let naming = Naming::of(&o, &s.doc()?.doc.title)?;
// `<filename>`: the document's file, as Save names it, or its title before it is saved.
let st = s.doc()?;
let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&st.doc.title))?;
let selection = o.selection_mode()?;
let selected = if selection.is_some() { selected_artwork(s)? } else { vec![] };
let doc = s.doc()?.doc.clone();
@@ -846,13 +848,14 @@ pub fn first_file(s: &Session, p: &Value) -> Result<String> {
let rows = rows_of(s, &p, o.preset.as_deref())?;
o.one_source()?;
let selection = o.selection_mode()?;
let doc = &s.doc()?.doc;
let st = s.doc()?;
let doc = &st.doc;
let mut selected = if selection.is_some() { selected_artwork(s)? } else { vec![] };
// A file per object: objects that draw nothing write no file.
if selection == Some(true) {
selected = selected.into_iter().find(|id| export_crop(doc, &[*id]).is_some()).into_iter().collect();
}
let naming = Naming::of(&o, &doc.title)?;
let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&doc.title))?;
let items = items(doc, &o, selection, &selected, &naming.filename, false, false)?;
let (Some(item), Some(r)) = (items.first(), rows.first()) else { return Err(bad(C, "nothing to export")) };
let (sub, file) = naming.file(item, 0, 0, r);
@@ -205,6 +205,39 @@ fn the_first_file_is_the_file_the_export_writes_first() {
}
}
/// A saved document's `<filename>`, Full Document file and ZIP are named after its file without the
/// extension, as Save names it, whatever its File Info title says.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn a_saved_document_exports_under_its_file_name() {
let mut s = session(1);
s.execute("file.info", &json!({"title": "Logo: final version"})).unwrap();
let dir = std::env::temp_dir().join(format!("vc-screens-filename-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
s.execute("document.save", &json!({"path": dir.join("my-logo.vectorcraft").to_string_lossy()})).unwrap();
for (p, name) in [
(json!({"name": "<filename>__<name>", "formats": [{"format": "png"}]}), "my-logo__Artboard 1.png"),
(json!({"fullDocument": true, "formats": [{"format": "png"}]}), "my-logo.png"),
] {
assert_eq!(names(&export(&mut s, p.clone())), [name], "{p}");
assert_eq!(super::screens::first_file(&s, &p).unwrap(), name, "{p}");
}
assert_eq!(export(&mut s, json!({"zip": true, "formats": [{"format": "svg"}]}))["name"], "my-logo.zip");
let _ = std::fs::remove_dir_all(dir);
}
/// A document that has not been saved has no file: `<filename>` is its title.
#[test]
fn an_unsaved_document_exports_under_its_title() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 40, "height": 30, "name": "Poster"})).unwrap();
s.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 20})).unwrap();
s.execute("file.info", &json!({"title": "Spring poster"})).unwrap();
let p = json!({"name": "<filename>", "formats": [{"format": "png"}]});
assert_eq!(names(&export(&mut s, p.clone())), ["Spring-poster.png"]);
assert_eq!(super::screens::first_file(&s, &p).unwrap(), "Spring-poster.png");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn sub_folders_are_made_in_the_export_folder() {
+3 -2
View File
@@ -3628,8 +3628,9 @@ fallback font…), each note once; the export still writes or returns every file
Batch exports (#1935):
- **What:** `layers: [id…]` (each layer's art alone) or `selection: "one" | "each"` instead of artboards
(`assets`, `fullDocument` as before).
- **Names:** `name` is a pattern for every file: `<filename>` (the document), `<name>` (the artboard, asset, layer
or object), `<index>`, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"`
- **Names:** `name` is a pattern for every file: `<filename>` (the document's file name without its extension, or
its title before it is saved; the Full Document file and the `.zip` use the same name), `<name>` (the artboard,
asset, layer or object), `<index>`, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"`
or `"file"`), `YYYYMMDD`, `YYYY-MM-DD`, `<date>`, `<time>`, `<format>`, `<scale>` and `<suffix>` (else the row's
suffix ends the name; `date` sets the day the pattern uses). Different files it names alike get `-2`, `-3`….
- **Folders:** `folders: "none" | "rows" | "format" | "item" | "itemFormat"` (all in one, a folder per row, per