mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 03:01:33 +00:00
Merge pull request #5517 from dotCooCoo/fix/ai-save-missing-artboard
.ai save: an artboard the document does not have is refused, as an export refuses it
This commit is contained in:
@@ -608,6 +608,28 @@ fn named_for_save(cmd: &str, e: crate::EngineError) -> crate::EngineError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuse the artboards a `.ai` or `.ait` save is given (`artboard`, `artboards`, or a `range`
|
||||
/// that isn't empty) when one of them is not in `doc`, with the error an export gives. The save
|
||||
/// then writes every artboard, or a legacy `.ai` file's `range`, whatever `artboard` and
|
||||
/// `artboards` name. An empty `artboards` list is not refused: the save writes every artboard.
|
||||
fn named_artboards_exist(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<()> {
|
||||
let n = doc.artboards.len();
|
||||
// The encoders refuse a document without artboards themselves.
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
// The encoders refuse a value of the wrong type themselves, each with its own label.
|
||||
let Ok(mut picked) = super::encode::options::<ArtboardPick>(&written_as(cmd, f, p), p) else {
|
||||
return Ok(());
|
||||
};
|
||||
picked.range = picked.range.filter(|r| !r.trim().is_empty());
|
||||
if picked.range.is_none() && picked.artboards.as_ref().is_some_and(Vec::is_empty) {
|
||||
return Ok(());
|
||||
}
|
||||
picked.resolve(n).map_err(|e| bad(cmd, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `.ai` option `editingData`: whether the file also carries the editing copy of the art that
|
||||
/// other apps read (default on).
|
||||
#[derive(Default, serde::Deserialize)]
|
||||
@@ -736,11 +758,16 @@ fn job_for_stamped(s: &Session, st: &DocState, plan: SavePlan, stamped: Option<i
|
||||
let boards = separate_boards(cmd, plan.format, &doc, ¶ms)?;
|
||||
let mut params = super::pdf::expand_preset(s, cmd, ¶ms)?.into_owned();
|
||||
let legacy = super::legacy_ai::wanted(cmd, plan.format, ¶ms)?;
|
||||
if matches!(plan.format.id, "ai" | "ait") {
|
||||
named_artboards_exist(cmd, plan.format, &doc, ¶ms)?;
|
||||
}
|
||||
if let (Some(o), "ai" | "ait") = (params.as_object_mut(), plan.format.id) {
|
||||
// Save keeps every artboard; exports may choose a subset instead. A legacy .ai file per
|
||||
// artboard keeps a range given: the artboards to save.
|
||||
let chosen = legacy && plan.format.id == "ai" && o.get("range").and_then(Value::as_str).is_some_and(|r| !r.trim().is_empty());
|
||||
if !chosen {
|
||||
// A `range` that isn't text is left for the encoder to refuse, as an export refuses it.
|
||||
let text = o.get("range").is_none_or(|r| r.is_string() || r.is_null());
|
||||
if !chosen && text {
|
||||
o.insert("range".into(), json!("all"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +525,42 @@ fn a_range_saves_those_artboards_and_save_writes_them_again() {
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{r}");
|
||||
}
|
||||
|
||||
/// The message of a refused command's error.
|
||||
fn refusal(s: &mut Session, cmd: &str, p: &Value) -> String {
|
||||
match s.execute(cmd, p) {
|
||||
Err(EngineError::BadParams { cmd: c, msg }) if c == cmd => msg,
|
||||
other => panic!("{cmd} {p}: {:?}", other.map(|r| r["files"].as_array().map(Vec::len))),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_artboard_the_document_does_not_have_is_refused_as_an_export_refuses_it() {
|
||||
let dir = Folder::new("ai7-no-artboard");
|
||||
let mut s = three_boards();
|
||||
let path = dir.file("Poster.ai");
|
||||
for pick in [json!({"artboards": [5]}), json!({"artboards": [0, 3]}), json!({"artboard": 3}), json!({"range": "4"})] {
|
||||
let mut p = json!({"format": "ai", "aiVersion": "7"});
|
||||
p.as_object_mut().unwrap().extend(pick.as_object().unwrap().clone());
|
||||
let export = refusal(&mut s, "document.export", &p);
|
||||
assert!(export.starts_with("no artboard") || export.starts_with("range `4`"), "{p}: {export}");
|
||||
assert_eq!(refusal(&mut s, "file.saveCopy", &p), export, "{p}");
|
||||
p["path"] = json!(path);
|
||||
assert_eq!(refusal(&mut s, "file.saveAs", &p), export, "{p}");
|
||||
assert_eq!(refusal(&mut s, "document.save", &p), export, "{p}");
|
||||
}
|
||||
// A `range` that isn't text is refused as an export refuses it.
|
||||
let p = json!({"format": "ai", "aiVersion": "7", "range": 42});
|
||||
assert_eq!(refusal(&mut s, "file.saveCopy", &p), refusal(&mut s, "document.export", &p));
|
||||
assert_eq!(std::fs::read_dir(&dir.0).unwrap().count(), 0, "nothing is written");
|
||||
// An artboard the document has is saved with the artboards of the range: every one.
|
||||
let r = s.execute("file.saveAs", &json!({"path": path, "format": "ai", "aiVersion": "7", "artboards": [2]})).unwrap();
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(3), "{r}");
|
||||
// Once that artboard is deleted, Save is not refused for it.
|
||||
s.execute("artboard.delete", &json!({"index": 2})).unwrap();
|
||||
let r = s.execute("document.save", &json!({})).unwrap();
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{r}");
|
||||
}
|
||||
|
||||
/// A 32×32 PNG checkerboard of 16-pixel squares: `a` top left and bottom right, `b` elsewhere.
|
||||
fn checker(a: [u8; 4], b: [u8; 4]) -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(32, 32, |x, y| image::Rgba(if (x < 16) == (y < 16) { a } else { b }));
|
||||
|
||||
@@ -244,6 +244,59 @@ fn ai_option_errors_name_a_file_saved_without_pdf_content() {
|
||||
assert!(r["warnings"].to_string().contains("`version` was not used: PDF-compatible .ai files don't read it"), "{r}");
|
||||
}
|
||||
|
||||
/// The message of a refused command's error.
|
||||
fn refusal(s: &mut Session, cmd: &str, p: &Value) -> String {
|
||||
match s.execute(cmd, p) {
|
||||
Err(EngineError::BadParams { cmd: c, msg }) if c == cmd => msg,
|
||||
other => panic!("{cmd} {p}: {:?}", other.map(|r| r["bytes"].clone())),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_save_refuses_an_artboard_the_document_does_not_have_as_an_export_does() {
|
||||
let dir = Folder::new("ai-no-artboard");
|
||||
let mut s = three_boards();
|
||||
let path = dir.file("x.ai");
|
||||
let missing = [
|
||||
json!({"artboards": [5]}),
|
||||
json!({"artboards": [0, 3]}),
|
||||
json!({"artboard": 3}),
|
||||
json!({"range": "4"}),
|
||||
json!({"separateArtboards": true, "artboards": [5]}),
|
||||
];
|
||||
let wrong_type = [json!({"artboard": "x"}), json!({"artboards": ["x"]}), json!({"range": 42})];
|
||||
for pdf in [true, false] {
|
||||
for pick in missing.iter().chain(&wrong_type) {
|
||||
let mut p = json!({"format": "ai", "pdfCompatible": pdf});
|
||||
p.as_object_mut().unwrap().extend(pick.as_object().unwrap().clone());
|
||||
let export = refusal(&mut s, "document.export", &p);
|
||||
p["path"] = json!(path);
|
||||
for cmd in ["file.saveCopy", "file.saveAs", "document.save"] {
|
||||
assert_eq!(refusal(&mut s, cmd, &p), export, "{cmd} {p}");
|
||||
}
|
||||
// A template is not exported: its errors name the template where they name the format.
|
||||
p["format"] = json!("ait");
|
||||
let template = refusal(&mut s, "file.saveAsTemplate", &p);
|
||||
if missing.contains(pick) {
|
||||
assert_eq!(template, export, "{p}");
|
||||
} else {
|
||||
assert!(template.contains("invalid type"), "{p}: {template}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(&dir.0).unwrap().count(), 0, "nothing is written");
|
||||
// An artboard the document has, an empty `artboards` list, or `artboards` that a `range`
|
||||
// overrides is saved with every artboard, as before.
|
||||
for pick in [json!({"artboards": [1]}), json!({"artboards": []}), json!({"range": "1", "artboards": [5]})] {
|
||||
let mut p = json!({"format": "ai"});
|
||||
p.as_object_mut().unwrap().extend(pick.as_object().unwrap().clone());
|
||||
let r = s.execute("file.saveCopy", &p).unwrap();
|
||||
let mut o = Session::new();
|
||||
o.execute("document.open", &json!({"name": "x.ai", "dataBase64": r["dataBase64"]})).unwrap();
|
||||
assert_eq!(o.doc().unwrap().doc.artboards.len(), 3, "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_remembered_artboard_the_document_no_longer_has_is_dropped() {
|
||||
let dir = Folder::new("remembered-artboards");
|
||||
|
||||
Reference in New Issue
Block a user