Save as Template: aiVersion 7 is an error for a .ait, and the dialog shows no Version

file.saveAsTemplate with format ait and aiVersion: "7" wrote the
legacy PostScript .ai of version 7 under the .ait name: for a
one-artboard test document, a 7,556-byte file starting %!PS-Adobe-3.0.
The web's Save as Template dialog showed the .ai Version menu for
.ait, whose Legacy (version 7) choice set aiVersion 7.

aiVersion 7 for a .ait is now an error, "aiVersion 7 writes legacy .ai
files; a .ait template is always PDF-compatible", and nothing is
written. The 17 catalogs that translate every message have a row for
it. The dialog shows no Version row for .ait, and aiVersion "pdf"
still writes the PDF-compatible template.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/file-format-parity.md
(two stale rows for .ai and .ait removed).
This commit is contained in:
dotCooCoo
2026-10-18 12:35:18 +07:00
committed by bflatastic
parent d23030ce2b
commit 0cf12a1611
26 changed files with 91 additions and 19 deletions
+13 -8
View File
@@ -35,15 +35,20 @@ use vectorcraft_geom::{Anchor, FillRule, PathData, Point, Rect, SubPath};
use super::super::*;
use super::{ArtboardPick, Encoded, Format};
/// Does `p` ask for a legacy `.ai` file of version 7 (`aiVersion: "7"`)? An unknown version is an error.
pub(super) fn wanted(cmd: &str, p: &Value) -> Result<bool> {
match p.get("aiVersion") {
None | Some(Value::Null) => Ok(false),
Some(Value::String(v)) if v.trim().is_empty() || v.eq_ignore_ascii_case("pdf") => Ok(false),
Some(Value::String(v)) if v.trim() == "7" => Ok(true),
Some(Value::Number(n)) if n.as_f64() == Some(7.0) => Ok(true),
Some(v) => Err(bad(cmd, format!("aiVersion {v}: \"pdf\" (PDF-compatible, the default) or \"7\" (legacy .ai, version 7)"))),
/// Does `p` ask a save in `f` for a legacy `.ai` file of version 7 (`aiVersion: "7"`)? An unknown
/// version is an error, and so is version 7 for a `.ait` template, which is PDF-compatible only.
pub(super) fn wanted(cmd: &str, f: &Format, p: &Value) -> Result<bool> {
let legacy = match p.get("aiVersion") {
None | Some(Value::Null) => false,
Some(Value::String(v)) if v.trim().is_empty() || v.eq_ignore_ascii_case("pdf") => false,
Some(Value::String(v)) if v.trim() == "7" => true,
Some(Value::Number(n)) if n.as_f64() == Some(7.0) => true,
Some(v) => return Err(bad(cmd, format!("aiVersion {v}: \"pdf\" (PDF-compatible, the default) or \"7\" (legacy .ai, version 7)"))),
};
if legacy && f.id == "ait" {
return Err(bad(cmd, "aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible"));
}
Ok(legacy)
}
/// What a legacy `.ai` does with the images it can't hold (`transparentImages`).
+1 -1
View File
@@ -103,7 +103,7 @@ pub const AI_OPTIONS: &[FormatOption] = &[
name: "aiVersion",
ty: "string",
default: "\"pdf\"",
description: "pdf (a PDF-compatible .ai carrying the VectorCraft document) or 7 (the legacy, PostScript-form .ai of version 7, which keeps layers, groups, compound paths and clipping groups in apps that read it; one file per artboard, transparent objects opaque; the other .ai options don't apply)",
description: "pdf (a PDF-compatible .ai carrying the VectorCraft document) or 7 (the legacy, PostScript-form .ai of version 7, which keeps layers, groups, compound paths and clipping groups in apps that read it; one file per artboard, transparent objects opaque; the other .ai options don't apply; an error for a .ait template)",
},
FormatOption {
name: "transparentImages",
+5 -5
View File
@@ -371,7 +371,7 @@ impl<W: std::io::Write> std::io::Write for Counted<W> {
/// `separateArtboards`, those `range` names (default all).
fn separate_boards(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Vec<usize>> {
// A legacy .ai file is a file per artboard already.
if !is_native(f) || !bool_or(p, "separateArtboards", false) || super::legacy_ai::wanted(cmd, p)? {
if !is_native(f) || !bool_or(p, "separateArtboards", false) || super::legacy_ai::wanted(cmd, f, p)? {
return Ok(vec![]);
}
let range = str_param(p, "range").filter(|r| !r.trim().is_empty()).map(str::to_string);
@@ -424,7 +424,7 @@ const NOT_PDF_COMPATIBLE: &str = "saved without PDF content: VectorCraft opens i
/// The shared Save and Export encoder for PDF-compatible `.ai` files (and `.ait` templates, the
/// same file).
pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Encoded> {
if super::legacy_ai::wanted(cmd, p)? {
if super::legacy_ai::wanted(cmd, f, p)? {
return super::legacy_ai::encode(cmd, f, doc, p);
}
let mut params = if p.is_object() { p.clone() } else { json!({}) };
@@ -508,7 +508,7 @@ fn job_for_stamped(s: &Session, st: &DocState, plan: SavePlan, stamped: Option<i
}
let boards = separate_boards(cmd, plan.format, &doc, &params)?;
let mut params = super::pdf::expand_preset(s, cmd, &params)?.into_owned();
let legacy = super::legacy_ai::wanted(cmd, &params)?;
let legacy = super::legacy_ai::wanted(cmd, plan.format, &params)?;
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.
@@ -558,7 +558,7 @@ impl SaveJob {
let (cmd, f) = (self.plan.mode.command(), self.plan.format);
let enc = if matches!(f.id, "ai" | "ait") { encode_ai(cmd, f, doc, &self.params)? } else { encode_all(doc, f.id, &self.params)? };
// A legacy .ai file per artboard.
let per_artboard = f.id == "ai" && super::legacy_ai::wanted(cmd, &self.params)?;
let per_artboard = f.id == "ai" && super::legacy_ai::wanted(cmd, f, &self.params)?;
if enc.files.len() != 1 && !per_artboard {
return Err(bad(cmd, "Save writes one artboard: name one, or export several with document.export"));
}
@@ -797,7 +797,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Save as Template…",
["File"],
None,
"{…as document.save} {path? (saved with the format's extension: one naming another format is replaced, and one is added to a name without it), format?: template (default) | ait (also when format is not given and the path ends in .ait), compress?, version?, preview?} a template that opens as a new untitled document: a native template (.vctemplate), or a PDF-compatible .ait written as a .ai file is (the .ai options and the PDF options, as document.save takes them for .ai); the document is unchanged → {path, format, bytes, warnings}; no path → {dataBase64, format, name: \"<name> template.<ext>\", folder: the Templates folder (preference templatesFolder), warnings}",
"{…as document.save} {path? (saved with the format's extension: one naming another format is replaced, and one is added to a name without it), format?: template (default) | ait (also when format is not given and the path ends in .ait), compress?, version?, preview?} a template that opens as a new untitled document: a native template (.vctemplate), or a PDF-compatible .ait written as a .ai file is (the .ai options and the PDF options, as document.save takes them for .ai; aiVersion 7 is an error); the document is unchanged → {path, format, bytes, warnings}; no path → {dataBase64, format, name: \"<name> template.<ext>\", folder: the Templates folder (preference templatesFolder), warnings}",
has_doc,
|s, p| save(s, SaveMode::Template, p)
),
@@ -254,6 +254,30 @@ fn exports_of_an_ait_name_save_as_template() {
assert_eq!(writable_format(Some("ps"), None).unwrap_err(), "PostScript files can be opened but not written (see document.formats)");
}
/// Save as Template refuses `aiVersion` 7 for a `.ait`, given in `options` or flat, and writes no
/// file; `aiVersion` "pdf" writes the PDF-compatible template.
#[test]
fn save_as_template_refuses_a_legacy_ait() {
let mut s = rich();
let path = tmp("legacy-ait/card.ait");
let dir = std::path::Path::new(&path).parent().unwrap().to_path_buf();
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for p in [
json!({"path": path, "options": {"aiVersion": "7"}}),
json!({"path": path, "format": "ait", "aiVersion": 7}),
json!({"format": "ait", "options": {"aiVersion": "7"}}),
] {
let e = s.execute("file.saveAsTemplate", &p).unwrap_err().to_string();
assert!(e.contains("aiVersion 7 writes legacy .ai files"), "{p}: {e}");
}
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "nothing written");
let r = s.execute("file.saveAsTemplate", &json!({"path": path, "options": {"aiVersion": "pdf"}})).unwrap();
assert_eq!(r["format"], "ait", "{r}");
let bytes = std::fs::read(&path).unwrap();
assert!(bytes.starts_with(b"%PDF") && vectorcraft_pdf::editing(&bytes).is_some_and(|e| e.intact), "PDF-compatible, carrying the document");
}
#[test]
fn standards_and_screens_leave_the_editing_data_out() {
let mut s = rich();
+13 -2
View File
@@ -75,7 +75,8 @@ fn separates(f: &Format) -> bool {
}
/// The options the dialog shows: arrays and objects are for agents, and a page range replaces the
/// single artboard choice; none for a format with its own dialog.
/// single artboard choice; none for a format with its own dialog. A `.ait` template has no
/// Version: Save as Template writes it PDF-compatible only.
fn shown(f: &Format) -> impl Iterator<Item = &'static FormatOption> {
let range = f.options.iter().any(|o| o.name == "range");
let separate = separates(f);
@@ -84,7 +85,8 @@ fn shown(f: &Format) -> impl Iterator<Item = &'static FormatOption> {
!(matches!(o.ty, "array" | "object")
|| (range && o.name == "artboard")
|| (separate && o.name == "range")
|| (matches!(f.id, "ai" | "ait") && o.name == "useArtboards"))
|| (matches!(f.id, "ai" | "ait") && o.name == "useArtboards")
|| (f.id == "ait" && o.name == "aiVersion"))
})
}
@@ -726,6 +728,15 @@ mod tests {
assert!(bytes.starts_with(b"%PDF"));
}
/// The `.ait` template's options have no Version row: Save as Template writes it
/// PDF-compatible only. A `.ai` file's options keep it.
#[test]
fn the_ait_template_options_have_no_version_row() {
let names = |id: &str| shown(fileio::format(id).unwrap()).map(|o| o.name).collect::<Vec<_>>();
assert!(names("ai").contains(&"aiVersion"), "{:?}", names("ai"));
assert!(!names("ait").contains(&"aiVersion"), "{:?}", names("ait"));
}
/// A name typed in the Save as Template panel with a Save As format's extension gets the
/// template's extension added, as Save As adds its own to a name it doesn't write.
#[test]
+1
View File
@@ -4071,6 +4071,7 @@ imageTrace Outline Obrys
@msg Save for Web: the image is empty Uložit pro web: obraz je prázdný
@msg Save writes {_1}, not {_2}: use document.export for other formats Uložení zapisuje {_1}, ne {_2}: pro jiné formáty použijte document.export
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Uložení zapisuje {_1}, ne {_2}: k jeho zápisu použijte file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 zapisuje starší soubory .ai; šablona .ait je vždy kompatibilní s PDF
@msg Saved recovery data for {title} Data pro obnovení uložena pro {title}
@msg Saved to User Defined: {_1} Uloženo do uživatelských knihoven: {_1}
@msg Saved {path} Uloženo: {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3705,6 +3705,7 @@ theme Appearance Erscheinungsbild
@msg Save for Web: the image is empty Für Web speichern: Das Bild ist leer
@msg Save writes {_1}, not {_2}: use document.export for other formats „Speichern“ schreibt {_1}, nicht {_2}: Verwenden Sie document.export für andere Formate
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it „Speichern“ schreibt {_1}, nicht {_2}: Verwenden Sie dafür file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 schreibt ältere .ai-Dateien; eine .ait-Vorlage ist immer PDF-kompatibel
@msg Saved recovery data for {title} Wiederherstellungsdaten für {title} gespeichert
@msg Saved to User Defined: {_1} In „Benutzerdefiniert“ gespeichert: {_1}
@msg Saved {path} {path} gespeichert
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2063,6 +2063,7 @@ axis Both Και τα δύο
@msg Save for Web: the image is empty Αποθήκευση για ιστό: η εικόνα είναι κενή
@msg Save writes {_1}, not {_2}: use document.export for other formats Η αποθήκευση γράφει {_1}, όχι {_2}: χρησιμοποιήστε το document.export για άλλες μορφές
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Η αποθήκευση γράφει {_1}, όχι {_2}: χρησιμοποιήστε το file.saveAsTemplate για να το γράψετε
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible Το aiVersion 7 γράφει αρχεία .ai παλαιού τύπου· ένα πρότυπο .ait είναι πάντα συμβατό με PDF
@msg Saved recovery data for {title} Αποθηκεύτηκαν δεδομένα ανάκτησης για το {title}
@msg Saved to User Defined: {_1} Αποθηκεύτηκε στις προσαρμοσμένες ρυθμίσεις: {_1}
@msg Saved {path} Αποθηκεύτηκε το {path}
Can't render this file because it contains an unexpected character in line 2005 and column 19.
+1
View File
@@ -3768,6 +3768,7 @@ axis Both Ambos
@msg Save for Web: the image is empty Guardar para web: la imagen está vacía
@msg Save writes {_1}, not {_2}: use document.export for other formats Guardar escribe {_1}, no {_2}: usa document.export para otros formatos
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Guardar escribe {_1}, no {_2}: usa file.saveAsTemplate para escribirlo
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 escribe archivos .ai heredados; una plantilla .ait siempre es compatible con PDF
@msg Saved recovery data for {title} Se guardaron los datos de recuperación de {title}
@msg Saved to User Defined: {_1} Se guardó en Definido por el usuario: {_1}
@msg Saved {path} Se guardó {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3998,6 +3998,7 @@ imageTrace Outline Ääriviiva
@msg Save for Web: the image is empty Tallenna verkkoon: kuva on tyhjä
@msg Save writes {_1}, not {_2}: use document.export for other formats Tallenna kirjoittaa muotoa {_1}, ei {_2}: käytä muille muodoille komentoa document.export
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Tallenna kirjoittaa muotoa {_1}, ei {_2}: kirjoita se komennolla file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 kirjoittaa vanhoja .ai-tiedostoja; .ait-malli on aina PDF-yhteensopiva
@msg Saved recovery data for {title} Tallennettiin palautustiedot: {title}
@msg Saved to User Defined: {_1} Tallennettiin kohteeseen Käyttäjän määrittämä: {_1}
@msg Saved {path} Tallennettiin {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3765,6 +3765,7 @@ axis Both Les deux
@msg Save for Web: the image is empty Enregistrer pour le Web : l’image est vide
@msg Save writes {_1}, not {_2}: use document.export for other formats Enregistrer écrit du {_1}, pas du {_2} : utilisez document.export pour les autres formats
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Enregistrer écrit du {_1}, pas du {_2} : utilisez file.saveAsTemplate pour l’écrire
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 écrit des fichiers .ai hérités, alors qu’un modèle .ait est toujours compatible PDF
@msg Saved recovery data for {title} Données de récupération enregistrées pour {title}
@msg Saved to User Defined: {_1} Enregistré dans Défini par l’utilisateur : {_1}
@msg Saved {path} {path} enregistré
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4754,6 +4754,7 @@ imageTrace Outline Körvonal
@msg Save for Web: the image is empty Mentés webre: a kép üres
@msg Save writes {_1}, not {_2}: use document.export for other formats a Mentés ezt írja: {_1}, ezt nem: {_2}; más formátumhoz használd a document.export parancsot
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it a Mentés ezt írja: {_1}, ezt nem: {_2}; ennek írásához használd a file.saveAsTemplate parancsot
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible az aiVersion 7 régi .ai fájlokat ír; egy .ait sablon mindig PDF-kompatibilis
@msg saved panel layout is invalid; reset the workspace first a mentett panelelrendezés érvénytelen; előbb állítsd vissza a munkaterületet
@msg Saved recovery data for {title} Helyreállítási adatok mentve: {title}
@msg Saved to User Defined: {_1} Mentve a felhasználóiak közé: {_1}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3807,6 +3807,7 @@ axis Both Entrambi
@msg Save for Web: the image is empty Salva per il web: l'immagine è vuota
@msg Save writes {_1}, not {_2}: use document.export for other formats Salva scrive {_1}, non {_2}: usa document.export per gli altri formati
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Salva scrive {_1}, non {_2}: usa file.saveAsTemplate per scriverlo
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 scrive file .ai tradizionali; un modello .ait è sempre compatibile PDF
@msg Saved recovery data for {title} Dati di recupero salvati per {title}
@msg Saved to User Defined: {_1} Salvato in Definito dall'utente: {_1}
@msg Saved {path} Salvato {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3906,6 +3906,7 @@ swatch Bright Violet ビビッドバイオレット
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: この環境にはファイルシステムがありません (開くには dataBase64 を渡し、dataBase64 を受け取るにはパスを省略してください)
@msg Save writes {_1}, not {_2}: use document.export for other formats 保存できる形式は {_1} のみで、{_2} 形式では保存できません: ほかの形式には document.export を使用してください
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it 保存できる形式は {_1} のみで、{_2} 形式では保存できません: この形式で保存するには file.saveAsTemplate を使用してください
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 は従来の .ai ファイルを書き出します。.ait テンプレートは常に PDF 互換です
@msg no changes since the last save 前回の保存以降、変更はありません
@msg unknown format `{f}` (Save writes {_1}) `{f}` は不明な形式です (保存できる形式: {_1})
@msg `svg` must be an object of SVG options (see document.formats) `svg` には SVG オプションのオブジェクトを指定してください (document.formats を参照)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3838,6 +3838,7 @@ theme Appearance 외관
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: 여기에는 파일 시스템이 없습니다(열려면 dataBase64를 전달하고, dataBase64를 얻으려면 경로를 생략하세요)
@msg Save writes {_1}, not {_2}: use document.export for other formats 저장은 {_2}이(가) 아닌 {_1}(으)로 기록합니다. 다른 형식은 document.export를 사용하세요
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it 저장은 {_2}이(가) 아닌 {_1}(으)로 기록합니다. 이 형식으로 기록하려면 file.saveAsTemplate를 사용하세요
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7은(는) 레거시 .ai 파일을 기록합니다. .ait 템플릿은 항상 PDF 호환입니다
@msg no changes since the last save 마지막 저장 이후 변경 내용이 없습니다
@msg unknown format `{f}` (Save writes {_1}) 알 수 없는 형식 `{f}`(저장은 {_1}(으)로 기록)
@msg `svg` must be an object of SVG options (see document.formats) `svg`는 SVG 옵션 개체여야 합니다(document.formats 참조)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3893,6 +3893,7 @@ imageTrace Outline Kontur
@msg Save for Web: the image is empty Zapisz dla Internetu: obraz jest pusty
@msg Save writes {_1}, not {_2}: use document.export for other formats Zapis tworzy {_1}, nie {_2}: dla innych formatów użyj document.export
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Zapis tworzy {_1}, nie {_2}: aby go zapisać, użyj file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 zapisuje starsze pliki .ai; szablon .ait jest zawsze zgodny z PDF
@msg Saved recovery data for {title} Zapisano dane odzyskiwania dla {title}
@msg Saved to User Defined: {_1} Zapisano w zdefiniowanych przez użytkownika: {_1}
@msg Saved {path} Zapisano {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3358,6 +3358,7 @@ axis Both Ambos
@msg Save for Web: the image is empty Salvar para Web: a imagem está vazia
@msg Save writes {_1}, not {_2}: use document.export for other formats Salvar grava {_1}, não {_2}: use document.export para outros formatos
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Salvar grava {_1}, não {_2}: use file.saveAsTemplate para gravá-lo
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 grava arquivos .ai legados; um modelo .ait é sempre compatível com PDF
@msg Saved recovery data for {title} Dados de recuperação de {title} salvos
@msg Saved to User Defined: {_1} Salvo em Definido pelo usuário: {_1}
@msg Saved {path} {path} salvo
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -3803,6 +3803,7 @@ axis Both Оба
@msg Save for Web: the image is empty Сохранить для web: изображение пусто
@msg Save writes {_1}, not {_2}: use document.export for other formats Сохранение записывает {_1}, а не {_2}: используйте document.export для других форматов
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Сохранение записывает {_1}, а не {_2}: чтобы записать его, используйте file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 записывает устаревшие файлы .ai; шаблон .ait всегда совместим с PDF
@msg Saved recovery data for {title} Сохранены данные восстановления для {title}
@msg Saved to User Defined: {_1} Сохранено в «Пользовательский»: {_1}
@msg Saved {path} Сохранено: {path}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3697,6 +3697,7 @@ axis Both Båda
@msg Save for Web: the image is empty Spara för webb: bilden är tom
@msg Save writes {_1}, not {_2}: use document.export for other formats Spara skriver {_1}, inte {_2}: använd document.export för andra format
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Spara skriver {_1}, inte {_2}: använd file.saveAsTemplate för att skriva det
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 skriver äldre .ai-filer; en .ait-mall är alltid PDF-kompatibel
@msg Saved recovery data for {title} Sparade återställningsdata för {title}
@msg Saved to User Defined: {_1} Sparat i Användardefinierat: {_1}
@msg Saved {path} Sparade {path}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+15
View File
@@ -1665,6 +1665,21 @@ fn blend_spine_param_errors_are_translated() {
assert!(untranslated.is_empty(), "untranslated spine errors:\n{}", untranslated.join("\n"));
}
/// The parameter error of Save as Template for a `.ait` with `aiVersion` 7 is translated whole in
/// each language of [`COMPLETE_MESSAGES`].
#[test]
fn legacy_ait_param_error_is_translated() {
use serde_json::json;
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({})).unwrap();
let e = s.execute("file.saveAsTemplate", &json!({"format": "ait", "aiVersion": "7"})).unwrap_err();
let vectorcraft_engine::EngineError::BadParams { msg, .. } = &e else { panic!("{e}") };
for code in COMPLETE_MESSAGES {
let shown = message(Lang::from_code(code).unwrap(), &e.to_string());
assert!(!shown.contains(msg.as_str()), "{code}: {shown}");
}
}
/// The errors for a parameter key a command doesn't take (#1442), whose text the scan above
/// doesn't read, are translated whole in each language of [`COMPLETE_MESSAGES`].
#[test]
+1
View File
@@ -3791,6 +3791,7 @@ axis Both Обидві
@msg Save for Web: the image is empty Зберегти для Інтернету: зображення пусте
@msg Save writes {_1}, not {_2}: use document.export for other formats Збереження записує {_1}, а не {_2}: використовуйте document.export для інших форматів
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Збереження записує {_1}, а не {_2}: щоб записати його, використовуйте file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 записує застарілі файли .ai; шаблон .ait завжди сумісний із PDF
@msg Saved recovery data for {title} Збережено дані відновлення для {title}
@msg Saved to User Defined: {_1} Збережено в «Користувацькі»: {_1}
@msg Saved {path} Збережено: {path}
Can't render this file because it contains an unexpected character in line 725 and column 40.
+1
View File
@@ -4404,6 +4404,7 @@ theme Appearance Giao diện
@msg Save for Web: the image is empty Lưu cho Web: ảnh trống
@msg Save writes {_1}, not {_2}: use document.export for other formats Lưu ghi {_1}, không phải {_2}: dùng document.export cho các định dạng khác
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Lưu ghi {_1}, không phải {_2}: dùng file.saveAsTemplate để ghi định dạng này
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 ghi tệp .ai cũ; mẫu .ait luôn tương thích PDF
@msg saved panel layout is invalid; reset the workspace first bố cục bảng đã lưu không hợp lệ; hãy đặt lại không gian làm việc trước
@msg Saved recovery data for {title} Đã lưu dữ liệu phục hồi cho {title}
@msg Saved to User Defined: {_1} Đã lưu vào Người dùng tùy chỉnh: {_1}
Can't render this file because it contains an unexpected character in line 3947 and column 10.
+1
View File
@@ -5000,6 +5000,7 @@ lighting Light 光源
Enable Guides for 9-Slice Scaling 启用 9-slice 缩放参考线
@msg cancelled; nothing was changed 已取消;未做任何更改
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it 保存会写入 {_1},而不是 {_2}:要写入该格式请使用 file.saveAsTemplate
@msg aiVersion 7 writes legacy .ai files; a .ait template is always PDF-compatible aiVersion 7 写入旧版 .ai 文件;.ait 模板始终与 PDF 兼容
@msg a mask has more than one art block 蒙版包含多个图稿块
@msg mask art ends outside a mask dictionary 蒙版图稿在蒙版字典之外结束
@msg `{key}` must be a #rrggbb color `{key}` 必须是 #rrggbb 格式的颜色
Can't render this file because it contains an unexpected character in line 2 and column 73.
-2
View File
@@ -90,10 +90,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Format | Illustrator | Ours: open/place | Ours: save/export | Fidelity and tests |
|---|---|---|---|---|
| VectorCraft `.vectorcraft` / `.vctemplate` | — | yes | yes | lossless JSON, versioned, save down to v1; property-tested round trips (`format/tests/prop_format.rs`); documents with a lot of path data are written as compact JSON, about a fifth of the size (#1492); a save falls back to a plain fsync and to writing elsewhere and copying in when the folder refuses the usual steps (#1625, #1649, part of #1620; not confirmed on a real network share); the size limits on embedded data also apply to files read from memory (#1783), and saving over a private file on Unix leaves no readable temporary copy beside it (#1778) |
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure, File Info) | PDF-compatible only | see above; no real-file corpus |
| Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) |
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus |
| Illustrator template `.ait` | open, save | yes | no (save as `.vctemplate`) | |
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text placement issues in some files (#722); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781) |
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
+2 -1
View File
@@ -485,7 +485,8 @@ Saving beyond `save_file` goes through `run_command`: `file.saveAs {path?, forma
on the new file and format; for `format: "ait"`, or a `.ait` path without `format`, it fails with an error that names
`file.saveAsTemplate`), `file.saveCopy` (the document keeps its path, title and modified state),
`file.saveAsTemplate {path?, format?}` (it writes a `.vctemplate`, or, for `format: "ait"` or a `.ait` path, a
PDF-compatible `.ait` written as a `.ai` file is; both open untitled; a path whose extension names another format has
PDF-compatible `.ait` written as a `.ai` file is, and with `aiVersion: "7"` it fails with an error; both open untitled;
a path whose extension names another format has
it replaced by the template's extension, and any other path that does not end in that extension has it added, so
`card.png` is saved as `card.vctemplate` and `card` as `card.vctemplate`; the suggested name and the Templates folder
come back without a path), `file.newFromTemplate {path}`, `file.revert` and `file.formatOptions {format?}` (a format's options
+1
View File
@@ -690,6 +690,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing).
- macOS: the save sheet of Save As, Save a Copy, Save as Template and the first Save of a new document has a Format menu that lists each format the save can write, with the suggested file's format selected. Choosing a format changes the extension in the name field, and a name typed without an extension gets the chosen format's. A name typed with another listed extension is saved in that format. The menu is set up before the sheet opens; a save panel with one file type is shown by rfd as before (#866, `apps/vectorcraft/src/mac_save_format.rs`).
- Save as Template writes a PDF-compatible `.ait` as well as a `.vctemplate`, which stays the default: the save panel and the web build's Save as Template dialog list both, and New from Template opens the `.ait` as an untitled document with its layers and art (`file.saveAsTemplate {format: "ait"}`, or a path ending in `.ait`).
Save as Template always writes a file named with its template type's extension: `file.saveAsTemplate` saves a path ending in another format's extension (`card.png`) as `card.vctemplate` (or `card.ait`), the desktop save panel adds `.vctemplate` to a name typed with a Save As format's extension (`card.pdf.vctemplate`), and `file.saveAs` and `document.save` refuse `ait` with an error that names `file.saveAsTemplate`. A `.ait` is PDF-compatible only: `aiVersion: "7"` is an error for it, and the web build's dialog shows no Version row for it.
Save as Template always writes a file named with its template type's extension: `file.saveAsTemplate` saves a path ending in another format's extension (`card.png`) as `card.vctemplate` (or `card.ait`), the desktop save panel adds `.vctemplate` to a name typed with a Save As format's extension (`card.pdf.vctemplate`), and `file.saveAs` and `document.save` refuse `ait` with an error that names `file.saveAsTemplate`.
Exports refuse a `.ait` with an error that names `file.saveAsTemplate`, and `vectorcraft-cli convert in.pdf out.ait` and `run --export out.ait` write the template as `file.saveAsTemplate` does.
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.