Export for Screens: an artboard name keeps its dots (#6143)

The artboard and asset file stems came from unique_file_names, which turned
every character except letters, digits, - and _ into a dash, so the dot of
example.artboard became a dash. Export for Screens now uses a variant that keeps
dots (a name of only dots takes the Artboard-N fallback); safe_file_name still
drops trailing periods and guards device names.
Test: an_artboard_name_keeps_its_dots.
This commit is contained in:
SBaksa
2026-11-09 02:03:08 +07:00
committed by bflatastic
parent a3ed60650e
commit 0b3ea7c1d2
4 changed files with 43 additions and 7 deletions
+3 -1
View File
@@ -129,7 +129,9 @@ pub(crate) use access::{drive_share_path, share_path, touched};
pub(crate) use access::{may_read, may_write};
pub use commands::specs;
pub use export_names::artboard_file_names;
pub(crate) use export_names::{default_name, merge, unique_file_names, with_black_output, write_or_return};
pub(crate) use export_names::{
artboard_file_names_keeping_dots, default_name, merge, unique_file_names_keeping_dots, with_black_output, write_or_return,
};
use export_names::{distinct_names_by, write_encoded};
use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, import_name, writable};
pub use formats::{
+24 -2
View File
@@ -5,19 +5,41 @@ use super::{Encoded, EngineError, Result, Session, Value, json};
/// `-`, `Artboard-N` when unnamed, and `-2`, `-3`… after a name already used (compared without
/// case: `Icon` and `icon` are one file on most desktop file systems).
pub fn artboard_file_names(doc: &vectorcraft_doc::Document, boards: &[usize]) -> Vec<String> {
unique_file_names(boards.iter().map(|&b| (doc.artboards.get(b).map_or("", |a| a.name.as_str()), format!("Artboard-{}", b + 1))))
unique_file_names(artboard_names(doc, boards))
}
/// [`artboard_file_names`] with the dots of a name kept (`example.artboard`), for Export for Screens,
/// which puts every stem through `safe_file_name` after this (trailing periods, device names).
pub(crate) fn artboard_file_names_keeping_dots(doc: &vectorcraft_doc::Document, boards: &[usize]) -> Vec<String> {
unique_file_names_keeping_dots(artboard_names(doc, boards))
}
/// Each of `boards`' name, with the file name to use when it has none.
fn artboard_names<'a>(doc: &'a vectorcraft_doc::Document, boards: &'a [usize]) -> impl Iterator<Item = (&'a str, String)> {
boards.iter().map(|&b| (doc.artboards.get(b).map_or("", |a| a.name.as_str()), format!("Artboard-{}", b + 1)))
}
/// File-name parts for `(name, fallback)` pairs (artboards, assets): the name with unsafe
/// characters as `-`, the fallback when it is empty, and `-2`, `-3`… after a name already used
/// (compared without case).
pub(crate) fn unique_file_names<'a>(names: impl IntoIterator<Item = (&'a str, String)>) -> Vec<String> {
fn unique_file_names<'a>(names: impl IntoIterator<Item = (&'a str, String)>) -> Vec<String> {
distinct_names(names.into_iter().map(|(name, fallback)| {
let base: String = name.chars().map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '-' }).collect();
if base.is_empty() { fallback } else { base }
}))
}
/// [`unique_file_names`] keeping `.` in a name (an artboard called `example.artboard` exports as
/// `example.artboard.png`, #6143). A name of only dots (`.` and `..` name other folders) takes the
/// fallback; the caller still runs each stem through `safe_file_name`, which drops trailing periods
/// and guards Windows device names.
pub(crate) fn unique_file_names_keeping_dots<'a>(names: impl IntoIterator<Item = (&'a str, String)>) -> Vec<String> {
distinct_names(names.into_iter().map(|(name, fallback)| {
let base: String = name.chars().map(|c| if c.is_alphanumeric() || matches!(c, '-' | '_' | '.') { c } else { '-' }).collect();
if base.trim_matches('.').is_empty() { fallback } else { base }
}))
}
/// `names` with `-2`, `-3`… after a name already used (compared without case).
pub(super) fn distinct_names(names: impl IntoIterator<Item = String>) -> Vec<String> {
distinct_names_by(names, str::to_lowercase)
+4 -4
View File
@@ -21,8 +21,8 @@ use super::encode::single_artboard;
use super::export::{export_crop, isolated, selected_artwork, without_artboards};
use super::naming::{self, NameParts};
use super::{
ARTBOARD_PARAMS, ArtboardPick, Format, artboard_file_names, create_dir, distinct_names_by, encode_with_warnings, merge, unique_file_names,
with_black_output, writable, write_file,
ARTBOARD_PARAMS, ArtboardPick, Format, artboard_file_names_keeping_dots, create_dir, distinct_names_by, encode_with_warnings, merge,
unique_file_names_keeping_dots, with_black_output, writable, write_file,
};
const C: &str = "document.exportForScreens";
@@ -1184,7 +1184,7 @@ fn items(
// the item folders are (`fold`).
let safe = |stems: Vec<String>| distinct_names_by(stems.iter().map(|s| safe_file_name(s)), fold);
let named = |pieces: Vec<(String, Arc<Document>, Option<usize>, Rect)>, fallback: &str| -> Vec<Item> {
let stems = safe(unique_file_names(pieces.iter().map(|(n, ..)| (n.as_str(), fallback.to_string()))));
let stems = safe(unique_file_names_keeping_dots(pieces.iter().map(|(n, ..)| (n.as_str(), fallback.to_string()))));
pieces
.into_iter()
.zip(stems)
@@ -1261,7 +1261,7 @@ fn items(
if boards.is_empty() {
return Err(bad(C, "the document has no artboard"));
}
let stems = safe(artboard_file_names(doc, &boards));
let stems = safe(artboard_file_names_keeping_dots(doc, &boards));
Ok(boards
.into_iter()
.zip(stems)
@@ -148,6 +148,18 @@ fn device_names_and_trailing_periods_follow_windows_naming_rules() {
assert_eq!(r["name"], "nul-.zip");
}
/// A dot in an artboard's name stays a dot (`example.artboard.png`, #6143); a name of only dots, or
/// ending in one, still can't name another folder or end in a period, and `/` and `:` are dashes.
#[test]
fn an_artboard_name_keeps_its_dots() {
let mut s = session(5);
for (index, name) in ["example.artboard", "v1.2.final", "..", "a/b:c", "trail."].iter().enumerate() {
s.execute("artboard.setProps", &json!({"index": index, "name": name})).unwrap();
}
let r = export(&mut s, json!({"formats": [{"format": "png"}]}));
assert_eq!(names(&r), ["example.artboard.png", "v1.2.final.png", "Artboard-3.png", "a-b-c.png", "trail.png"]);
}
/// `first_file` names the file the export writes first, for the same params: the item's file name,
/// a prefix and a suffix with characters Windows does not allow, a range, a Windows device name,
/// every kind of folder, presets, size suffixes, a name pattern with its counter and date, an