From 100730d4d30eab084ada0ced4cd910bd82135cd4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 11:14:21 +0700 Subject: [PATCH] .ai import: a file whose PDF part can't be read opens from its editing data Squashed from #5051 onto main. --- crates/engine/src/cmd/fileio/load.rs | 37 +++- .../engine/src/cmd/fileio/tests_aiimport.rs | 161 ++++++++++++++++++ crates/engine/src/cmd/place.rs | 6 +- crates/engine/src/cmd/place/document.rs | 8 +- crates/engine/tests/import_fuzz.rs | 82 +++++++++ crates/eps/src/import.rs | 2 +- crates/eps/src/import/ai/read/types.rs | 2 +- crates/eps/src/import/native.rs | 2 +- crates/eps/src/import/native/build.rs | 21 +++ crates/eps/src/import/native/layers.rs | 12 +- crates/eps/src/lib.rs | 4 +- crates/pdf/src/lib.rs | 2 +- crates/pdf/src/pages.rs | 80 +++++++-- crates/pdf/src/tests/import_layers.rs | 32 ++++ crates/testkit/src/ai.rs | 37 ++-- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + docs/file-format-parity.md | 3 + docs/target-app-parity.md | 1 + 43 files changed, 476 insertions(+), 42 deletions(-) diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index 008313069..c0976f6f8 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -200,6 +200,20 @@ fn from_editing_data_alone(bytes: &[u8], opts: &LoadOptions) -> Option Option<(Document, Vec)> { + if opts.pages.is_some() || !opts.layers || !opts.editing_data { + return None; + } + let private = vectorcraft_pdf::illustrator_data_without_pdf_part(bytes, opts.password.as_deref())?; + let (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit).ok()?; + let notes = std::iter::once(format!("the PDF part couldn't be read ({e}): the file opened from its editing data")).chain(warnings).collect(); + Some(outlined_if_asked(doc, notes, opts)) +} + /// Is `w` a note on the PDF part as a page, which says nothing was skipped: that the art off the /// artboards is in the editing data alone, or that art past the page was clipped to it? fn is_page_note(w: &str) -> bool { @@ -255,22 +269,29 @@ fn every_page(bytes: &[u8], opts: &LoadOptions) -> bool { vectorcraft_pdf::info(bytes, opts.password.as_deref()).is_ok_and(|i| parse_range(range, i.pages.len()).is_ok_and(|p| p.len() == i.pages.len())) } -/// A page read from a PDF or `.ai` (`read`, as [`super::page_document`] reads it) → (document, -/// notes, false); for a `.ai` saved without PDF compatibility (its page only a placeholder), the -/// file `name` read as `document.open` reads it with `password`, from its editing copy alone → -/// (document, notes, true). +/// Page `page_index` (0-based) read from a PDF or `.ai` (`read`, as [`super::page_document`] reads +/// it) → (document, notes, false); for a `.ai` saved without PDF compatibility (its page only a +/// placeholder), the file `name` read as `document.open` reads it with `opts`' password, from its +/// editing copy alone → (document, notes, true). So too, read with `opts` but every page, for the +/// first page of a `.ai` or PDF whose PDF part can't be read and whose editing copy reads; the +/// error of any other page stays. pub(crate) fn page_or_editing_data( name: &str, bytes: &[u8], - password: Option<&str>, + opts: &LoadOptions, + page_index: usize, read: Result<(Document, Vec)>, ) -> Result<(Document, Vec, bool)> { match read { Ok((doc, notes)) => Ok((doc, notes, false)), Err(e) if is_placeholder(&e) => { - let l = load_with(name, bytes, &LoadOptions { password: password.map(str::to_string), ..Default::default() })?; + let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), ..Default::default() })?; Ok((l.doc, l.warnings, true)) } + Err(e) if page_index == 0 => match load_with(name, bytes, &LoadOptions { pages: None, ..opts.clone() }) { + Ok(l) if l.editing_data => Ok((l.doc, l.warnings, true)), + _ => Err(e), + }, Err(e) => Err(e), } } @@ -345,7 +366,9 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result .map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts)) .inspect(|_| editing_data = true) } - Err(e) => Err(e), + // A PDF part that can't be read (a stream the import refuses): the editing copy + // alone where it reads, else the error. + Err(e) => without_pdf_part(bytes, opts, &e).inspect(|_| editing_data = true).ok_or(e), })? } "dxf" => { diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 97980f7de..2d641b176 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -348,6 +348,167 @@ fn editing_data_that_cant_be_read_keeps_the_pdf_parts_notes() { assert_eq!(r["warnings"], json!([vectorcraft_pdf::PAST_PAGE_NOTE, unread]), "{r}"); } +/// Decode parameters the import refuses, for the page's content stream (#4366). +const REFUSED_CONTENT: &str = "/DecodeParms << /Predictor 12 /Columns 0 >>"; + +/// A `.ai` and a PDF saved with the editing data, carrying `private`, whose page's content stream +/// the import refuses. +fn refused_files(private: &[u8], page: &str) -> [(&'static str, Vec); 2] { + [("art.ai", ai::ai_with_content(private, page, REFUSED_CONTENT)), ("art.pdf", ai::pdf_with_content(private, page, REFUSED_CONTENT))] +} + +/// The note of a file opened from its editing data because its PDF part fails with `e`. +fn unread_pdf_part(e: &str) -> String { + format!("the PDF part couldn't be read ({e}): the file opened from its editing data") +} + +/// #4366: a `.ai`, or a PDF saved with the editing data, whose PDF part can't be read opens from +/// its editing data, with a first note saying why. Pages picked, layers off or the editing data +/// off keep the error, and so does editing data that can't be read. +#[test] +fn a_file_whose_pdf_part_cant_be_read_opens_from_its_editing_data() { + let mut s = Session::new(); + for (name, file) in refused_files(&compressed(&ai::sample_data()), ai::page_pdf()) { + let e = open(&mut s, name, &file, json!({"editingData": false})).unwrap_err().to_string(); + assert!(e.contains("a page's content in it (object 4) has decode parameters that can't be applied"), "{name}: {e}"); + let r = open(&mut s, name, &file, json!({})).unwrap(); + assert_eq!(r["format"], json!(&name[4..]), "{name}: {r}"); + assert_eq!(r["warnings"], json!([unread_pdf_part(&e)]), "{name}: {r}"); + check_sample(&s); + assert!(load(name, &file).unwrap().editing_data, "{name}"); + for p in [json!({"pages": "1"}), json!({"layers": false})] { + assert_eq!(open(&mut s, name, &file, p.clone()).unwrap_err().to_string(), e, "{name}: {p}"); + } + } + for (name, file) in refused_files(b"%AI12_CompressedData not zlib", ai::page_pdf()) { + let plain = open(&mut s, name, &file, json!({"editingData": false})).unwrap_err().to_string(); + assert_eq!(open(&mut s, name, &file, json!({})).unwrap_err().to_string(), plain, "{name}"); + } +} + +/// A file saved in units of 10 points (`/UserUnit 10`, as in +/// `an_ai_file_with_a_user_unit_opens_at_its_size_in_points`) whose PDF part can't be read opens +/// from its editing data at its size in points. +#[test] +fn a_file_whose_pdf_part_cant_be_read_opens_at_its_size_in_points() { + let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", &format!("0 1 1 0 1 0 0 Xa\n{}f\n", ai::rect(20.0, 20.0, 60.0, 50.0)))); + let file = ai::ai_with_entries_and_content(&compressed(data.as_bytes()), "1 0 0 rg 20 20 40 30 re f", "/UserUnit 10", REFUSED_CONTENT); + let near = |r: vectorcraft_geom::Rect, want: [f64; 4]| [r.x0, r.y0, r.x1, r.y1].iter().zip(want).all(|(a, b)| (a - b).abs() < 0.01); + let mut s = Session::new(); + let r = open(&mut s, "large.ai", &file, json!({})).unwrap(); + assert!(r["warnings"][0].as_str().unwrap().starts_with("the PDF part couldn't be read ("), "{r}"); + let doc = &s.doc().unwrap().doc; + assert!(near(doc.artboards[0].rect, [0.0, 0.0, 2000.0, 1000.0]), "{:?}", doc.artboards[0].rect); + let art = doc.art_bounds().unwrap(); + assert!(near(art, [200.0, 500.0, 600.0, 800.0]), "{art:?}"); + // On a page turned a quarter, the same. + let file = ai::ai_with_entries_and_content(&compressed(data.as_bytes()), "", "/UserUnit 10 /Rotate 90", REFUSED_CONTENT); + open(&mut s, "turned.ai", &file, json!({})).unwrap(); + assert!(near(s.doc().unwrap().doc.artboards[0].rect, [0.0, 0.0, 2000.0, 1000.0]), "{:?}", s.doc().unwrap().doc.artboards[0].rect); + // A page cropped to the art: its box has the art's size in those units. + let art = format!("0 1 1 0 1 0 0 Xa\n{}f\n", ai::rect(20.0, 20.0, 60.0, 50.0)); + let cropped = ai::editing_data_in([20.0, 20.0, 60.0, 50.0], [0.0, 0.0, 200.0, 100.0], &ai::layer("Art", &art)); + let file = ai::ai_with_entries_and_content(&compressed(cropped.as_bytes()), "", "/UserUnit 10 /TrimBox [20 20 60 50]", REFUSED_CONTENT); + open(&mut s, "cropped.ai", &file, json!({})).unwrap(); + let doc = &s.doc().unwrap().doc; + assert!(near(doc.artboards[0].rect, [0.0, 0.0, 2000.0, 1000.0]), "{:?}", doc.artboards[0].rect); + assert!(near(doc.art_bounds().unwrap(), [200.0, 500.0, 600.0, 800.0]), "{:?}", doc.art_bounds()); + // Saved in points, it opens as it is. + let file = ai::ai_with_entries_and_content(&compressed(data.as_bytes()), "", "", REFUSED_CONTENT); + open(&mut s, "points.ai", &file, json!({})).unwrap(); + assert!(near(s.doc().unwrap().doc.artboards[0].rect, [0.0, 0.0, 200.0, 100.0])); +} + +/// Type asked for as outlines in a file whose PDF part can't be read: the editing data's type, +/// outlined, as for a file saved without its PDF part. +#[test] +fn a_file_whose_pdf_part_cant_be_read_outlines_its_type_when_asked() { + let [(name, file), _] = refused_files(&typed_data(), ""); + let mut s = Session::new(); + let r = open(&mut s, name, &file, json!({})).unwrap(); + assert_eq!(texts_and_paths(&s).0, 1, "by default the editing data's type is type: {r}"); + let r = open(&mut s, name, &file, json!({"textAs": "outlines"})).unwrap(); + let (texts, paths) = texts_and_paths(&s); + assert_eq!(texts, 0, "no type left: {r}"); + assert!(paths > 0, "the outlines are there: {r}"); +} + +/// An image too large to read from the editing data of a file whose PDF part can't be read is +/// left out, and both notes say so. +#[test] +fn a_file_whose_pdf_part_cant_be_read_notes_an_image_left_out() { + let data = String::from_utf8_lossy(&ai::sample_data()).replace("] 0 0 2 1 2 1 8 3", "] 0 0 6000 6000 6000 6000 8 3"); + let [(name, file), _] = refused_files(&compressed(data.as_bytes()), ai::page_pdf()); + let mut s = Session::new(); + let e = open(&mut s, name, &file, json!({"editingData": false})).unwrap_err().to_string(); + let r = open(&mut s, name, &file, json!({})).unwrap(); + assert_eq!(r["warnings"], json!([unread_pdf_part(&e), "an image too large to read from the editing data was left out"]), "{r}"); +} + +/// A copied file whose PDF part can't be read pastes the objects of its editing data when asked +/// for them, with the note; without `editingData` its page can't be read. +#[test] +fn a_copied_file_whose_pdf_part_cant_be_read_pastes_from_its_editing_data() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + for (name, file) in refused_files(&compressed(&ai::sample_data()), ai::page_pdf()) { + let (r, pasted) = paste(&mut s, &file, true); + assert_eq!(r["count"], 7, "{name}: {r}"); + let pasted: Vec = pasted.iter().map(|n| n.name.clone().unwrap_or_default()).collect(); + assert_eq!(pasted, ["Pair", "Ring", "Window", "Shaded", "", "Sub", ""], "{name}"); + assert!(r["warnings"][0].as_str().unwrap().starts_with("the PDF part couldn't be read (cannot read PDF: "), "{name}: {r}"); + let p = json!({"dataBase64": vectorcraft_format::base64_encode(&file)}); + let e = s.execute("clipboard.importPdf", &p).unwrap_err().to_string(); + assert!(e.contains("decode parameters"), "{name}: {e}"); + } +} + +/// A file whose PDF part can't be read places its first artboard from its editing data, with the +/// note; any other page keeps the error. +#[test] +fn a_file_whose_pdf_part_cant_be_read_places_from_its_editing_data() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + for (name, file) in refused_files(&compressed(&ai::sample_data()), ai::page_pdf()) { + let data = vectorcraft_format::base64_encode(&file); + let r = s.execute("file.place", &json!({"name": name, "dataBase64": data, "link": false})).unwrap(); + assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{name}: {r}"); + assert!(r["warnings"][0].as_str().unwrap().starts_with("the PDF part couldn't be read ("), "{name}: {r}"); + let placed = s.doc().unwrap().doc.layers[0].children().unwrap().last().unwrap().clone(); + assert_eq!(names(&placed.children().unwrap()[1..]), ["Pair", "Ring", "Window"], "{name}: after the clipping path"); + for off in [json!({"editingData": false}), json!({"layers": false})] { + let mut p = json!({"name": name, "dataBase64": data, "link": false}); + p.as_object_mut().unwrap().extend(off.as_object().unwrap().clone()); + let e = s.execute("file.place", &p).unwrap_err().to_string(); + assert!(e.contains("decode parameters"), "{name}: {off}: {e}"); + } + } + let [(name, file), _] = refused_files(&typed_data(), ""); + let data = vectorcraft_format::base64_encode(&file); + let r = s.execute("file.place", &json!({"name": name, "dataBase64": data, "link": false, "textAs": "outlines", "crop": "bounding"})).unwrap(); + assert!(r["warnings"][0].as_str().unwrap().starts_with("the PDF part couldn't be read ("), "{r}"); + let placed = s.doc().unwrap().doc.layers[0].children().unwrap().last().unwrap().clone(); + let mut texts = 0; + placed.walk(&mut |n| texts += usize::from(matches!(n.kind, NodeKind::Text(_)))); + assert_eq!(texts, 0, "placed with its type outlined: {r}"); + // Two artboards: the second can't be placed. + let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\ + %_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\ + %_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\ + %_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n"; + let art = format!("0 g\n{}f\n{}f\n", ai::rect(10.0, 10.0, 50.0, 50.0), ai::rect(10.0, -140.0, 70.0, -100.0)); + let paged = ai::editing_data(100.0, 100.0, &(boards.to_string() + &ai::layer("Art", &art))); + let [(name, file), _] = refused_files(&compressed(paged.as_bytes()), ai::page_pdf()); + let data = vectorcraft_format::base64_encode(&file); + let size = |s: &mut Session, page: u64| { + let r = s.execute("file.place.info", &json!({"name": name, "dataBase64": data, "page": page, "crop": "bounding"})); + r.map(|r| (r["width"].as_f64(), r["height"].as_f64())).map_err(|e| e.to_string()) + }; + assert_eq!(size(&mut s, 1), Ok((Some(40.0), Some(40.0)))); + let second = size(&mut s, 2); + assert!(second.as_ref().is_err_and(|e| e.contains("decode parameters")), "{second:?}"); +} + /// A copied `.ai` whose editing data can't be read, or isn't there, pastes its page as a PDF /// does: the same objects as without `editingData`. #[test] diff --git a/crates/engine/src/cmd/place.rs b/crates/engine/src/cmd/place.rs index 9efb2cb3a..48d43d8eb 100644 --- a/crates/engine/src/cmd/place.rs +++ b/crates/engine/src/cmd/place.rs @@ -284,7 +284,8 @@ fn load(p: &Value, cmd: &str, board: Option, link_pdf: bool) -> Result bool { fileio::detect(name, bytes).is_some_and(|f| matches!(f.id, "pdf" | "ai" | "ait")) && !vectorcraft_pdf::is_postscript(bytes) } -/// The editing data of the `.ai` file `name` (`bytes`) when its PDF part is only a placeholder: +/// The editing data of the `.ai` file `name` (`bytes`) when its PDF part is only a placeholder, or +/// can't be read: /// the document its pages are read from ([`super::pdf_art`]), with the files it links. `None` for /// other files. pub(crate) fn editing_data(name: &str, bytes: &[u8]) -> Option { if !is_pdf(name, bytes) { return None; } - let read = fileio::page_document(bytes, 0, &fileio::LoadOptions::default()); - match fileio::page_or_editing_data(name, bytes, None, read) { + let opts = fileio::LoadOptions::default(); + let read = fileio::page_document(bytes, 0, &opts); + match fileio::page_or_editing_data(name, bytes, &opts, 0, read) { Ok((doc, _, true)) => Some(doc), _ => None, } diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index ea03a6bfc..37fb0d3f8 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -2678,6 +2678,39 @@ fn zstd(text: &str) -> Vec { ruzstd::encoding::compress_to_vec(text.as_bytes(), ruzstd::encoding::CompressionLevel::Fastest) } +/// A one-page `.ai` whose page's content stream has decode parameters the import refuses (#4366), +/// carrying `private` in one stream with the entries `filter`, under the key `{key}1` of its +/// `PieceInfo`; with an object stream of entries `object_stream` when they aren't empty. +fn refused_ai(private: &[u8], key: &str, filter: &str, object_stream: &str) -> Vec { + let stream = |dict: &str, data: &[u8]| [format!("<< {dict} /Length {} >>\nstream\n", data.len()).as_bytes(), data, b"\nendstream"].concat(); + let mut objects: Vec> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R /PieceInfo << /Illustrator << /Private << /{key}1 5 0 R /NumBlock 1 >> >> >> >>") + .into_bytes(), + stream("/DecodeParms << /Predictor 12 /Columns 0 >>", b"0 g 10 10 20 20 re f"), + stream(filter, private), + ]; + if !object_stream.is_empty() { + objects.push(stream(object_stream, b"8 0 << >>")); + } + let mut out = b"%PDF-1.5\n".to_vec(); + let mut offsets = vec![]; + for (i, o) in objects.iter().enumerate() { + offsets.push(out.len()); + out.extend(format!("{} 0 obj\n", i + 1).bytes()); + out.extend_from_slice(o); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + out.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).bytes()); + for off in offsets { + out.extend(format!("{off:010} 00000 n \n").bytes()); + } + out.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).bytes()); + out +} + /// An EPS drawing a square whose private data holds `editing`. fn editing_eps(editing: &str) -> Vec { let lines: Vec = ascii85(&zstd(editing)).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect(); @@ -3004,6 +3037,55 @@ proptest! { survive("mutated .ai editing data", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?; } + /// A `.ai` whose PDF part can't be read (#4366), its private data damaged or hostile, under + /// any key and filter: it opens from its editing data, or with the error of its PDF part. + #[test] + fn ai_whose_pdf_part_cant_be_read_never_panics( + cut in 0usize..3_000, + edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', ' ', '\n', '(', ')', '/', '%', 'u', 'U', 'L'])), 0..8), + junk in prop::collection::vec(any::(), 0..64), + key in prop::sample::select(vec!["AIPrivateData", "AIPDFPrivateData", "AIPrivateData2", "Other"]), + filter in prop::sample::select(vec!["", "/Filter /FlateDecode", "/Filter /Bogus", "/Filter [/FlateDecode /FlateDecode]", "/DP << /Predictor 2 >>", "/Filter 7"]), + ) { + let text = mutate_text(&editing_text(), cut, &edits); + let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&text), &junk].concat(); + let bytes = refused_ai(&private, key, filter, ""); + let plain = vectorcraft_engine::cmd::fileio::LoadOptions { editing_data: false, ..Default::default() }; + let page_error = vectorcraft_engine::cmd::fileio::load_with("x.ai", &bytes, &plain).err().map(|e| e.to_string()); + prop_assert!(page_error.is_some()); + let _ = catch_quiet(|| vectorcraft_pdf::illustrator_data_without_pdf_part(&bytes, None)).expect("the private data read panicked"); + let mut opened = None; + survive("a .ai whose PDF part can't be read", || { + let l = vectorcraft_engine::cmd::fileio::load("x.ai", &bytes); + opened = Some(l.as_ref().map(|l| l.editing_data).map_err(|e| e.to_string())); + l.ok().map(|l| l.doc) + })?; + match opened { + Some(Ok(editing)) => prop_assert!(editing), + Some(Err(e)) => prop_assert_eq!(Some(e), page_error), + None => {} + } + } + + /// The same file with a damaged table of objects and an object stream the reader would fail + /// on: refused, as before. + #[test] + fn ai_with_a_hostile_object_stream_is_refused(damage in prop::collection::vec((0usize..200, any::()), 0..6)) { + let private = [&b"%AI12_CompressedData"[..], &deflated(&vectorcraft_testkit::ai::sample_data())].concat(); + let mut bytes = refused_ai(&private, "AIPrivateData", "/Filter /FlateDecode", "/Type /ObjStm /N 1 /First 4 /DecodeParms << /Predictor 12 /Columns 0 >>"); + let table = bytes.windows(5).rposition(|w| w == b"xref\n").unwrap_or(0); + for (at, b) in damage { + let n = bytes.len() - table; + if let Some(x) = bytes.get_mut(table + at % n) { + *x = b; + } + } + let read = catch_quiet(|| vectorcraft_pdf::illustrator_data_without_pdf_part(&bytes, None)).expect("the private data read panicked"); + prop_assert!(read.is_none()); + let opened = catch_quiet(|| vectorcraft_engine::cmd::fileio::load("x.ai", &bytes).is_ok()).expect("the open panicked"); + prop_assert!(!opened); + } + /// A `.ai` whose variable library is damaged or hostile: the library is counted for the warning /// whether the layers are read or not (an inserted `Z` can make an operator the reader doesn't /// handle, and then the layers aren't read). diff --git a/crates/eps/src/import.rs b/crates/eps/src/import.rs index 5a7a9b491..5b50cbbcf 100644 --- a/crates/eps/src/import.rs +++ b/crates/eps/src/import.rs @@ -69,7 +69,7 @@ use graphics::{GState, Out}; use interp::{Fault, Interp}; use obj::PsError; -pub use native::{ai_alone, is_loss, layered_ai}; +pub use native::{ai_alone, ai_alone_on_page, is_loss, layered_ai}; pub use text::family_style; /// The page of a PostScript file without a bounding box (US Letter). diff --git a/crates/eps/src/import/ai/read/types.rs b/crates/eps/src/import/ai/read/types.rs index 784108fe9..47976a3b1 100644 --- a/crates/eps/src/import/ai/read/types.rs +++ b/crates/eps/src/import/ai/read/types.rs @@ -25,7 +25,7 @@ pub(super) const MAX_PIXELS: u64 = 1 << 25; pub(super) const MAX_PLACED_ELSEWHERE: usize = 4096; /// The note for an image past [`MAX_PIXELS`]: the file then opens from its PDF part, which reads -/// larger images (#1479). +/// larger images (#1479), unless that part can't be read either; then the image is left out. pub const IMAGE_TOO_LARGE: &str = "an image too large to read from the editing data was left out"; /// Most hex digits of an image's samples read from comment lines (CMYK and alpha at the largest size). diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index 13a746b78..128ce4cb8 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -51,7 +51,7 @@ use build::{Frame, MADE, prune_unseen}; use compare::{ MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, Shares, compare, compared, comparison_render, comparison_samples, differ, differ_in_colour, }; -pub use layers::{ai_alone, layered_ai}; +pub use layers::{ai_alone, ai_alone_on_page, layered_ai}; pub(super) use layers::{layered, program_alone}; pub use slots::is_loss; #[cfg(test)] diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index b4f8ee30c..819e8c674 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -53,6 +53,27 @@ pub(super) fn build( build_from(ai::read(data, matches!(page, Page::Art))?, data, visible, page, outlined, trust, unit) } +/// [`build`] without a page, for a `.ai` whose PDF part can't be read, its first page `page` +/// (width and height, either way round) points large in units of `unit` points: the document is +/// drawn `unit` times as large when its first artboard, or its art's box, has the page's size in +/// those units and not in points, as [`build_from`] decides with a page. +pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64) -> Result<(Document, Vec), String> { + let structure = ai::read(data, false)?; + let board = structure.artboard.map(|a| structure.to_doc.transform_rect_bbox(a)); + let art = structure.bbox.map(|[a, b, c, d]| structure.to_doc.transform_rect_bbox(Rect::new(a, b, c, d))); + let (w, h) = page; + let fits = |r: Option, k: f64| { + r.is_some_and(|r| [(w, h), (h, w)].iter().any(|(a, b)| (r.width() - a / k).abs() <= 0.5 && (r.height() - b / k).abs() <= 0.5)) + }; + let in_units = |r: Option| unit.is_finite() && unit > 1.0 && fits(r, unit) && !fits(r, 1.0); + let scale = in_units(board) || (!fits(board, 1.0) && in_units(art)); + let (mut doc, notes) = build_from(structure, data, None, Page::Artboard, false, false, 1.0)?; + if scale { + scale_document(&mut doc, unit); + } + Ok((doc, notes)) +} + /// [`build`] from `structure`, the editing copy `data` already read. pub(super) fn build_from( structure: Structure, diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index c4e961c47..0c26081bf 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -1,4 +1,4 @@ -use super::build::{PAGE_MOVE, Page, build, build_from}; +use super::build::{PAGE_MOVE, Page, build, build_from, build_on_page}; use super::slots::LAYERS_UNREAD; use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai}; @@ -216,6 +216,16 @@ pub fn ai_alone(private: &[u8]) -> Result<(Document, Vec), String> { build(&data, None, Page::Artboard, false, false, 1.0) } +/// [`ai_alone`] for a `.ai` whose PDF part can't be read, its first page `page` (width and height) +/// points large in units of `unit` points (its `UserUnit`): editing data whose first artboard, or +/// whose art's box, has the page's size in those units, and not in points, comes in `unit` times as +/// large, as [`layered_ai`] reads it. A page turned a quarter (`/Rotate 90`) is measured either +/// way round. +pub fn ai_alone_on_page(private: &[u8], page: (f64, f64), unit: f64) -> Result<(Document, Vec), String> { + let data = ai::decode_private(private)?; + build_on_page(&data, page, unit) +} + /// Does the editing data (or a program in the legacy format) have layers? fn has_layers(data: &[u8]) -> bool { data.windows(15).any(|w| w == b"%AI5_BeginLayer") diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index 2b828e093..cbeed588e 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -37,7 +37,9 @@ mod ps; mod scene; mod tiff; -pub use import::{Imported, POSTSCRIPT_ERRORS, ai_alone, family_style, import, import_trusting, import_with, is_loss, layered_ai, page}; +pub use import::{ + Imported, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, family_style, import, import_trusting, 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/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 95a0260c6..8a8bd10e0 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -78,7 +78,7 @@ pub use import::{ INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines, import, import_with_report, is_glyph_outlines, is_outlines_note, }; -pub use pages::{PageInfo, PdfInfo, has_illustrator_data, illustrator_data, info, is_postscript}; +pub use pages::{IllustratorData, PageInfo, PdfInfo, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info, is_postscript}; pub use post::{THUMBNAIL_SIZE, Thumbnail}; pub use presets::*; pub use print::*; diff --git a/crates/pdf/src/pages.rs b/crates/pdf/src/pages.rs index 6a72bc51f..1468d6d8d 100644 --- a/crates/pdf/src/pages.rs +++ b/crates/pdf/src/pages.rs @@ -241,6 +241,18 @@ pub(crate) fn open_options() -> craft_pdf::OpenOptions { } pub(crate) fn open_with_options(bytes: &[u8], password: Option<&str>, options: &craft_pdf::OpenOptions) -> Result { + // Twice: looking at every object makes the reader repair a damaged table of objects (once + // only), and the objects are other ones after that. + open_structural_with_options(bytes, password, options).and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) { + Some(why) => Err(PdfError::Parse(why)), + None => Ok(pdf), + }) +} + +/// [`open_with_options`] without looking at the file's objects ([`hostile`]): the reader builds its +/// table of objects and its page tree, and nothing on a page is read. For reading one object that +/// is checked on its own ([`illustrator_data_without_pdf_part`]), never for importing a page. +fn open_structural_with_options(bytes: &[u8], password: Option<&str>, options: &craft_pdf::OpenOptions) -> Result { if is_postscript(bytes) { return Err(PdfError::PostScript); } @@ -265,12 +277,6 @@ pub(crate) fn open_with_options(bytes: &[u8], password: Option<&str>, options: & craft_pdf::OpenError::Load(LoadPdfError::Decryption(DecryptionError::PasswordProtected)) => PdfError::WrongPassword, e => PdfError::Parse(e.to_string()), }) - // Twice: looking at every object makes the reader repair a damaged table of objects (once - // only), and the objects are other ones after that. - .and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) { - Some(why) => Err(PdfError::Parse(why)), - None => Ok(pdf), - }) } /// The pages and their boxes. An encrypted PDF needs its `password` @@ -367,7 +373,37 @@ const MAX_PRIVATE: usize = 256 << 20; /// `AIPrivateData1`. `None` for a PDF without either (or with a stream that can't be read). /// What it holds is the editor's own copy of the art (see `vectorcraft_eps::layered_ai`). pub fn illustrator_data(bytes: &[u8], password: Option<&str>) -> Option> { - let pdf = open(bytes, password).ok()?; + private_data(&open(bytes, password).ok()?) +} + +/// The private data of a file whose PDF part can't be read ([`illustrator_data_without_pdf_part`]), +/// with the size of its first page. +#[derive(Clone, Debug, PartialEq)] +pub struct IllustratorData { + /// The private data, as [`illustrator_data`] reads it. + pub data: Vec, + /// The size in points of a unit of the first page's user space (its `UserUnit`). + pub user_unit: f64, + /// The first page's trim box (its crop box when it has none) in points, rotated as the page + /// shows: width and height. + pub trim: (f64, f64), +} + +/// [`illustrator_data`] for a file whose PDF part can't be read (a stream the import refuses, or +/// objects built on one another without end): the file is opened without looking at its other +/// objects, and only the private data streams are read, with the same limits. `None` when the +/// file itself can't be opened, or as for [`illustrator_data`]. +pub fn illustrator_data_without_pdf_part(bytes: &[u8], password: Option<&str>) -> Option { + let pdf = open_structural_with_options(bytes, password, &open_options()).ok()?; + let data = private_data(&pdf)?; + let page = pdf.pages().first()?; + // (The page transform is in points: it applies the `UserUnit`.) + let trim = frame(page, CropTo::Trim).1; + Some(IllustratorData { data, user_unit: f64::from(page.user_unit()), trim: (trim.width(), trim.height()) }) +} + +/// The private data of `pdf`'s first page, as [`illustrator_data`] reads it. +fn private_data(pdf: &Pdf) -> Option> { let page = pdf.pages().first()?; let private = page.raw().get::>(b"PieceInfo")?.get::>(PRIVATE_DATA_OWNER)?.get::>(b"Private")?; let key = if private.contains_key(b"AIPrivateData1") { "AIPrivateData" } else { "AIPDFPrivateData" }; @@ -388,19 +424,37 @@ pub fn has_illustrator_data(bytes: &[u8], password: Option<&str>) -> bool { /// `stream` decoded, up to `most` bytes: the decoder is run here rather than by the PDF reader's /// `decoded()`, which has no limit, so a stream of zeros can't grow to the size of the memory. Only a -/// stream that is plain or only deflated is read (the private data is no other); else, or when it -/// is larger once decoded, `None`. +/// stream that is plain or only deflated is read (the private data is no other), without decode +/// parameters; else, or when it is larger once decoded, `None`. A filter the reader doesn't know +/// counts as another filter. fn inflated(stream: &Stream<'_>, most: usize) -> Option> { use std::io::Read; - if stream.dict().get::>(b"DecodeParms").is_some() { + let dict = stream.dict(); + let named = |key: &[u8]| match dict.get::>(key) { + None | Some(Object::Null(_)) => Some(0), + Some(Object::Name(_)) => Some(1), + Some(Object::Array(a)) => Some(items(&a).count()), + Some(_) => None, + }; + let params = |key: &[u8]| match dict.get::>(key) { + None | Some(Object::Null(_)) => false, + Some(Object::Array(a)) => items(&a).any(|m| !matches!(m, MaybeRef::NotRef(Object::Null(_)))), + Some(_) => true, + }; + if params(b"DecodeParms") || params(b"DP") { + return None; + } + let filters = stream.filters(); + // (The reader leaves out the filters it doesn't know.) + if named(b"Filter")?.max(named(b"F")?) != filters.len() { return None; } let raw = stream.raw_data(); let mut out = vec![]; let limit = u64::try_from(most).ok()?.checked_add(1)?; - match stream.filters().len() { - 0 => out.extend_from_slice(&raw), - 1 => { + match filters.as_slice() { + [] if raw.len() <= most => out.extend_from_slice(&raw), + [craft_pdf::syntax::Filter::FlateDecode] => { flate2::read::ZlibDecoder::new(&*raw).take(limit).read_to_end(&mut out).ok()?; } _ => return None, diff --git a/crates/pdf/src/tests/import_layers.rs b/crates/pdf/src/tests/import_layers.rs index a30f6ba1c..8ddeb2f83 100644 --- a/crates/pdf/src/tests/import_layers.rs +++ b/crates/pdf/src/tests/import_layers.rs @@ -416,3 +416,35 @@ fn a_pdfs_private_data_is_its_pdf_streams_joined() { let both = format!("/AIPDFPrivateData1 {a} 0 R /AIPDFPrivateData2 {b} 0 R /AIPrivateData1 {c} 0 R"); assert_eq!(illustrator_data(&pdf_with_catalog(&[page(both)], &[&one, &two, &other], "", None), None).unwrap(), b"%AI24_ZStandard_Data other"); } + +/// The private data of a file whose other objects the import refuses (#4366) is read without its +/// PDF part, and only then; a private data stream under a filter the reader doesn't know, or with +/// decode parameters, is read neither way. +#[test] +fn private_data_reads_without_a_pdf_part_that_cant_be_read() { + let (a, b) = (first_extra(1), first_extra(1) + 1); + let page = PdfPage { + entries: format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {a} 0 R /AIPrivateData2 {b} 0 R /NumBlock 2 >> >> >>"), + ..PdfPage::new(100.0, 100.0, "1 0 0 rg 10 10 30 30 re f") + }; + let (one, two) = (stream("", "%AI24_ZStandard_Data first "), stream("", "second")); + let refused = stream("/DecodeParms << /Predictor 12 /Columns 0 >>", "x"); + let pdf = pdf_with_catalog(std::slice::from_ref(&page), &[&one, &two, &refused], "", None); + assert!(illustrator_data(&pdf, None).is_none()); + let read = illustrator_data_without_pdf_part(&pdf, None).unwrap(); + assert_eq!((read.data.as_slice(), read.user_unit, read.trim), (&b"%AI24_ZStandard_Data first second"[..], 1.0, (100.0, 100.0))); + let whole = pdf_with_catalog(std::slice::from_ref(&page), &[&one, &two], "", None); + assert_eq!(illustrator_data_without_pdf_part(&whole, None).map(|p| p.data), illustrator_data(&whole, None)); + // The trim box in points, as the page shows it. + let large = PdfPage { entries: format!("{} /UserUnit 10 /TrimBox [10 10 60 30] /Rotate 90", page.entries), ..page.clone() }; + let read = illustrator_data_without_pdf_part(&pdf_with_catalog(&[large], &[&one, &two, &refused], "", None), None).unwrap(); + assert_eq!((read.user_unit, read.trim), (10.0, (200.0, 500.0))); + assert!(illustrator_data_without_pdf_part(b"not a pdf", None).is_none()); + for odd in ["/Filter /Bogus", "/Filter [/Bogus]", "/DP << /Predictor 2 >>", "/Filter /FlateDecode /DecodeParms [<< /Predictor 2 >>]", "/Filter 5"] + { + let first = stream(odd, "%AI24_ZStandard_Data first "); + let pdf = pdf_with_catalog(std::slice::from_ref(&page), &[&first, &two], "", None); + assert!(illustrator_data(&pdf, None).is_none(), "{odd}"); + assert!(illustrator_data_without_pdf_part(&pdf, None).is_none(), "{odd}"); + } +} diff --git a/crates/testkit/src/ai.rs b/crates/testkit/src/ai.rs index d94423ef6..6cb7de2ed 100644 --- a/crates/testkit/src/ai.rs +++ b/crates/testkit/src/ai.rs @@ -190,7 +190,19 @@ pub fn ai(private: &[u8], page: &str) -> Vec { /// [`ai`] with more entries in the page dictionary (`/ArtBox [0 0 200 100]`). pub fn ai_with(private: &[u8], page: &str, entries: &str) -> Vec { - with_private(private, page, "AIPrivateData", entries) + with_private(private, page, "AIPrivateData", entries, "") +} + +/// [`ai`] with more entries in the dictionary of the page's content stream +/// (`/DecodeParms << /Predictor 12 /Columns 0 >>`). +pub fn ai_with_content(private: &[u8], page: &str, content: &str) -> Vec { + with_private(private, page, "AIPrivateData", "", content) +} + +/// [`ai`] with more `entries` in the page dictionary, as [`ai_with`], and more in its content +/// stream's dictionary, as [`ai_with_content`]. +pub fn ai_with_entries_and_content(private: &[u8], page: &str, entries: &str, content: &str) -> Vec { + with_private(private, page, "AIPrivateData", entries, content) } /// A PDF saved with Illustrator's editing data: [`ai`]'s file with the two streams under @@ -198,15 +210,20 @@ pub fn ai_with(private: &[u8], page: &str, entries: &str) -> Vec { /// #868, and nothing else of that file is used; ExifTool's PDF tag table lists them too /// (). pub fn pdf(private: &[u8], page: &str) -> Vec { - with_private(private, page, "AIPDFPrivateData", "") + with_private(private, page, "AIPDFPrivateData", "", "") } -/// The file of [`ai`] and [`pdf`], with `private` in the streams `{key}1` and `{key}2` and -/// `entries` more in the page dictionary. -fn with_private(private: &[u8], page: &str, key: &str, entries: &str) -> Vec { +/// [`pdf`] with more entries in the dictionary of the page's content stream, as [`ai_with_content`]. +pub fn pdf_with_content(private: &[u8], page: &str, content: &str) -> Vec { + with_private(private, page, "AIPDFPrivateData", "", content) +} + +/// The file of [`ai`] and [`pdf`], with `private` in the streams `{key}1` and `{key}2`, `entries` +/// more in the page dictionary and `content` more in its content stream's dictionary. +fn with_private(private: &[u8], page: &str, key: &str, entries: &str, content: &str) -> Vec { let (first, second) = private.split_at(private.len() / 2); - let stream = |part: &[u8]| { - let mut s = format!("<< /Length {} >>\nstream\n", part.len()).into_bytes(); + let stream = |part: &[u8], more: &str| { + let mut s = format!("<< /Length {} {more}>>\nstream\n", part.len()).into_bytes(); s.extend_from_slice(part); s.extend_from_slice(b"\nendstream"); s @@ -220,9 +237,9 @@ fn with_private(private: &[u8], page: &str, key: &str, entries: &str) -> Vec /PieceInfo << /{PIECE} << /Private << /{key}1 5 0 R /{key}2 6 0 R /NumBlock 2 >> >> >> >>" ) .into_bytes(), - stream(page.as_bytes()), - stream(first), - stream(second), + stream(page.as_bytes(), content), + stream(first, ""), + stream(second, ""), ]; let mut out = b"%PDF-1.5\n".to_vec(); let mut offsets = vec![]; diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 5ba0f57d1..ac7d2596a 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4439,6 +4439,7 @@ axis Both И двете @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise текстовите рамки за област в редове, твърде къси за шрифта, в който се показват, получиха по-високи рамки ({_1}): текстът им иначе нямаше да се види @msg its editing data doesn't say where its art is данните му за редактиране не посочват къде се намира неговата графика @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) този файл е записан без съвместимост с PDF и данните му за редактиране не могат да бъдат прочетени ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF частта не можа да бъде прочетена ({_1}): файлът беше отворен от данните си за редактиране @msg No fonts are missing in this document В този документ няма липсващи шрифтове @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft не търси в папките на други приложения или на системата: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft не търси в папките Shared или Public в Users: {path} diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 39b283fd8..e19a780ee 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4841,6 +4841,7 @@ imageTrace Outline Obris @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise tekst u području u okvirima prekratkim za red fonta u kojem se prikazuje dobio je više okvire ({_1}): inače se tekst ne bi prikazao @msg its editing data doesn't say where its art is njeni podaci za uređivanje ne govore gdje joj je crtež @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ova datoteka je sačuvana bez PDF kompatibilnosti, a njeni podaci za uređivanje se ne mogu pročitati ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF dio se nije mogao pročitati ({_1}): datoteka je otvorena iz svojih podataka za uređivanje @msg No fonts are missing in this document U ovom dokumentu ne nedostaje nijedan font @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft ne pretražuje foldere drugih aplikacija ni sistemske foldere: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft ne pretražuje folder Shared ili Public u Users: {path} diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 588902191..91834ada9 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5531,6 +5531,7 @@ digits Malayalam Malaiàlam @msg there are no printer settings to open here aquí no hi ha cap configuració d’impressora per obrir @msg thickness must be from 0.01 to 5000 pt el gruix ha de ser d’entre 0,01 i 5000 pt @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) aquest fitxer s’ha desat sense compatibilitat amb PDF i les seves dades d’edició no es poden llegir ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data no s’ha pogut llegir la part PDF ({_1}): el fitxer s’ha obert a partir de les seves dades d’edició @msg this file's PostScript couldn't be read ({why}): {what} no s’ha pogut llegir el PostScript d’aquest fitxer ({why}): {what} @msg this format holds one artboard: export several as PDF or with document.exportForScreens aquest format admet un sol tauler: exporta’n diversos com a PDF o amb document.exportForScreens @msg this is a DWG drawing, which can't be read: save it from the CAD app as an ASCII DXF and open that aquest és un dibuix DWG, que no es pot llegir: desa’l des de l’aplicació CAD com a DXF ASCII i obre’l diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index b7a729b59..64f0f9e55 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4885,6 +4885,7 @@ imageTrace Outline Obrys @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise text v oblasti v rámcích příliš nízkých pro řádek písma, ve kterém se zde zobrazuje, dostal vyšší rámce ({_1}): jinak by nebyl vidět @msg its editing data doesn't say where its art is z jeho dat pro úpravy není zřejmé, kde je kresba @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) tento soubor byl uložen bez kompatibility s PDF a jeho data pro úpravy nelze přečíst ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data část PDF nelze přečíst ({_1}): soubor byl otevřen z jeho dat pro úpravy @msg No fonts are missing in this document V tomto dokumentu nechybí žádná písma @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft neprohledává složky jiných aplikací ani systému: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft neprohledává složku Shared ani Public ve složce Users: {path} diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index c89609498..eacf7491a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4530,6 +4530,7 @@ imageTrace Outline Kontur @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise rammetekst i rammer, der er for lave til en tekstlinje i den skrifttype, den viser i, fik højere rammer ({_1}): teksten ville ellers ikke blive vist @msg its editing data doesn't say where its art is dens redigeringsdata siger ikke, hvor dens grafik er @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) denne fil blev gemt uden PDF-kompatibilitet, og dens redigeringsdata kan ikke læses ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF-delen kunne ikke læses ({_1}): filen blev åbnet fra dens redigeringsdata @msg No fonts are missing in this document Ingen skrifttyper mangler i dette dokument @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft gennemsøger ikke andre programmers eller systemets mapper: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft gennemsøger ikke mappen Shared eller Public i Users: {path} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 3441ef8a4..b4bdbc071 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4494,6 +4494,7 @@ theme Appearance Erscheinungsbild @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise Flächentext in Rahmen, die für eine Zeile der Schrift, in der er hier erscheint, zu niedrig sind, hat höhere Rahmen bekommen ({_1}): Sonst wäre der Text nicht zu sehen @msg its editing data doesn't say where its art is seine Bearbeitungsdaten geben nicht an, wo seine Grafik liegt @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) diese Datei wurde ohne PDF-Kompatibilität gespeichert, und ihre Bearbeitungsdaten können nicht gelesen werden ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data der PDF-Teil konnte nicht gelesen werden ({_1}): die Datei wurde aus ihren Bearbeitungsdaten geöffnet @msg No fonts are missing in this document In diesem Dokument fehlen keine Schriften @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft durchsucht keine Ordner anderer Apps oder des Systems: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft durchsucht nicht den Ordner „Shared“ oder „Public“ unter „Users“: {path} diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 1fd604e40..c0baa2bb3 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4527,6 +4527,7 @@ digits Malayalam Μαλαγιάλαμ @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise οι περιοχές κειμένου με πλαίσια πολύ μικρά για μία γραμμή της γραμματοσειράς τους απέκτησαν μεγαλύτερο ύψος ({_1}), διαφορετικά το κείμενο δεν θα εμφανιζόταν @msg its editing data doesn't say where its art is τα δεδομένα επεξεργασίας του δεν προσδιορίζουν τη θέση του σχεδίου @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) το αρχείο αποθηκεύτηκε χωρίς συμβατότητα PDF και τα δεδομένα επεξεργασίας του δεν μπορούν να διαβαστούν ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data το τμήμα PDF δεν μπόρεσε να διαβαστεί ({_1}): το αρχείο άνοιξε από τα δεδομένα επεξεργασίας του @msg No fonts are missing in this document Δεν λείπουν γραμματοσειρές από αυτό το έγγραφο @msg VectorCraft doesn't search other apps' or the system's folders: {path} Το VectorCraft δεν αναζητά στους φακέλους άλλων εφαρμογών ή του συστήματος: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} Το VectorCraft δεν αναζητά στους φακέλους Shared ή Public των Χρηστών: {path} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f326f1faa..72f3a297e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4579,6 +4579,7 @@ axis Both Ambos @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise el texto de área en marcos demasiado bajos para una línea de la fuente en que se muestra recibió marcos más altos ({_1}): de otro modo no se vería @msg its editing data doesn't say where its art is sus datos de edición no dicen dónde está su ilustración @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) este archivo se guardó sin compatibilidad con PDF y sus datos de edición no se pueden leer ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data no se pudo leer la parte PDF ({_1}): el archivo se abrió desde sus datos de edición @msg No fonts are missing in this document No faltan fuentes en este documento @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft no busca en carpetas de otras aplicaciones ni del sistema: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft no busca en las carpetas Shared ni Public de Users: {path} diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index c7ad2d820..9ef4e7186 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4787,6 +4787,7 @@ imageTrace Outline Ääriviiva @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise aluetekstin kehykset, jotka olivat liian matalia yhdelle riville niiden fontilla, korotettiin ({_1}): muuten niiden teksti ei näkyisi @msg its editing data doesn't say where its art is sen muokkaustiedoista ei selviä kuvituksen sijainti @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) tämä tiedosto tallennettiin ilman PDF-yhteensopivuutta, eikä sen muokkaustietoja voi lukea ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF-osaa ei voitu lukea ({_1}): tiedosto avattiin sen muokkaustiedoista @msg No fonts are missing in this document Tästä asiakirjasta ei puutu fontteja @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft ei hae muiden sovellusten tai järjestelmän kansioista: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft ei hae Users-kansion Shared- tai Public-kansiosta: {path} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 7b78d2ed4..29045e538 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4569,6 +4569,7 @@ axis Both Les deux @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise le texte captif dans des cadres trop bas pour une ligne de la police dans laquelle il s’affiche a reçu des cadres plus hauts ({_1}) : il ne s’afficherait pas autrement @msg its editing data doesn't say where its art is ses données d’édition ne disent pas où se trouve son illustration @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ce fichier a été enregistré sans compatibilité PDF et ses données d’édition sont illisibles ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data la partie PDF n’a pas pu être lue ({_1}) : le fichier a été ouvert depuis ses données de modification @msg No fonts are missing in this document Aucune police ne manque dans ce document @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft ne recherche pas dans les dossiers d’autres apps ni du système : {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft ne recherche pas dans les dossiers Shared et Public de Users : {path} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f78bcb86f..bdd54e2b8 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5199,6 +5199,7 @@ imageTrace Outline Körvonal @msg there are no objects to release nincs felszabadítható objektum @msg there are no printer settings to open here itt nincsenek megnyitható nyomtatóbeállítások @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ezt a fájlt PDF-kompatibilitás nélkül mentették, a szerkesztési adatai nem olvashatók ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data a PDF-rész nem olvasható ({_1}): a fájl a szerkesztési adataiból nyílt meg @msg this file's PostScript couldn't be read ({why}): {what} a fájl PostScript-tartalma nem olvasható ({why}): {what} @msg this format holds one artboard: export several as PDF or with document.exportForScreens ez a formátum egy rajztáblát tárol: többet PDF-ként vagy a document.exportForScreens paranccsal exportálj @msg this is a DWG drawing, which can't be read: save it from the CAD app as an ASCII DXF and open that ez DWG-rajz, amely nem olvasható: mentsd ASCII DXF-ként a CAD-alkalmazásból, és azt nyisd meg diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index f44967bbb..577a5e400 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4530,6 +4530,7 @@ imageTrace Outline Kontur @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise teks area dalam bingkai yang terlalu pendek untuk satu baris font yang ditampilkannya mendapat bingkai yang lebih tinggi ({_1}): tanpa itu teksnya tidak akan tampil @msg its editing data doesn't say where its art is data pengeditannya tidak menyatakan di mana karya seninya berada @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) berkas ini disimpan tanpa kompatibilitas PDF, dan data pengeditannya tidak dapat dibaca ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data bagian PDF tidak dapat dibaca ({_1}): file dibuka dari data pengeditannya @msg No fonts are missing in this document Tidak ada font yang hilang dalam dokumen ini @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft tidak mencari di folder aplikasi lain atau folder sistem: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft tidak mencari di folder Shared atau Public di Users: {path} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 99043800a..7dd14ab66 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4620,6 +4620,7 @@ axis Both Entrambi @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise il testo in area in cornici troppo basse per una riga del font in cui viene mostrato ha ricevuto cornici più alte ({_1}): altrimenti non sarebbe visibile @msg its editing data doesn't say where its art is i suoi dati di modifica non dicono dove si trova la grafica @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) questo file è stato salvato senza compatibilità PDF e i suoi dati di modifica non si possono leggere ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data non è stato possibile leggere la parte PDF ({_1}): il file è stato aperto dai suoi dati di modifica @msg No fonts are missing in this document In questo documento non mancano font @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft non cerca nelle cartelle di altre app né di sistema: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft non cerca nelle cartelle Shared e Public di Users: {path} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 99399c46c..4ea777bda 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4102,6 +4102,7 @@ swatch Bright Violet ビビッドバイオレット @msg the document has no artboard ドキュメントにアートボードがありません @msg this format holds one artboard: export several as PDF or with document.exportForScreens この形式で書き出せるアートボードは 1 つだけです: 複数のアートボードは PDF として書き出すか、document.exportForScreens を使用してください @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) このファイルは PDF 互換性なしで保存されており、編集データを読み込めません ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF 部分を読み込めませんでした ({_1})。編集データからファイルを開きました @msg the export wrote no file 書き出しでファイルが作成されませんでした @msg {_1} files can be opened but not written (see document.formats) {_1} ファイルは開けますが、保存や書き出しはできません (document.formats を参照してください) @msg {_1} files are written by file.saveAsTemplate {_1} ファイルは file.saveAsTemplate で保存します diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 7c57960d7..1e9a4ba4c 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4317,6 +4317,7 @@ font-technology Variable ცვლადი @msg an image whose data is cut short was left out გამოსახულება, რომლის მონაცემებიც შეკვეცილია, გამოტოვებულია @msg its editing data doesn't say where its art is მისი რედაქტირების მონაცემები არ მიუთითებს, სად არის მისი ნახატი @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ეს ფაილი PDF-თავსებადობის გარეშეა შენახული და მისი რედაქტირების მონაცემების წაკითხვა ვერ ხერხდება ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF ნაწილის წაკითხვა ვერ მოხერხდა ({_1}): ფაილი რედაქტირების მონაცემებიდან გაიხსნა @msg No fonts are missing in this document ამ დოკუმენტში შრიფტები არ აკლია @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft არ ეძებს სხვა აპების ან სისტემის საქაღალდეებში: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft არ ეძებს Users-ის Shared ან Public საქაღალდეში: {path} diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ce5485246..8802c7ebc 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4029,6 +4029,7 @@ theme Appearance 외관 @msg the document has no artboard 문서에 아트보드가 없습니다 @msg this format holds one artboard: export several as PDF or with document.exportForScreens 이 형식은 아트보드를 하나만 담을 수 있습니다. 여러 개는 PDF로 내보내거나 document.exportForScreens를 사용하세요 @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) 이 파일은 PDF 호환성 없이 저장되어 편집 데이터를 읽을 수 없습니다({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF 부분을 읽지 못했습니다({_1}). 편집 데이터에서 파일을 열었습니다 @msg the export wrote no file 내보내기에서 파일이 만들어지지 않았습니다 @msg {_1} files can be opened but not written (see document.formats) {_1} 파일은 열 수 있지만 쓸 수 없습니다(document.formats 참조) @msg {_1} files are written by file.saveAsTemplate {_1} 파일은 file.saveAsTemplate로 기록합니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index bb9497cf9..2658dde87 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4542,6 +4542,7 @@ imageTrace Outline Contour @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise vlaktekst in kaders die te kort zijn voor één regel van het lettertype waarin de tekst wordt weergegeven, heeft hogere kaders gekregen ({_1}): anders zou de tekst niet zichtbaar zijn @msg its editing data doesn't say where its art is zijn bewerkingsgegevens zeggen niet waar de illustratie zich bevindt @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) dit bestand is opgeslagen zonder PDF-compatibiliteit en de bewerkingsgegevens kunnen niet worden gelezen ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data het PDF-gedeelte kon niet worden gelezen ({_1}): het bestand is geopend vanuit de bewerkingsgegevens @msg No fonts are missing in this document Er ontbreken geen lettertypen in dit document @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft doorzoekt geen mappen van andere apps of van het systeem: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft doorzoekt de map Gedeeld of Openbaar onder Gebruikers niet: {path} diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index e41b3d222..861babdad 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4711,6 +4711,7 @@ imageTrace Outline Kontur @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise tekst akapitowy w ramkach niższych niż wiersz wyświetlanej czcionki otrzymał wyższe ramki ({_1}): inaczej tekst nie byłby widoczny @msg its editing data doesn't say where its art is jego dane edycyjne nie określają położenia kompozycji @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) ten plik zapisano bez zgodności z PDF, a jego danych edycyjnych nie można odczytać ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data nie udało się odczytać części PDF ({_1}): plik otwarto z jego danych edycyjnych @msg No fonts are missing in this document W tym dokumencie nie brakuje czcionek @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft nie przeszukuje folderów innych aplikacji ani systemowych: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft nie przeszukuje folderu Shared ani Public w Users: {path} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 132a77018..114062750 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4163,6 +4163,7 @@ axis Both Ambos @msg an image whose data is cut short was left out uma imagem com dados incompletos foi omitida @msg its editing data doesn't say where its art is seus dados de edição não indicam onde está a arte @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) este arquivo foi salvo sem compatibilidade com PDF e seus dados de edição não podem ser lidos ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data não foi possível ler a parte PDF ({_1}): o arquivo foi aberto a partir dos seus dados de edição @msg No fonts are missing in this document Nenhuma fonte está ausente neste documento @msg VectorCraft doesn't search other apps' or the system's folders: {path} O VectorCraft não pesquisa pastas de outros aplicativos nem do sistema: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} O VectorCraft não pesquisa a pasta Compartilhado ou Público em Usuários: {path} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 0c8e8a86d..950c3b951 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4615,6 +4615,7 @@ axis Both Оба @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise текст в области в рамках, слишком низких для строки шрифта, которым он отображается, получил более высокие рамки ({_1}): иначе он не был бы виден @msg its editing data doesn't say where its art is в его данных редактирования не указано, где находится графика @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) этот файл сохранён без совместимости с PDF, и его данные редактирования не удаётся прочитать ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data не удалось прочитать PDF-часть ({_1}): файл открыт из его данных для редактирования @msg No fonts are missing in this document В этом документе нет отсутствующих шрифтов @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft не ищет в папках других приложений и системы: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft не ищет в папках Shared и Public внутри Users: {path} diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 26c89b954..1cb3c432e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4478,6 +4478,7 @@ axis Both Båda @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise områdestext i ramar som var för låga för en rad i det typsnitt den visas med fick högre ramar ({_1}): annars skulle texten inte synas @msg its editing data doesn't say where its art is dess redigeringsdata anger inte var dess grafik finns @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) den här filen sparades utan PDF-kompatibilitet och dess redigeringsdata kan inte läsas ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF-delen kunde inte läsas ({_1}): filen öppnades från dess redigeringsdata @msg No fonts are missing in this document Inga typsnitt saknas i det här dokumentet @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft söker inte i andra appars eller systemets mappar: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft söker inte i mappen Shared eller Public under Users: {path} diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 10d54c893..f6d53a9c2 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4316,6 +4316,7 @@ axis Both Her İkisi @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise çerçevelerdeki alan türü, daha uzun çerçevelerde gösterdiği yazı tipinin bir satırı için çok kısa ({_1}): bunların türü aksi halde gösterilmez @msg its editing data doesn't say where its art is düzenleme verileri sanatın nerede olduğunu söylemiyor @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) bu dosya PDF uyumluluğu olmadan kaydedildi ve düzenleme verileri okunamıyor ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data PDF kısmı okunamadı ({_1}): dosya düzenleme verilerinden açıldı @msg No fonts are missing in this document Bu belgede eksik yazı tipi yok @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft diğer uygulamaların veya sistemin klasörlerini aramaz: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft, Kullanıcılar'daki Paylaşılan veya Genel klasörü aramıyor: {path} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 34886aa76..751ec623c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4604,6 +4604,7 @@ axis Both Обидві @msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise текст в області в рамках, занизьких для рядка шрифту, яким він показується, отримав вищі рамки ({_1}): інакше його не було б видно @msg its editing data doesn't say where its art is у даних редагування не зазначено, де знаходиться графіка @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) цей файл збережений без сумісності з PDF, і його дані редагування не вдається прочитати ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data не вдалося прочитати PDF-частину ({_1}): файл відкрито з його даних для редагування @msg No fonts are missing in this document У цьому документі немає відсутніх шрифтів @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft не шукає в папках інших програм і системи: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft не шукає в папках Shared і Public у Users: {path} diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 423787a8a..8b4d043cc 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4859,6 +4859,7 @@ theme Appearance Giao diện @msg there are no objects to release không có đối tượng nào để giải phóng @msg there are no printer settings to open here không có cài đặt máy in để mở ở đây @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) tệp này được lưu không có tương thích PDF, và dữ liệu chỉnh sửa không thể đọc được ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data không đọc được phần PDF ({_1}): tệp được mở từ dữ liệu chỉnh sửa @msg this file's PostScript couldn't be read ({why}): {what} PostScript của tệp này không thể đọc được ({why}): {what} @msg this format holds one artboard: export several as PDF or with document.exportForScreens định dạng này chứa một bảng vẽ: xuất nhiều bảng vẽ dưới dạng PDF hoặc với document.exportForScreens @msg this is a DWG drawing, which can't be read: save it from the CAD app as an ASCII DXF and open that đây là bản vẽ DWG, không thể đọc: lưu từ ứng dụng CAD dưới dạng ASCII DXF và mở tệp đó diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f20e071c9..017a8fddd 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3833,6 +3833,7 @@ imageTrace Outline 轮廓 @msg the document has no artboard 文档没有画板 @msg this format holds one artboard: export several as PDF or with document.exportForScreens 此格式只能包含一个画板:要导出多个画板,请导出为 PDF 或使用 document.exportForScreens @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) 此文件保存时未启用 PDF 兼容性,且无法读取其编辑数据 ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data 无法读取 PDF 部分 ({_1}):已从其编辑数据打开文件 @msg the export wrote no file 导出未写入任何文件 @msg {_1} files can be opened but not written (see document.formats) {_1} 文件可以打开,但不能写入 (请参阅 document.formats) @msg {_1} files are written by file.saveAsTemplate {_1} 文件由 file.saveAsTemplate 写入 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index d690d146d..8a9f8e427 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1761,6 +1761,7 @@ file-filter Templates 範本 @msg the document has no artboard 文件沒有工作區域 @msg this format holds one artboard: export several as PDF or with document.exportForScreens 此格式只能容納一個工作區域:若要匯出多個,請匯出為 PDF 或使用 document.exportForScreens @msg this file was saved without PDF compatibility, and its editing data can't be read ({why}) 此檔案儲存時未啟用 PDF 相容性,且無法讀取其編輯資料 ({why}) +@msg the PDF part couldn't be read ({_1}): the file opened from its editing data 無法讀取 PDF 部分 ({_1}):已從其編輯資料開啟檔案 @msg the export wrote no file 匯出未寫入任何檔案 @msg {_1} files can be opened but not written (see document.formats) {_1} 檔案可以開啟,但無法寫入 (請參閱 document.formats) @msg {_1} files are written by file.saveAsTemplate {_1} 檔案由 file.saveAsTemplate 寫入 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 2a2ecbe9b..8f5b2793d 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -51,6 +51,9 @@ private editing data. VectorCraft: A PDF saved with its editing data (`AIPDFPrivateData`) opens the same way. The swatches of a version 7 `.ai` palette, which carry no names, come in named by their spot color, gradient or values. + When the PDF part of either can't be read (a stream the import refuses), the file opens from its editing + data alone, with the PDF part's error as its first warning, and without the bleed and File Info the PDF + part would give (part of #4366). - **Writes** a PDF-compatible `.ai` (Save As › `ai`): Illustrator opens it as plain art. It never writes Illustrator's private editing data, which is undocumented. - **Writes** a legacy `.ai` of version 7 (Save As › `ai` › Version: Legacy (version 7), `aiVersion: "7"`): the diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9a348408a..4d50fe995 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1085,6 +1085,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur `.ai` and EPS editing-data import keeps template layers, and opens canvas and artboard ruler guides as ruler guides on their layers; guide objects stay editable guide paths (#779; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). `.ai` and EPS editing-data import keeps each path's Attributes panel note (`%AI3_Note:`, `%%Note:`, more lines in `%AI5_NoteMore:`, up to 1,024 characters) and Show Center (`1 Ap`); a note or Show Center applies to every path after it until the next one, and a note before a group or compound path is not theirs (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_illustrator.rs`). A `.ai` saved without PDF compatibility (its PDF part only a placeholder page) is placed and pasted from its editing data, as it opens: File › Place places the art of the layers that show on the first artboard (`file.place`'s `page` places another one), and Paste of it as PDF data (`clipboard.importPdf`) takes every object. `document.open` with `pages` naming every page of such a file opens it the same way (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). + A `.ai`, or a PDF saved with Illustrator's editing data, whose PDF part can't be read (a stream the import refuses) opens from its editing data, with the PDF part's error as its first warning; File › Place places its first artboard the same way, and Paste of it as PDF data with `editingData: true` takes every object (part of #4366; `crates/engine/src/cmd/fileio/tests_aiimport.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; 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`).