Merge pull request #2914 from dotCooCoo/fix/pdfinfo-reads-postscript

MCP: document.pdfInfo answers one page for an EPS, a .ps file or a PostScript .ai
This commit is contained in:
BFlat
2026-10-19 00:08:04 +07:00
committed by GitHub
24 changed files with 124 additions and 20 deletions
+1 -1
View File
@@ -207,7 +207,7 @@ pub fn specs() -> Vec<CommandSpec> {
"PDF Info",
[],
None,
"{path | name? | dataBase64 (a file's path, or its bytes), password?, thumbnail?: page (1-based), thumbnailSize?: 160 (px, longest side), cropTo?: crop (the thumbnail's box)} → {pages, needsPassword, wrongPassword?, pageInfo: [{width, height (pt, as shown), rotation, boxes: {media, crop, bleed, trim, art: [x0, y0, x1, y1] (PDF space, pt)}}], thumbnail?: PNG dataBase64}; a page whose UserUnit (PDF 1.6) is above 1 gives its size and boxes in pt, its numbers times the UserUnit (one above 75,000 counts as 75,000); an encrypted PDF without its password → {pages: 0, needsPassword: true}",
"{path | name? | dataBase64 (a file's path, or its bytes), password?, thumbnail?: page (1-based), thumbnailSize?: 160 (px, longest side), cropTo?: crop (the thumbnail's box)} → {pages, needsPassword, wrongPassword?, pageInfo: [{width, height (pt, as shown), rotation, boxes: {media, crop, bleed, trim, art: [x0, y0, x1, y1] (PDF space, pt)}}], thumbnail?: PNG dataBase64}; a page whose UserUnit (PDF 1.6) is above 1 gives its size and boxes in pt, its numbers times the UserUnit (one above 75,000 counts as 75,000); an encrypted PDF without its password → {pages: 0, needsPassword: true}; a PostScript file (an EPS, a .ps file, a .ai in PostScript form) → {pages: 1, needsPassword: false, pageInfo: [{width, height (its bounding box, a US Letter page without one), rotation: 0, boxes: {}}]} (password and cropTo don't apply; thumbnail: 1 shows what the page prints)",
always,
pdfimport::pdf_info
),
+33 -5
View File
@@ -1,6 +1,6 @@
//! Opening and placing PDF (and PDF-compatible `.ai`/`.ait`) files: the options `document.open`
//! and `file.place` read ([`LoadOptions`]), the pages they import and `document.pdfInfo` (pages,
//! boxes, a thumbnail) for the Import PDF dialog.
//! boxes, a thumbnail; one page for a PostScript file) for the Import PDF dialog.
use serde_json::{Value, json};
use vectorcraft_doc::{ColorMode, Document};
@@ -162,6 +162,10 @@ const INFO: &str = "document.pdfInfo";
pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
let bytes = source(p, INFO)?.bytes;
let o = LoadOptions::from_params(INFO, p)?;
// Programs whose header is only `%!` are PostScript too, as `clipboard.importEps` reads them.
if bytes.starts_with(b"%!") || vectorcraft_pdf::is_postscript(&bytes) {
return postscript_info(&bytes, p);
}
let info = match vectorcraft_pdf::info(&bytes, o.password.as_deref()) {
Ok(i) => i,
Err(e @ (PdfError::NeedsPassword | PdfError::WrongPassword)) => {
@@ -183,10 +187,34 @@ pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
let page = usize::try_from(n).ok().and_then(|n| n.checked_sub(1)).filter(|i| *i < info.pages.len());
let page = page.ok_or_else(|| bad(INFO, format!("thumbnail: no page {n} (the PDF has {})", info.pages.len())))?;
let (doc, _) = page_document(&bytes, page, &o)?;
let size = f64_or(p, "thumbnailSize", 160.0).clamp(16.0, 1024.0);
let r = doc.artboards.first().map(|a| a.rect).ok_or_else(|| err("the page has no area"))?;
let png = super::encode(&doc, "png", &json!({"scale": size / r.width().max(r.height()).max(1.0)}))?;
out["thumbnail"] = json!(vectorcraft_format::base64_encode(&png));
out["thumbnail"] = json!(thumbnail(&doc, p)?);
}
Ok(out)
}
/// `document.pdfInfo` of a PostScript file (an EPS, a `.ps` file, a `.ai` in PostScript form): one
/// page, the page [`vectorcraft_eps::page`] reads, with no PDF boxes. Its thumbnail is the file as
/// the EPS reader reads it without any editing data it carries: what the page prints, or the
/// file's preview when its PostScript can't be read.
fn postscript_info(bytes: &[u8], p: &Value) -> Result<Value> {
let page = vectorcraft_eps::page(bytes).map_err(err)?;
let info = json!({"width": page.width(), "height": page.height(), "rotation": 0, "boxes": {}});
let mut out = json!({"pages": 1, "needsPassword": false, "pageInfo": [info]});
if let Some(n) = p.get("thumbnail").and_then(Value::as_u64) {
if n != 1 {
return Err(bad(INFO, format!("thumbnail: no page {n} (the file has 1)")));
}
let doc = vectorcraft_eps::import_with(bytes, false).map_err(err)?.document;
out["thumbnail"] = json!(thumbnail(&doc, p)?);
}
Ok(out)
}
/// A PNG of `doc`'s first artboard, `thumbnailSize` px (default 160, from 16 to 1024) on its longer
/// side, base64.
fn thumbnail(doc: &Document, p: &Value) -> Result<String> {
let size = f64_or(p, "thumbnailSize", 160.0).clamp(16.0, 1024.0);
let r = doc.artboards.first().map(|a| a.rect).ok_or_else(|| err("the page has no area"))?;
let png = super::encode(doc, "png", &json!({"scale": size / r.width().max(r.height()).max(1.0)}))?;
Ok(vectorcraft_format::base64_encode(&png))
}
@@ -157,27 +157,31 @@ fn a_postscript_ai_file_says_it_cannot_be_opened_yet() {
let e = s.execute("document.open", &json!({"name": name, "dataBase64": b64(ps)})).unwrap_err().to_string();
assert!(e.contains(name) && e.contains("PostScript") && e.contains("save it as PDF"), "{name}: {e}");
}
// An EPS with a binary header too, and the PDF info of either.
// An EPS with a binary header too, and the PDF info of each. The plain program's PDF info gives
// its page, and its thumbnail gives the reader's error; the binary file's header points at no
// PostScript, so its PDF info is an error.
let mut binary = vec![0xC5, 0xD0, 0xD3, 0xC6];
binary.extend_from_slice(&[0; 28]);
binary.extend_from_slice(ps);
assert!(s.execute("document.open", &json!({"name": "x.eps", "dataBase64": b64(&binary)})).unwrap_err().to_string().contains("PostScript"));
assert!(s.execute("document.pdfInfo", &json!({"dataBase64": b64(ps)})).unwrap_err().to_string().contains("PostScript"));
assert!(s.execute("document.pdfInfo", &json!({"dataBase64": b64(&binary)})).unwrap_err().to_string().contains("PostScript"));
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(ps)})).unwrap();
assert_eq!((v["pages"].as_u64(), v["pageInfo"][0]["width"].as_f64()), (Some(1), Some(100.0)), "{v}");
let e = s.execute("document.pdfInfo", &json!({"dataBase64": b64(ps), "thumbnail": 1})).unwrap_err().to_string();
assert!(e.contains("PostScript") && e.contains("frobnicate"), "{e}");
assert!(s.active().is_none());
}
/// PostScript given where a PDF is read (`document.pdfInfo`, `clipboard.importPdf`) is refused
/// with an error that names File › Open and File › Place, and both of those read the file.
/// PostScript given where a PDF is read (`clipboard.importPdf`) is refused with an error that
/// names File › Open and File › Place, and both of those read the file.
#[test]
fn postscript_read_as_a_pdf_names_open_and_place() {
let ps = b"%!PS-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n1 0 0 setrgbcolor 10 10 50 50 rectfill\nshowpage\n%%EOF\n";
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
for cmd in ["document.pdfInfo", "clipboard.importPdf"] {
let e = s.execute(cmd, &json!({"dataBase64": b64(ps)})).unwrap_err().to_string();
assert!(e.contains("PostScript") && e.contains("not a PDF: File › Open and File › Place read it as EPS"), "{cmd}: {e}");
assert!(!e.contains("can't be opened"), "{cmd}: {e}");
}
let e = s.execute("clipboard.importPdf", &json!({"dataBase64": b64(ps)})).unwrap_err().to_string();
assert!(e.contains("PostScript") && e.contains("not a PDF: File › Open and File › Place read it as EPS"), "{e}");
assert!(!e.contains("can't be opened"), "{e}");
for name in ["legacy.ai", "art.eps"] {
let r = s.execute("document.open", &json!({"name": name, "dataBase64": b64(ps)})).unwrap();
assert_eq!(r["warnings"], json!([]), "{name}: {r}");
@@ -186,6 +190,44 @@ fn postscript_read_as_a_pdf_names_open_and_place() {
}
}
/// `document.pdfInfo` reads PostScript (an EPS, a `.ai` in PostScript form, a `.ps` file) as one
/// page, its bounding box or a letter page without one, and its thumbnail shows that page. Each
/// file here opens on that page with `editingData: false`.
#[test]
fn pdf_info_reads_postscript_as_one_page() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
let data = |r: Value| vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
let eps = data(s.execute("document.exportEps", &json!({"useArtboards": true})).unwrap());
let ai = data(s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7"})).unwrap());
assert!(eps.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) && ai.starts_with(b"%!PS"));
let hires = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 101 41\n%%HiResBoundingBox: 0 0 100.5 40.25\n%%EndComments\n0 0 1 setrgbcolor 0 0 50 20 rectfill\n";
let letter = b"%!PS\n1 0 0 setrgbcolor 72 72 144 72 rectfill\nshowpage\n";
let bare = b"%!\n0 0 1 setrgbcolor 100 100 50 50 rectfill\nshowpage\n";
for (name, bytes, size) in [
("art.eps", &eps[..], (300.0, 200.0)),
("legacy.ai", &ai, (101.0, 51.0)),
("hires.eps", hires, (100.5, 40.25)),
("page.ps", letter, (612.0, 792.0)),
("bare.ps", bare, (612.0, 792.0)),
] {
let v = s.execute("document.pdfInfo", &json!({"dataBase64": b64(bytes), "thumbnail": 1, "thumbnailSize": 100})).unwrap();
let png = vectorcraft_format::base64_decode(v["thumbnail"].as_str().unwrap()).unwrap();
let img = image::load_from_memory(&png).unwrap();
assert_eq!(img.width().max(img.height()), 100, "{name}");
let mut v = v;
v["thumbnail"] = json!("png");
let page = json!({"width": size.0, "height": size.1, "rotation": 0, "boxes": {}});
assert_eq!(v, json!({"pages": 1, "needsPassword": false, "pageInfo": [page], "thumbnail": "png"}), "{name}");
s.execute("document.open", &json!({"name": name, "dataBase64": b64(bytes), "editingData": false})).unwrap();
let board = s.doc().unwrap().doc.artboards[0].rect;
assert_eq!((board.width(), board.height()), size, "{name}: the page document.open reads");
}
let e = s.execute("document.pdfInfo", &json!({"dataBase64": b64(&eps), "thumbnail": 2})).unwrap_err().to_string();
assert!(e.contains("no page 2"), "{e}");
}
/// A page showing only text, with an editor's private data when `private`.
fn text_page(private: bool, also: &str) -> PdfPage {
PdfPage {
+17 -3
View File
@@ -186,13 +186,27 @@ pub fn import(bytes: &[u8]) -> Result<Imported, String> {
import_with(bytes, true)
}
/// [`import`], an Illustrator EPS through the editing copy of its art only when `editing_data`
/// (else what its page prints, as any EPS).
pub fn import_with(bytes: &[u8], editing_data: bool) -> Result<Imported, String> {
/// The PostScript of an EPS or PostScript file (behind its binary header, if it has one).
fn program(bytes: &[u8]) -> Result<&[u8], String> {
let (ps, _) = crate::sections(bytes).ok_or("the file's preview header points outside the file")?;
if !ps.starts_with(b"%!") {
return Err("this is not a PostScript file".into());
}
Ok(ps)
}
/// An EPS or PostScript file's page at the origin, read from its comments without running its
/// program: its bounding box (`%%HiResBoundingBox` when it has one), a US Letter page without
/// one. [`import_with`] runs the program on the same page when it reads what the page prints.
pub fn page(bytes: &[u8]) -> Result<Rect, String> {
let [llx, lly, urx, ury] = Dsc::read(program(bytes)?).bbox.unwrap_or(LETTER);
Ok(Rect::new(0.0, 0.0, urx - llx, ury - lly))
}
/// [`import`], an Illustrator EPS through the editing copy of its art only when `editing_data`
/// (else what its page prints, as any EPS).
pub fn import_with(bytes: &[u8], editing_data: bool) -> Result<Imported, String> {
let ps = program(bytes)?;
let dsc = Dsc::read(ps);
// A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no
// type), and its program has all of it, which is read as the format (as before the file
+1 -1
View File
@@ -37,7 +37,7 @@ mod ps;
mod scene;
mod tiff;
pub use import::{Imported, ai_alone, family_style, import, import_with, is_loss, layered_ai};
pub use import::{Imported, ai_alone, family_style, import, import_with, is_loss, layered_ai, page};
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai};
pub use print::{PrintJob, PrintPage, print};
+1
View File
@@ -4356,6 +4356,7 @@ imageTrace Outline Obrys
@msg object {id} is not a selected top-level object objekt {id} není vybraný objekt nejvyšší úrovně
@msg page size {w} × {h} velikost stránky {w} × {h}
@msg page {0} does not exist (the PDF has {1}) stránka {0} neexistuje (PDF jich má {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: stránka {n} neexistuje (soubor má 1)
@msg paste unavailable vložení není k dispozici
@msg path is already closed cesta už je uzavřená
@msg paths contain invalid coordinates cesty obsahují neplatné souřadnice
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3993,6 +3993,7 @@ theme Appearance Erscheinungsbild
@msg object {id} is not a selected top-level object Objekt {id} ist kein ausgewähltes Objekt der obersten Stufe
@msg page size {w} × {h} Seitengröße {w} × {h}
@msg page {0} does not exist (the PDF has {1}) Seite {0} ist nicht vorhanden (die PDF-Datei hat {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: Seite {n} ist nicht vorhanden (die Datei hat 1)
@msg paste unavailable Einfügen nicht verfügbar
@msg path is already closed der Pfad ist bereits geschlossen
@msg paths contain invalid coordinates die Pfade enthalten ungültige Koordinaten
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4032,6 +4032,7 @@ theme Appearance Εμφάνιση
@msg object {id} is not a selected top-level object το αντικείμενο {id} δεν είναι επιλεγμένο αντικείμενο ανώτατης βαθμίδας
@msg page size {w} × {h} μέγεθος σελίδας {w} × {h}
@msg page {0} does not exist (the PDF has {1}) η σελίδα {0} δεν υπάρχει (το PDF έχει {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: η σελίδα {n} δεν υπάρχει (το αρχείο έχει 1)
@msg paste unavailable η επικόλληση δεν είναι διαθέσιμη
@msg path is already closed η διαδρομή είναι ήδη κλειστή
@msg paths contain invalid coordinates οι διαδρομές περιέχουν μη έγκυρες συντεταγμένες
Can't render this file because it contains an unexpected character in line 2006 and column 19.
+1
View File
@@ -4055,6 +4055,7 @@ axis Both Ambos
@msg object {id} is not a selected top-level object el objeto {id} no es un objeto seleccionado de nivel superior
@msg page size {w} × {h} tamaño de página {w} × {h}
@msg page {0} does not exist (the PDF has {1}) la página {0} no existe (el PDF tiene {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: la página {n} no existe (el archivo tiene 1)
@msg paste unavailable pegar no está disponible
@msg path is already closed el trazado ya está cerrado
@msg paths contain invalid coordinates los trazados contienen coordenadas no válidas
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4279,6 +4279,7 @@ imageTrace Outline Ääriviiva
@msg object {id} is not a selected top-level object objekti {id} ei ole valittu ylimmän tason objekti
@msg page size {w} × {h} sivun koko {w} × {h}
@msg page {0} does not exist (the PDF has {1}) sivua {0} ei ole olemassa (PDF-tiedostossa on {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: sivua {n} ei ole olemassa (tiedostossa on 1)
@msg paste unavailable liittäminen ei ole käytettävissä
@msg path is already closed polku on jo suljettu
@msg paths contain invalid coordinates polut sisältävät virheellisiä koordinaatteja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4052,6 +4052,7 @@ axis Both Les deux
@msg object {id} is not a selected top-level object l’objet {id} n’est pas un objet sélectionné de premier niveau
@msg page size {w} × {h} format de page {w} × {h}
@msg page {0} does not exist (the PDF has {1}) la page {0} n’existe pas (le PDF en a {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail : la page {n} n’existe pas (le fichier en a 1)
@msg paste unavailable collage indisponible
@msg path is already closed le tracé est déjà fermé
@msg paths contain invalid coordinates les tracés contiennent des coordonnées non valides
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4713,6 +4713,7 @@ imageTrace Outline Körvonal
@msg Packaged {n} file(s){note} Becsomagolt fájlok: {n}{note}
@msg page size {w} × {h} oldalméret: {w} × {h}
@msg page {0} does not exist (the PDF has {1}) nincs ilyen oldal: {0} (a PDF oldalszáma: {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: nincs ilyen oldal: {n} (a fájl oldalszáma: 1)
@msg panel group is empty a panelcsoport üres
@msg panel must name a registered panel a panel egy regisztrált panelt kell megnevezzen
@msg paste unavailable a beillesztés nem érhető el
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4094,6 +4094,7 @@ axis Both Entrambi
@msg object {id} is not a selected top-level object l'oggetto {id} non è un oggetto selezionato di primo livello
@msg page size {w} × {h} dimensioni pagina {w} × {h}
@msg page {0} does not exist (the PDF has {1}) la pagina {0} non esiste (il PDF ne ha {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: la pagina {n} non esiste (il file ne ha 1)
@msg paste unavailable impossibile incollare
@msg path is already closed il tracciato è già chiuso
@msg paths contain invalid coordinates i tracciati contengono coordinate non valide
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4166,6 +4166,7 @@ swatch Bright Violet ビビッドバイオレット
@msg cannot read PDF: {0} PDF を読み込めません: {0}
@msg invalid PDF setting: {0} PDF の設定が正しくありません: {0}
@msg page {0} does not exist (the PDF has {1}) ページ {0} は存在しません (この PDF のページ数は {1} です)
@msg thumbnail: no page {n} (the file has 1) thumbnail: ページ {n} は存在しません (このファイルのページ数は 1 です)
@msg the PDF has no pages PDF にページがありません
@msg the PDF is password-protected: give its password この PDF はパスワードで保護されています: パスワードを入力してください
@msg the PDF password is wrong PDF のパスワードが正しくありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4098,6 +4098,7 @@ theme Appearance 외관
@msg cannot read PDF: {0} PDF를 읽을 수 없습니다: {0}
@msg invalid PDF setting: {0} 잘못된 PDF 설정: {0}
@msg page {0} does not exist (the PDF has {1}) 페이지 {0}이(가) 없습니다(PDF의 페이지 수: {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: 페이지 {n}이(가) 없습니다(파일의 페이지 수: 1)
@msg the PDF has no pages PDF에 페이지가 없습니다
@msg the PDF is password-protected: give its password PDF가 암호로 보호되어 있습니다: 암호를 입력하세요
@msg the PDF password is wrong PDF 암호가 올바르지 않습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4180,6 +4180,7 @@ imageTrace Outline Kontur
@msg object {id} is not a selected top-level object obiekt {id} nie jest zaznaczonym obiektem najwyższego poziomu
@msg page size {w} × {h} rozmiar strony {w} × {h}
@msg page {0} does not exist (the PDF has {1}) strona {0} nie istnieje (PDF ma {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: strona {n} nie istnieje (plik ma 1)
@msg paste unavailable wklejanie niedostępne
@msg path is already closed ścieżka jest już zamknięta
@msg paths contain invalid coordinates ścieżki zawierają nieprawidłowe współrzędne
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3636,6 +3636,7 @@ axis Both Ambos
@msg object {id} is not a path o objeto {id} não é um traçado
@msg page size {w} × {h} tamanho da página {w} × {h}
@msg page {0} does not exist (the PDF has {1}) a página {0} não existe (o PDF tem {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: a página {n} não existe (o arquivo tem 1)
@msg paste unavailable colar indisponível
@msg path is already closed o traçado já está fechado
@msg paths contain invalid coordinates os traçados contêm coordenadas inválidas
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4090,6 +4090,7 @@ axis Both Оба
@msg object {id} is not a selected top-level object объект {id} не является выделенным объектом верхнего уровня
@msg page size {w} × {h} размер страницы {w} × {h}
@msg page {0} does not exist (the PDF has {1}) страница {0} не существует (в PDF {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: страница {n} не существует (в файле 1)
@msg paste unavailable вставка недоступна
@msg path is already closed контур уже замкнут
@msg paths contain invalid coordinates контуры содержат недопустимые координаты
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3980,6 +3980,7 @@ axis Both Båda
@msg object {id} is not a selected top-level object objekt {id} är inte ett markerat objekt på toppnivå
@msg page size {w} × {h} sidstorlek {w} × {h}
@msg page {0} does not exist (the PDF has {1}) sidan {0} finns inte (PDF-filen har {1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: sidan {n} finns inte (filen har 1)
@msg paste unavailable inklistring är inte tillgänglig
@msg path is already closed banan är redan sluten
@msg paths contain invalid coordinates banorna innehåller ogiltiga koordinater
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4078,6 +4078,7 @@ axis Both Обидві
@msg object {id} is not a selected top-level object об’єкт {id} не є виділеним об’єктом верхнього рівня
@msg page size {w} × {h} розмір сторінки {w} × {h}
@msg page {0} does not exist (the PDF has {1}) сторінка {0} не існує (в PDF{1})
@msg thumbnail: no page {n} (the file has 1) thumbnail: сторінка {n} не існує (у файлі 1)
@msg paste unavailable вставка недоступна
@msg path is already closed контур вже замкнутий
@msg paths contain invalid coordinates контури містять неприпустимі координати
Can't render this file because it contains an unexpected character in line 725 and column 40.
+1
View File
@@ -4362,6 +4362,7 @@ theme Appearance Giao diện
@msg Packaged {n} file(s){note} Đã đóng gói {n} tệp{note}
@msg page size {w} × {h} kích thước trang {w} × {h}
@msg page {0} does not exist (the PDF has {1}) trang {0} không tồn tại (PDF có {1} trang)
@msg thumbnail: no page {n} (the file has 1) thumbnail: trang {n} không tồn tại (tệp có 1 trang)
@msg panel group is empty nhóm bảng trống
@msg panel must name a registered panel panel phải đặt tên bảng đã đăng ký
@msg paste unavailable dán không khả dụng
Can't render this file because it contains an unexpected character in line 3948 and column 10.
+1
View File
@@ -3911,6 +3911,7 @@ imageTrace Outline 轮廓
@msg cannot read PDF: {0} 无法读取 PDF:{0}
@msg invalid PDF setting: {0} PDF 设置无效:{0}
@msg page {0} does not exist (the PDF has {1}) 页面 {0} 不存在 (该 PDF 有 {1} 页)
@msg thumbnail: no page {n} (the file has 1) thumbnail:页面 {n} 不存在 (该文件有 1 页)
@msg the PDF has no pages 该 PDF 没有页面
@msg the PDF is password-protected: give its password 该 PDF 受密码保护:请提供其密码
@msg the PDF password is wrong PDF 密码错误
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -640,7 +640,9 @@ Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.
`cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets;
with `trim` the bleed box round it becomes the document's bleed, and a PDF-compatible `.ai` or a PDF saved with its editing data opens on its trim boxes for
`crop`, its crop box taking the bleed in) and `password` for an encrypted file. `document.pdfInfo` reads a file without opening it: the page count, each page's
size and boxes, `needsPassword`, and with `thumbnail: n` a PNG of page n. A page whose `UserUnit` (PDF 1.6) is above 1
size and boxes, `needsPassword`, and with `thumbnail: n` a PNG of page n. It reads a PostScript file (an EPS, a `.ps`
file, a `.ai` in PostScript form) as one page the size of its bounding box (a US Letter page without one), with an
empty `boxes`, and `thumbnail: 1` shows what that page prints. A page whose `UserUnit` (PDF 1.6) is above 1
opens at its size in points, the file's numbers times the `UserUnit` (one above 75,000 counts as 75,000), and
`document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of
such a page opens from that data at the page's size.
+1
View File
@@ -712,6 +712,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests_links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part; CCITT Group 3 and planar files with extra channels are not read); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
- The Open and Place dialogs list PostScript `.ps` files and `.epsf` files. A `.ps` file opens and places through the EPS reader (its first page), also when its header is only `%!`, as the `ps` format, which exports don't write (Print to PostScript does); an `.epsf` file opens and places as EPS. Copied in a file manager, these files paste as EPS, and `clipboard.importEps` also reads PostScript whose header is only `%!` (`crates/engine/src/cmd/fileio/tests_epsimport.rs`).
`document.pdfInfo` reads an EPS, a `.ps` file or a `.ai` in PostScript form as one page the size of its bounding box (a US Letter page without one), and its `thumbnail: 1` is a PNG of what that page prints (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`).
- Targa files (`.tga`, `.vda`, `.icb`, `.vst`) open, place linked or embedded, relink and update at their pixel size: color-mapped, true-color and black-and-white, raw or run-length encoded, in any of the four row orders. The image descriptor's attribute bits and the extension area's Attributes Type decide the transparency: a file with no attribute bits, or with attribute data the Attributes Type calls undefined, opens opaque; with one bit, a 16-bit true-color pixel takes that bit as its alpha, and a color-mapped pixel takes the bit of the 16-bit color map entry it indexes; premultiplied colors are divided by their alpha. Open and Place recognize a Targa file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a Targa file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_tiffbmp.rs`, `crates/engine/src/tests_links.rs`).
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
- TIFFs that store each channel as its own plane (PlanarConfiguration 2) place, relink and open as the same image stored pixel by pixel does, 8 and 16 bits: grayscale with an alpha channel or other extra channels, and CMYK with or without them. A planar CMYK TIFF keeps its ink amounts and is stored as a CMYK TIFF with each pixel's inks together, at the file's resolution. A TIFF whose header claims more strips or tiles than can be counted is refused as too large to draw (#1466 in part; `crates/engine/src/cmd/fileio/tests.rs`).