diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 3120214b0..6d8ea9c1c 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -207,7 +207,7 @@ pub fn specs() -> Vec { "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 ), diff --git a/crates/engine/src/cmd/fileio/pdfimport.rs b/crates/engine/src/cmd/fileio/pdfimport.rs index ee9b68105..200cc5c44 100644 --- a/crates/engine/src/cmd/fileio/pdfimport.rs +++ b/crates/engine/src/cmd/fileio/pdfimport.rs @@ -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 { 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 { 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 { + 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 { + 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)) +} diff --git a/crates/engine/src/cmd/fileio/tests_pdfimport.rs b/crates/engine/src/cmd/fileio/tests_pdfimport.rs index 3d6293bb2..ac4e67673 100644 --- a/crates/engine/src/cmd/fileio/tests_pdfimport.rs +++ b/crates/engine/src/cmd/fileio/tests_pdfimport.rs @@ -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 { diff --git a/crates/eps/src/import/mod.rs b/crates/eps/src/import/mod.rs index 307a12256..1d300872f 100644 --- a/crates/eps/src/import/mod.rs +++ b/crates/eps/src/import/mod.rs @@ -186,13 +186,27 @@ pub fn import(bytes: &[u8]) -> Result { 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 { +/// 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 { + 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 { + 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 diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index 6328964c2..7e25903d3 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -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}; diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 04b2fbec8..bdc643e9f 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 6b5aa6291..293ced591 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8ed6f13a8..5b63f17ab 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -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 οι διαδρομές περιέχουν μη έγκυρες συντεταγμένες diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 23ca1e1f0..d9e324ade 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e8c505393..87c117893 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f012d8183..33b33a5d7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index afec09a3f..f120b374f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 068730fb3..a69f3c7b1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1408c146e..5ab639781 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 のパスワードが正しくありません diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 2270d6e77..16691bd1f 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -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 암호가 올바르지 않습니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 0643950d9..4148880a9 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 423403558..49aeb075e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ef5297d85..c19113163 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 контуры содержат недопустимые координаты diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index d2939baeb..64ec2e76c 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c6e109a5c..8a69dd734 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 контури містять неприпустимі координати diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index cd6550f5c..95950e470 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5be82a74b..3699a61fa 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 密码错误 diff --git a/docs/mcp.md b/docs/mcp.md index 4830fd506..67d83a67b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0b49059b7..ed4ee1d86 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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`).