diff --git a/Cargo.lock b/Cargo.lock index f79516a5a..798c159d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6511,6 +6511,7 @@ name = "vectorcraft-eps" version = "0.21.0" dependencies = [ "craft-pdf", + "encoding_rs", "fax", "flate2", "hayro-ccitt", diff --git a/Cargo.toml b/Cargo.toml index 7d1944df1..9de669345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,9 @@ ruzstd = { version = "0.9.1", default-features = false, features = ["std"] } hayro-ccitt = "0.3" # Substring search for the EPS reader's PostScript `search`. memchr = "2" +# The WHATWG Encoding Standard's legacy encodings: names in legacy .ai files read in the code page +# the user picks (Mac OS Roman, Korean, Japanese, Chinese). +encoding_rs = "0.8.42" jpeg-encoder = { version = "0.6", default-features = false, features = ["std"] } gif = { version = "0.14", default-features = false, features = ["std", "raii_no_panic"] } # CMYK image samples (vectorcraft-doc's cmyk module): the image crate's own TIFF and JPEG diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 8fc2661dc..acb665f49 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -5,7 +5,7 @@ //! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... //! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] [--ppi 300] //! vectorcraft-cli commands -//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] +//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] //! vectorcraft-cli info FILE //! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5] //! vectorcraft-cli perf [--paths 50000] [--deep] @@ -101,7 +101,7 @@ USAGE: vectorcraft-cli commands Print the command catalogue as JSON. - vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] + vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] Open IN (any readable format) and export OUT in the format its extension picks (see Writable formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects resolution; other formats leave them out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard @@ -114,7 +114,10 @@ USAGE: IN left things out (hidden text, art or layers it could not read; the notes say which) unless --replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy even where that draws differently from its PDF part or page (which can be older), as long as - it can be read. A PDF OUT omits the embedded native document unless --preserve-editing is + it can be read. --legacy-name-encoding reads the names in a .ai's or an EPS's editing data + that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles, + swatches, brushes, symbols, variables and data sets, and data sets' text values) in + windows-1252 (the default), macintosh, windows-949, shift_jis, gbk or big5. A PDF OUT omits the embedded native document unless --preserve-editing is given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the attachment is written the result's warnings say so (it is left out, with its own warning, for --artboard or a --range without every artboard). @@ -302,8 +305,20 @@ fn commands() -> Result<(), String> { } fn convert(args: &[String]) -> Result<(), String> { - let valued = - ["--scale", "-s", "--ppi", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"]; + let valued = [ + "--scale", + "-s", + "--ppi", + "--artboard", + "-a", + "--range", + "-r", + "--decimals", + "--ai-version", + "--hidden-objects", + "--transparent-images", + "--legacy-name-encoding", + ]; if help_requested(args, &valued) { return help(); } @@ -312,6 +327,9 @@ fn convert(args: &[String]) -> Result<(), String> { (None::, None::, None::, false, false, false, false); let mut ppi = None::; let mut decimals = None::; + // The encoding of a legacy `.ai`'s names that aren't UTF-8, passed on as `document.open`'s + // `legacyNameEncoding`; the engine checks it. + let mut legacy_names = None::; // Legacy `.ai` options, passed on as the export's own (`aiVersion`, `hiddenObjects`, // `transparentImages`); the engine checks their values. let mut legacy: Vec<(&str, String)> = vec![]; @@ -334,6 +352,9 @@ fn convert(args: &[String]) -> Result<(), String> { "--replace-lossy" => replace_lossy = true, "--editing-data" => editing_data = true, "--preserve-editing" => preserve_editing = true, + "--legacy-name-encoding" => { + legacy_names = Some(it.next().cloned().ok_or("--legacy-name-encoding needs an encoding such as windows-949")?); + } "--ai-version" => legacy.push(("aiVersion", it.next().cloned().ok_or("--ai-version needs 7 or pdf")?)), "--hidden-objects" => legacy.push(("hiddenObjects", it.next().cloned().ok_or("--hidden-objects needs layer, omit or visible")?)), "--transparent-images" => legacy.push(("transparentImages", it.next().cloned().ok_or("--transparent-images needs flatten or omit")?)), @@ -346,8 +367,14 @@ fn convert(args: &[String]) -> Result<(), String> { return Err("a .ait template holds every artboard: --artboard and --range don't apply".into()); } let mut h = Headless::new(); - let opened = if editing_data { - let params = json!({"path": input, "editingData": "always"}); + let opened = if editing_data || legacy_names.is_some() { + let mut params = json!({"path": input}); + if editing_data { + params["editingData"] = json!("always"); + } + if let Some(e) = &legacy_names { + params["legacyNameEncoding"] = json!(e); + } h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))? } else { h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))? diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index 52478ac13..41c24e087 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -9,7 +9,7 @@ pub fn specs() -> Vec { "Open Document", [], None, - "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)", + "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), legacyNameEncoding?: windows-1252 (default) | macintosh | windows-949 (also euc-kr) | shift_jis | gbk | big5 (the code page names in the editing data of a .ai or an EPS are read in when they aren't UTF-8: layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets, and data sets' text values; warnings list the first three such names as read, and count the rest), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)", always, load::open ), diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index a77bdd63c..fd40139cf 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -184,7 +184,8 @@ fn through_editing_data( ) -> (Document, Vec, bool) { let Some(layers) = layers else { return (doc, warnings, false) }; let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines; - let (doc, mut warnings, read) = vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit); + let (doc, mut warnings, read) = + vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit, opts.legacy_names); // The layers weren't read (or couldn't be): the notes of reading the PDF part still hold. if !read { return (doc, warnings, false); @@ -209,12 +210,12 @@ fn from_editing_data_alone(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions) return None; } let private = opened.illustrator_data()?; - Some(editing_copy_alone(&private)) + Some(editing_copy_alone(&private, opts)) } /// [`from_editing_data_alone`] of `private`, the editor's copy already read. -fn editing_copy_alone(private: &[u8]) -> Result<(Document, Vec)> { - vectorcraft_eps::ai_alone(private) +fn editing_copy_alone(private: &[u8], opts: &LoadOptions) -> Result<(Document, Vec)> { + vectorcraft_eps::ai_alone_with(private, opts.legacy_names) .map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})"))) } @@ -247,7 +248,7 @@ fn without_pdf_part(bytes: &[u8], opts: &LoadOptions, e: &EngineError) -> Option 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 (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit, opts.legacy_names).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)) } @@ -323,7 +324,7 @@ pub(crate) fn page_or_editing_data( match read { Ok((doc, notes)) => Ok((doc, notes, false)), Err(e) if is_placeholder(&e) => { - let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), ..Default::default() })?; + let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), legacy_names: opts.legacy_names, ..Default::default() })?; Ok((l.doc, l.warnings, true)) } Err(e) if page_index == 0 => match load_with(name, bytes, &LoadOptions { pages: None, ..opts.clone() }) { @@ -352,7 +353,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result _ if matches!(format.id, "eps" | "ps") || vectorcraft_pdf::is_postscript(bytes) => { let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes))); restore_or_import(editing, || { - let r = vectorcraft_eps::import_trusting(bytes, opts.editing_data, opts.trust_editing_data) + let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names) .map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?; Ok((r.document, r.warnings)) })? @@ -391,7 +392,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result if let Some((o, private)) = opened.as_ref().ok().filter(|_| reads_layers_alone(opts)).and_then(|o| Some((o, editing_copy(o, opts)?))) { let pages = o.trim_pages(); - match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit) { + match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit, opts.legacy_names) { Ok((mut doc, warnings)) => { doc.setup.bleed = pages.bleed; editing_data = true; @@ -413,8 +414,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result // decoded draws nothing either): its notes are at most the page's own. let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w)); let copy_alone = || match &private { - Some(private) => Some(editing_copy_alone(private)), - None if layers.is_some() => opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p)), + Some(private) => Some(editing_copy_alone(private, opts)), + None if layers.is_some() => { + opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p, opts)) + } None => None, }; let alone = (r.only_text || empty) diff --git a/crates/engine/src/cmd/fileio/pdfimport.rs b/crates/engine/src/cmd/fileio/pdfimport.rs index 9550cf90f..2a67cf4a4 100644 --- a/crates/engine/src/cmd/fileio/pdfimport.rs +++ b/crates/engine/src/cmd/fileio/pdfimport.rs @@ -39,6 +39,9 @@ pub struct LoadOptions { /// A `.ai` opens from its editing copy even where that draws differently from its PDF part /// (which can be older than it), with a warning, instead of from the PDF part. pub trust_editing_data: bool, + /// The encoding names in a `.ai` or EPS's editing data are read in when they aren't UTF-8 + /// (`legacyNameEncoding`). + pub legacy_names: vectorcraft_eps::NameEncoding, /// A PDF or `.ai` saved with Preserve Editing opens from the VectorCraft document it carries; /// off, it opens as another app would open it, from the editing copy of its art or its pages. pub native: bool, @@ -57,6 +60,7 @@ impl Default for LoadOptions { dxf: Default::default(), editing_data: true, trust_editing_data: false, + legacy_names: vectorcraft_eps::NameEncoding::default(), native: true, } } @@ -65,8 +69,9 @@ impl Default for LoadOptions { impl LoadOptions { /// From command params: `pages` (`"2-3, 5"`, a page number or `"all"`), `page` (one page, when /// `pages` isn't given), `cropTo` (or `crop`), `password`, `colorMode` (`rgb` | `cmyk`) with `grays`, - /// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`) and `dxf` - /// (the DXF import options). + /// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`), + /// `legacyNameEncoding` (one of [`vectorcraft_eps::NameEncoding::LABELS`]) and `dxf` (the DXF + /// import options). pub fn from_params(cmd: &str, p: &Value) -> Result { let pages = match p.get("pages").filter(|v| !v.is_null()).or_else(|| p.get("page").filter(|v| !v.is_null())) { None => None, @@ -95,8 +100,15 @@ impl LoadOptions { Some(Value::String(s)) if s.eq_ignore_ascii_case("always") => (true, true), Some(v) => (v.as_bool().ok_or_else(|| bad(cmd, "editingData must be true, false or \"always\""))?, false), }; + let legacy_names = match p.get("legacyNameEncoding").filter(|v| !v.is_null()) { + None => vectorcraft_eps::NameEncoding::default(), + Some(v) => v + .as_str() + .and_then(vectorcraft_eps::NameEncoding::from_label) + .ok_or_else(|| bad(cmd, format!("legacyNameEncoding must be one of {}", vectorcraft_eps::NameEncoding::LABELS.join(", "))))?, + }; let dxf = super::dxfimport::options(cmd, p.get("dxf"))?; - Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, native: true }) + Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, legacy_names, native: true }) } /// Does the document read only part of the file, or read it differently (a page range, another diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 489da992a..0f866c658 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -1022,6 +1022,21 @@ fn an_ai_file_saved_without_its_pdf_part_pastes_from_its_editing_data() { assert!(e.contains("placeholder"), "{e}"); } +/// `file.place` reads a `.ai` saved without its PDF part in the `legacyNameEncoding` it is given. +#[test] +fn an_ai_file_saved_without_its_pdf_part_places_in_the_encoding_picked() { + let mut data = ai::sample_data(); + let at = data.windows(4).position(|w| w == b"Pair").unwrap(); + // 빨강 in code page 949. + data.splice(at..at + 4, br"\273\241\260\255".iter().copied()); + let file = vectorcraft_format::base64_encode(&ai::ai(&compressed(&data), PLACEHOLDER)); + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let r = s.execute("file.place", &json!({"name": "art.ai", "dataBase64": file, "link": false, "legacyNameEncoding": "windows-949"})).unwrap(); + let placed = s.doc().unwrap().doc.layers[0].children().unwrap()[0].clone(); + assert_eq!(names(&placed.children().unwrap()[1..]), ["빨강", "Ring", "Window"], "{r}"); +} + #[test] fn an_ai_file_saved_without_its_pdf_part_places_from_its_editing_data() { let [sample, damaged, plain] = placeholder_files(); diff --git a/crates/engine/src/cmd/fileio/tests_epsimport.rs b/crates/engine/src/cmd/fileio/tests_epsimport.rs index 8c4141902..59caf85d6 100644 --- a/crates/engine/src/cmd/fileio/tests_epsimport.rs +++ b/crates/engine/src/cmd/fileio/tests_epsimport.rs @@ -184,6 +184,41 @@ showpage assert!(e.contains("junk.eps"), "{e}"); } +/// `legacyNameEncoding` picks the code page of a legacy `.ai`'s layer names that aren't UTF-8; by +/// default they come in as Windows-1252, with a warning naming them. An encoding it doesn't know is +/// refused. +#[test] +fn legacy_ai_layer_names_open_in_the_encoding_picked() { + let mut s = Session::new(); + // A layer named 레이어 1 in Windows code page 949. + let ai = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 100 100\n%%DocumentNeededResources: procset Adobe_IllustratorA_AI3 1.0 0\n%AI3_TemplateBox: 50 50 50 50\n%%EndComments\n\ + %%BeginProlog\n%%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 0\n%%EndProlog\n%%BeginSetup\nAdobe_IllustratorA_AI3 /initialize get exec\n%%EndSetup\n\ + %AI5_BeginLayer\n1 1 1 1 0 0 2 0 0 0 Lb\n(\\267\\271\\300\\314\\276\\356 1) Ln\n0 0 0 1 K\n0 i 1 w\n10 10 m\n90 90 L\nS\nLB\n%AI5_EndLayer--\n%%PageTrailer\n%%Trailer\n%%EOF\n"; + let open_as = |s: &mut Session, encoding: Option<&str>| { + let mut p = json!({"name": "korean.ai", "dataBase64": vectorcraft_format::base64_encode(ai)}); + if let Some(e) = encoding { + p["legacyNameEncoding"] = json!(e); + } + s.execute("document.open", &p) + }; + let r = open_as(&mut s, None).unwrap(); + assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("·¹À̾î 1")); + assert_eq!( + r["warnings"], + json!(["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"]) + ); + for encoding in ["windows-949", "EUC-KR"] { + let r = open_as(&mut s, Some(encoding)).unwrap(); + assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("레이어 1"), "{encoding}"); + assert!(r["warnings"][0].as_str().is_some_and(|w| w.contains("`레이어 1`") && w.contains("windows-949")), "{r}"); + } + let e = open_as(&mut s, Some("utf-16")).unwrap_err(); + assert!( + matches!(&e, EngineError::BadParams { msg, .. } if msg == "legacyNameEncoding must be one of windows-1252, macintosh, windows-949, shift_jis, gbk, big5"), + "{e}" + ); +} + #[test] fn eps_files_place_as_one_group_clipped_to_their_bounding_box() { let mut s = rich(); diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 87f6d9d67..4a33c5f01 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -3399,6 +3399,23 @@ fn the_shown_stories_fixture_opens_with_its_layers() { assert!(texts.iter().all(|t| t.contains('\u{2028}') && t.contains('\n')), "{texts:?}"); } +/// The fuzzed editing data with its layer, pattern and global colour named in code page 949 opens +/// with those names, read in that encoding, from its layers. +#[test] +fn the_editing_fixture_reads_names_in_a_legacy_encoding() { + // 레이어 1, then 빨강. + let text = editing_text() + .replace("(One) Ln", r"(\267\271\300\314\276\356 1) Ln") + .replace("(Dots)", r"(\273\241\260\255)") + .replace("(Black global)", r"(\273\241\260\255 global)"); + let bytes = editing_eps(&text); + let r = vectorcraft_eps::import_names(&bytes, true, true, vectorcraft_eps::NameEncoding::Korean).unwrap(); + let names: Vec<_> = r.document.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect(); + assert_eq!(names, ["레이어 1", "Two"], "{:?}", r.warnings); + assert!(r.document.pattern("빨강").is_some(), "{:?}", r.warnings); + assert!(r.document.swatch("빨강 global").is_some_and(|s| s.global), "{:?}", r.warnings); +} + proptest! { #![proptest_config(config())] @@ -3475,6 +3492,26 @@ proptest! { survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?; } + /// Names of any bytes (a layer's, a pattern's, a global colour's) read in any legacy name + /// encoding: each byte written as an octal escape, so every code page sees bytes it doesn't + /// define, cut sequences and UTF-8 alike. + #[test] + fn eps_editing_data_names_in_any_encoding_never_panic( + name in prop::collection::vec(any::(), 0..24), + encoding in prop::sample::select(vectorcraft_eps::NameEncoding::LABELS.to_vec()), + ) { + let escaped: String = name.iter().map(|b| format!("\\{b:03o}")).collect(); + let text = editing_text() + .replace("(One) Ln", &format!("({escaped}) Ln")) + .replace("(Dots)", &format!("({escaped})")) + .replace("(Black global)", &format!("({escaped} global)")); + let bytes = editing_eps(&text); + let encoding = vectorcraft_eps::NameEncoding::from_label(encoding).unwrap(); + survive("EPS editing data names in a legacy encoding", || { + vectorcraft_eps::import_names(&bytes, true, false, encoding).ok().map(|r| r.document) + })?; + } + /// A `.ai` whose envelopes are damaged or hostile: any kind, warp style, options word, /// Fidelity and grid size, corners out of range or repeated, the contents block cut short. #[test] diff --git a/crates/eps/Cargo.toml b/crates/eps/Cargo.toml index fb84265b2..13c1c76ad 100644 --- a/crates/eps/Cargo.toml +++ b/crates/eps/Cargo.toml @@ -37,6 +37,8 @@ hayro-ccitt = { workspace = true } serde_json = { workspace = true } # Substring search for PostScript `search`. memchr = { workspace = true } +# Names in legacy .ai files that aren't UTF-8, read in the encoding the user picks. +encoding_rs = { workspace = true } [dev-dependencies] vectorcraft-testkit = { workspace = true } diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 85d8a03ea..78058a7f9 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -462,7 +462,7 @@ fn an_artboard_larger_than_the_canvas_is_written_on_a_large_canvas() { let bounds = read.layers.iter().filter_map(|l| l.visual_bounds()).reduce(|a, b| a.union(b)).unwrap(); assert!((bounds.x0 - 1000.0).abs() < 1e-6 && (bounds.y1 - 2000.0).abs() < 1e-6 && (bounds.width() - 2000.0).abs() < 1e-6, "{bounds:?}"); // The same without a readable PDF part, on its first page's size. - let (alone, notes) = crate::ai_alone_on_page(&p.data, (20_000.0, 20_000.0), 1.389).unwrap(); + let (alone, notes) = crate::ai_alone_on_page(&p.data, (20_000.0, 20_000.0), 1.389, crate::NameEncoding::default()).unwrap(); assert_eq!(alone.artboards.iter().map(|a| a.rect).collect::>(), [Rect::new(0.0, 0.0, 20_000.0, 20_000.0)], "{notes:?}"); // And from the editing data alone, as a file saved without its PDF part opens. let alone = read_back(&doc); diff --git a/crates/eps/src/import.rs b/crates/eps/src/import.rs index 7f1391756..bc41feb7a 100644 --- a/crates/eps/src/import.rs +++ b/crates/eps/src/import.rs @@ -73,6 +73,7 @@ use graphics::{GState, Out}; use interp::{Fault, Interp}; use obj::PsError; +pub use ai::NameEncoding; #[cfg(test)] pub(crate) use ai::objects_read; pub(crate) use ai::{ @@ -80,7 +81,7 @@ pub(crate) use ai::{ symbol_art_member, warp_number, }; pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES}; -pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from}; +pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from}; pub(crate) use native::{MAX_TEXT, scale_document}; pub use text::family_style; @@ -251,6 +252,13 @@ pub fn import_with(bytes: &[u8], editing_data: bool) -> Result /// [`import_with`]; `trust`: an Illustrator EPS's layers are used even where they draw differently /// from its page, with a note (`editingData: "always"`, as for a `.ai`). pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result { + import_names(bytes, editing_data, trust, NameEncoding::default()) +} + +/// [`import_trusting`], with the names in the editing copy that aren't UTF-8 (layers, objects, +/// artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables +/// and data sets) read in `names`. +pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result { let ps = program(bytes)?; let dsc = Dsc::read(ps); // A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no @@ -258,7 +266,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result< // printed). if editing_data && dsc.own_legacy - && let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator) + && let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, names) { return Ok(Imported { document, warnings, preview: false }); } @@ -278,7 +286,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result< } let drew = !out.drawn.is_empty(); let why = match (&result, drew) { - (Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator)), + (Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, names)), (Ok(()), true) => return Ok(finish(out)), (Ok(()), false) => "it draws nothing VectorCraft's PostScript reader can show".to_string(), (Err(e), _) => reason(e, fault.as_ref()), @@ -286,7 +294,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result< // A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the // prolog defining its operators without including it): its layers, which the program has, or // its art on one layer when it has none (Illustrator 3). - let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator)).flatten() { + let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, names)).flatten() { Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }), Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"), None => why, diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index 6b5918530..b353a0088 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -39,14 +39,17 @@ pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN; pub(crate) use envelope::MAX_CHECKED; pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType}; pub(crate) use frames::text_frames; +pub use obj::{NameEncoding, Names}; pub(crate) use paint::MAX_STOPS as MAX_GRADIENT_STOPS; #[cfg(test)] pub(crate) use read::objects_read; #[cfg(test)] +pub use read::read; +#[cfg(test)] pub(crate) use read::with_image_limits; pub use read::{ BindingRef, Brushes, PaletteSwatches, PlacedPage, Structure, Symbols, active_artboard, artboards, bindings_note, brushes, graphic_styles, - palette, read, read_beside, symbols, variables, + palette, read_beside, read_in, symbols, variables, }; pub(crate) use read::{ MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with, diff --git a/crates/eps/src/import/ai/brush.rs b/crates/eps/src/import/ai/brush.rs index 8e1f27bad..dc94ff792 100644 --- a/crates/eps/src/import/ai/brush.rs +++ b/crates/eps/src/import/ai/brush.rs @@ -57,7 +57,7 @@ use vectorcraft_color::Paint; use vectorcraft_doc::{AppearanceItem, Node, NodeKind}; use super::lex::{Lexer, Tok}; -use super::obj; +use super::obj::Names; /// The brush manager's order: the plug-in name of its object. pub(super) const MANAGER: &str = "Adobe Brush Manager Order"; // brand-ok: the plug-in name the files use @@ -157,16 +157,17 @@ pub(super) enum BrushError { } /// The strings of a plug-in object's section (`section`: what follows `%AI8_BeginPluginObject`): -/// its plug-in name, its name and its parameters, joined. The name is `None` with one string, and -/// the whole is `None` with none. -pub(super) fn plugin_strings(section: &[u8]) -> Option<(String, Option, String)> { +/// its plug-in name, the bytes of its name and its parameters, joined, read by `names` without +/// notes. The name is `None` with one string, and the whole is `None` with none. A brush's name has +/// its note when the brush is read, and the names of its art when their art is read. +pub(super) fn plugin_strings(section: &[u8], names: &Names) -> Option<(String, Option>, String)> { let mut lex = Lexer::new(section); let mut strings = vec![]; let mut params = vec![]; while let Some(t) = lex.next_token() { if let Tok::Str(s) | Tok::Hex(s) = t.tok { if strings.len() < 2 { - strings.push(obj::text(&s)); + strings.push(s); } else { // Joined before they are text: a string may end inside a character. let room = MAX_PARAMS.saturating_sub(params.len()); @@ -175,8 +176,8 @@ pub(super) fn plugin_strings(section: &[u8]) -> Option<(String, Option, } } let mut strings = strings.into_iter(); - let kind = strings.next()?; - Some((kind, strings.next(), obj::text(¶ms))) + let kind = names.quiet(&strings.next()?); + Some((kind, strings.next(), names.quiet(¶ms))) } /// The brush manager's order (`params`: its parameters): the kind and name of each brush. Pairs diff --git a/crates/eps/src/import/ai/obj.rs b/crates/eps/src/import/ai/obj.rs index 10027f010..af343bd82 100644 --- a/crates/eps/src/import/ai/obj.rs +++ b/crates/eps/src/import/ai/obj.rs @@ -47,6 +47,237 @@ pub(super) fn text(bytes: &[u8]) -> String { } } +/// The encoding names in the editing data that aren't UTF-8 are read in (layers, objects, +/// artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables +/// and data sets, and data sets' text values). Legacy files store them in the code page of the +/// system that wrote them and don't say which. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum NameEncoding { + /// Windows-1252, Latin-1 for its gaps. + #[default] + Windows1252, + /// Mac OS Roman. + Macintosh, + /// Korean: Windows code page 949 (EUC-KR and Unified Hangul Code). + Korean, + /// Japanese: Shift_JIS (Windows code page 932). + ShiftJis, + /// Simplified Chinese: GBK (Windows code page 936). + Gbk, + /// Traditional Chinese: Big5 (Windows code page 950). + Big5, +} + +impl NameEncoding { + /// The labels [`NameEncoding::from_label`] reads, one for each encoding. + pub const LABELS: [&str; 6] = ["windows-1252", "macintosh", "windows-949", "shift_jis", "gbk", "big5"]; + + /// The encoding with label `label` (any case): one of [`Self::LABELS`], or `euc-kr` for + /// [`NameEncoding::Korean`]. + pub fn from_label(label: &str) -> Option { + let label = label.trim().to_ascii_lowercase(); + Some(match label.as_str() { + "windows-1252" => Self::Windows1252, + "macintosh" => Self::Macintosh, + "windows-949" | "euc-kr" => Self::Korean, + "shift_jis" => Self::ShiftJis, + "gbk" => Self::Gbk, + "big5" => Self::Big5, + _ => return None, + }) + } + + /// The encoding's label, as [`Self::LABELS`] gives it. + pub fn label(self) -> &'static str { + match self { + Self::Windows1252 => "windows-1252", + Self::Macintosh => "macintosh", + Self::Korean => "windows-949", + Self::ShiftJis => "shift_jis", + Self::Gbk => "gbk", + Self::Big5 => "big5", + } + } + + /// `bytes` as text: UTF-8 as it is, else read in this encoding, with each sequence it doesn't + /// define as U+FFFD (Windows-1252 defines every byte). + fn decode(self, bytes: &[u8]) -> Decoded { + if let Ok(s) = std::str::from_utf8(bytes) { + return Decoded { text: s.to_string(), fallback: false, replaced: false }; + } + let other = match self { + Self::Windows1252 => return Decoded { text: bytes.iter().map(|b| cp1252(*b)).collect(), fallback: true, replaced: false }, + Self::Macintosh => encoding_rs::MACINTOSH, + Self::Korean => encoding_rs::EUC_KR, + Self::ShiftJis => encoding_rs::SHIFT_JIS, + Self::Gbk => encoding_rs::GBK, + Self::Big5 => encoding_rs::BIG5, + }; + let (text, replaced) = other.decode_without_bom_handling(bytes); + Decoded { text: text.into_owned(), fallback: true, replaced } + } + + /// [`NameEncoding::decode`] as the text, whether the bytes weren't UTF-8, and whether some + /// weren't valid in the encoding either. + pub(super) fn decoded(self, bytes: &[u8]) -> (String, bool, bool) { + let d = self.decode(bytes); + (d.text, d.fallback, d.replaced) + } +} + +/// A name's hash, for counting the names past the notes. +fn hash(name: &str) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + name.hash(&mut h); + h.finish() +} + +/// Bytes read as text by [`NameEncoding::decode`]. +struct Decoded { + text: String, + /// The bytes weren't UTF-8. + fallback: bool, + /// Some weren't valid in the encoding either, and became U+FFFD. + replaced: bool, +} + +/// Most names that weren't UTF-8 each named in a note; the rest are counted in one. +const MAX_NAME_NOTES: usize = 3; +/// Most other names counted (the count stops there). +const MAX_NAMES_COUNTED: usize = 10_000; + +/// The names read in a [`NameEncoding`], and the notes on those that weren't UTF-8: one for each +/// name (the first [`MAX_NAME_NOTES`]), then one counting the others (up to +/// [`MAX_NAMES_COUNTED`]). +#[derive(Debug, Default)] +pub struct Names { + pub encoding: NameEncoding, + noted: Vec<(String, bool)>, + /// Hashes of the other names. + more: std::collections::HashSet, +} + +impl Names { + pub fn new(encoding: NameEncoding) -> Self { + Self { encoding, ..Self::default() } + } + + /// `bytes` read in the encoding, without a note (a list or a parameter string that repeats + /// names noted where they are defined). + pub fn quiet(&self, bytes: &[u8]) -> String { + self.encoding.decode(bytes).text + } + + /// A string's or name's text as a name: a string that isn't UTF-8 read in the encoding, + /// with a note. + pub(super) fn text(&mut self, v: &V) -> Option { + match v { + V::Str(s) => Some(self.bytes(s)), + V::Name(n) => Some(n.clone()), + _ => None, + } + } + + /// `bytes` read as a name (see [`Names::text`]). + pub fn bytes(&mut self, bytes: &[u8]) -> String { + let d = self.encoding.decode(bytes); + if d.fallback { + self.note(&d.text, d.replaced); + } + d.text + } + + /// The first text value of `o`'s entry `key`, as a name. + pub(super) fn entry(&mut self, o: &Obj, key: &str) -> Option { + let v = o.get(key)?.iter().find(|v| matches!(v, V::Str(_)))?; + self.text(v) + } + + /// The name the XML id in `id` (an `AI10_ArtUID`) gives its object ([`xml_name`]): a string that + /// isn't UTF-8 is read in the encoding, with a note on the name. + pub(super) fn xml_id_name(&mut self, id: &Obj) -> Option { + match id.content.iter().find(|v| matches!(v, V::Str(_) | V::Name(_)))? { + V::Str(bytes) => { + let d = self.encoding.decode(bytes); + let name = xml_name(&d.text); + if d.fallback { + self.note(&name, d.replaced); + } + Some(name) + } + v => v.text().map(|t| xml_name(&t)), + } + } + + /// A note on `name`, which wasn't UTF-8 (`replaced`: parts of it weren't valid in the encoding + /// either). + pub(super) fn note(&mut self, name: &str, replaced: bool) { + if self.noted.iter().any(|(n, _)| n == name) { + return; + } + if self.noted.len() < MAX_NAME_NOTES { + self.noted.push((name.to_string(), replaced)); + } else { + self.count(hash(name)); + } + } + + fn count(&mut self, h: u64) { + if self.more.len() < MAX_NAMES_COUNTED { + self.more.insert(h); + } + } + + /// Take the notes of `other`, a reader's for part of the same data. + pub fn absorb(&mut self, other: Names) { + for (name, replaced) in other.noted { + self.note(&name, replaced); + } + for h in other.more { + self.count(h); + } + } + + /// Take the notes of `other` on the names in `used` (names read but then not used, such as the + /// layers of data whose layers aren't used, left out). + pub fn absorb_among(&mut self, other: Names, used: &[&str]) { + for (name, replaced) in other.noted { + if used.contains(&name.as_str()) { + self.note(&name, replaced); + } + } + for n in used { + let h = hash(n); + if other.more.contains(&h) { + self.count(h); + } + } + } + + /// The notes on the names that weren't UTF-8. + pub fn notes(&self) -> Vec { + let label = self.encoding.label(); + let mut out: Vec = self + .noted + .iter() + .map(|(name, replaced)| { + if *replaced { + format!( + "the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding" + ) + } else { + format!("the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding") + } + }) + .collect(); + if !self.more.is_empty() { + out.push(format!("more names that aren't UTF-8 were read as {label} ({count})", count = self.more.len())); + } + out + } +} + fn cp1252(b: u8) -> char { const HIGH: [char; 32] = [ '€', '\u{81}', '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', '\u{8d}', 'Ž', '\u{8f}', '\u{90}', '‘', '’', '“', '”', '•', '–', '—', @@ -172,4 +403,107 @@ mod tests { assert_eq!(text(b"caf\xe9 \x80"), "café €"); assert_eq!(text("ü".as_bytes()), "ü"); } + + /// The default reads every byte as Windows-1252 does, the five bytes it leaves undefined as the + /// C1 controls of Latin-1, as `text` always has. + #[test] + fn names_default_to_windows_1252_with_latin_1_in_its_gaps() { + let all: Vec = (0x80..=0xff).collect(); + let mut names = Names::default(); + assert_eq!(names.bytes(&all), text(&all)); + let gaps: String = [0x81u8, 0x8d, 0x8f, 0x90, 0x9d].iter().map(|b| names.bytes(&[*b])).collect(); + assert_eq!(gaps, "\u{81}\u{8d}\u{8f}\u{90}\u{9d}"); + assert_eq!(names.bytes(b"\x80\x9f"), "€Ÿ"); + } + + /// A name that is UTF-8 is read as it is in every encoding, without a note. + #[test] + fn utf8_names_need_no_encoding() { + for label in NameEncoding::LABELS { + let mut names = Names::new(NameEncoding::from_label(label).unwrap()); + assert_eq!(names.bytes("레이어 1".as_bytes()), "레이어 1"); + assert_eq!(names.text(&V::Str(b"Layer 1".to_vec())).as_deref(), Some("Layer 1")); + assert!(names.notes().is_empty(), "{label}"); + } + } + + /// Each encoding reads the bytes of a name written in it. + #[test] + fn names_in_each_encoding() { + for (label, bytes, want) in [ + ("windows-1252", &b"Caf\xe9"[..], "Café"), + ("macintosh", b"Caf\x8e", "Café"), + ("windows-949", b"\xb7\xb9\xc0\xcc\xbe\xee 1", "레이어 1"), + ("euc-kr", b"\xb7\xb9\xc0\xcc\xbe\xee 1", "레이어 1"), + ("shift_jis", b"\x83\x8c\x83\x43\x83\x84\x81\x5b 1", "レイヤー 1"), + ("gbk", b"\xcd\xbc\xb2\xe3 1", "图层 1"), + ("big5", b"\xb9\xcf\xbch 1", "圖層 1"), + ] { + let mut names = Names::new(NameEncoding::from_label(label).unwrap()); + assert_eq!(names.bytes(bytes), want, "{label}"); + assert_eq!( + names.notes(), + vec![format!( + "the name `{want}` isn't UTF-8 and was read as {}: if it looks wrong, open the file with another legacyNameEncoding", + names.encoding.label() + )] + ); + } + assert_eq!(NameEncoding::from_label(" Windows-949 "), Some(NameEncoding::Korean)); + assert_eq!(NameEncoding::from_label("utf-8"), None); + for label in NameEncoding::LABELS { + assert_eq!(NameEncoding::from_label(label).map(NameEncoding::label), Some(label)); + } + } + + /// The Korean bytes read by default come in as Windows-1252, with a note naming what they + /// became. + #[test] + fn korean_bytes_by_default_come_in_as_windows_1252_with_a_note() { + let mut names = Names::default(); + let name = names.bytes(b"\xb7\xb9\xc0\xcc\xbe\xee 1"); + assert_eq!(name, "·¹À̾î 1"); + assert_eq!( + names.notes(), + vec!["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"] + ); + } + + /// Bytes the encoding doesn't define come in as U+FFFD, and the note says so. + #[test] + fn malformed_names_keep_a_replacement_character() { + let mut names = Names::new(NameEncoding::Korean); + let name = names.bytes(b"\xb7\xb9\xc0 1"); + assert!(name.contains('\u{fffd}'), "{name:?}"); + assert!(name.starts_with('레'), "{name:?}"); + let notes = names.notes(); + assert_eq!(notes.len(), 1); + assert!(notes[0].contains("isn't UTF-8, and parts of it aren't valid windows-949 either:"), "{notes:?}"); + } + + /// The notes name the first names once each, and count the others. + #[test] + fn name_notes_are_capped() { + let mut names = Names::new(NameEncoding::Macintosh); + for i in 0..10u8 { + let bytes = [b'a' + i, 0x8e]; + names.bytes(&bytes); + names.bytes(&bytes); + } + let notes = names.notes(); + assert_eq!(notes.len(), MAX_NAME_NOTES + 1); + assert!(notes[0].contains("`aé`"), "{notes:?}"); + assert_eq!(notes.last().map(String::as_str), Some("more names that aren't UTF-8 were read as macintosh (7)")); + let mut all = Names::new(NameEncoding::Macintosh); + all.absorb(names); + assert_eq!(all.notes(), notes); + // The count stops at its limit, whatever the file holds. + let mut many = Names::new(NameEncoding::Macintosh); + for i in 0..MAX_NAME_NOTES + MAX_NAMES_COUNTED + 50 { + let mut bytes = i.to_string().into_bytes(); + bytes.push(0x8e); + many.bytes(&bytes); + } + assert_eq!(many.notes().last().map(String::as_str), Some("more names that aren't UTF-8 were read as macintosh (10000)")); + } } diff --git a/crates/eps/src/import/ai/paint.rs b/crates/eps/src/import/ai/paint.rs index 8c863ba99..a25f2b82a 100644 --- a/crates/eps/src/import/ai/paint.rs +++ b/crates/eps/src/import/ai/paint.rs @@ -32,7 +32,7 @@ use vectorcraft_color::gradient::{Gradient, GradientGeom, GradientKind, Gradient use vectorcraft_color::{Color, Paint}; use super::lex::{Lexer, Tok}; -use super::obj::V; +use super::obj::{NameEncoding, Names, V}; /// Largest number of stops read in one gradient. pub(crate) const MAX_STOPS: usize = 256; @@ -49,8 +49,8 @@ pub(super) struct Named { } /// The paint of a colour operator's operands (`op`: its name, lower case or not), with the named -/// colour it uses. `None` when the operands don't fit. -pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option)> { +/// colour it uses, its name read by `names`. `None` when the operands don't fit. +pub(super) fn color_op_in(op: &str, vals: &[V], names: &mut Names) -> Option<(Paint, Option)> { let nums: Vec = vals.iter().filter_map(V::num).map(|v| v as f32).collect(); let unit = |v: f32| v.clamp(0.0, 1.0); let last = |n: usize| nums.get(nums.len().checked_sub(n)?..); @@ -64,9 +64,9 @@ pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option)> { let [r, g, b] = last(3)? else { return None }; Some((Paint::solid(Color::rgb(unit(*r), unit(*g), unit(*b))), None)) } - "x" => named(vals, Some(false)), - "xx" => named(vals, None), - "xk" => named(vals, None).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))), + "x" => named(vals, Some(false), names), + "xx" => named(vals, None, names), + "xk" => named(vals, None, names).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))), "xs" | "xz" => registration(op, vals), _ => None, } @@ -78,14 +78,14 @@ fn is_registration(name: &str) -> bool { /// `c m y k (name) tint x` or `c m y k [r g b | L a b] (name) tint type Xx` (type 1: RGB, 2: Lab); /// `rgb` forces RGB or CMYK. -fn named(vals: &[V], rgb: Option) -> Option<(Paint, Option)> { +fn named(vals: &[V], rgb: Option, names: &mut Names) -> Option<(Paint, Option)> { let at = vals.iter().rposition(|v| matches!(v, V::Str(_) | V::Name(_)))?; - let name = vals.get(at)?.text()?; + let name = vals.get(at)?; let before: Vec = vals.get(..at)?.iter().filter_map(V::num).map(|v| v as f32).collect(); let after: Vec = vals.get(at + 1..)?.iter().filter_map(V::num).map(|v| v as f32).collect(); let unit = |v: f32| v.clamp(0.0, 1.0); let tint = 1.0 - unit(*after.first()?); - if is_registration(&name) { + if is_registration(&name.text()?) { let full = Color::cmyk(1.0, 1.0, 1.0, 1.0); let paint = Paint::Solid { color: full.tinted(tint), swatch: Some(vectorcraft_color::swatch::REGISTRATION.into()), tint }; return Some((paint, None)); @@ -105,6 +105,7 @@ fn named(vals: &[V], rgb: Option) -> Option<(Paint, Option)> { let [c, m, y, k] = before.get(from..from + 4)? else { return None }; Color::cmyk(unit(*c), unit(*m), unit(*y), unit(*k)) }; + let name = names.text(name)?; let paint = Paint::Solid { color: color.tinted(tint), swatch: Some(name.clone()), tint }; Some((paint, Some(Named { name, color, tint, spot: true }))) } @@ -130,7 +131,7 @@ fn registration(op: &str, vals: &[V]) -> Option<(Paint, Option)> { } /// The colour of a gradient stop's colour values and style. -fn stop_color(style: f64, comps: &[V]) -> Option<(Color, Option)> { +fn stop_color(style: f64, comps: &[V], names: &mut Names) -> Option<(Color, Option)> { let nums: Vec = comps.iter().filter_map(V::num).map(|v| v as f32).collect(); let unit = |v: f32| v.clamp(0.0, 1.0); let tail = |n: usize| nums.get(nums.len().checked_sub(n)?..); @@ -145,7 +146,7 @@ fn stop_color(style: f64, comps: &[V]) -> Option<(Color, Option)> { (Color::rgb(unit(*r), unit(*g), unit(*b)), None) } _ => { - let (paint, named) = named(comps, Some(style as i64 == 4))?; + let (paint, named) = named(comps, Some(style as i64 == 4), names)?; (paint.color()?, named) } }) @@ -159,16 +160,16 @@ pub(super) struct GradientDef { } impl GradientDef { - /// `(name) type stops Bd`. - pub fn begin(vals: &[V]) -> Option { - let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None })?; + /// `(name) type stops Bd`, the name read by `names`. + pub fn begin(vals: &[V], names: &mut Names) -> Option { + let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v))?; let nums: Vec = vals.iter().filter_map(V::num).collect(); let kind = if nums.first().copied() == Some(1.0) { GradientKind::Radial } else { GradientKind::Linear }; Some(Self { name, gradient: Gradient::new(kind, vec![]) }) } - /// A stop (`Bs`). - pub fn stop(&mut self, vals: &[V], spots: &mut Vec) { + /// A stop (`Bs`), the name of its colour read by `names`. + pub fn stop(&mut self, vals: &[V], spots: &mut Vec, names: &mut Names) { if self.gradient.stops.len() >= MAX_STOPS { return; } @@ -179,7 +180,7 @@ impl GradientDef { let newer = n >= 5 && n.checked_sub(3).and_then(num) == Some(6.0); let (style_at, opacity) = if newer { (n.checked_sub(5), n.checked_sub(4).and_then(num).unwrap_or(1.0)) } else { (n.checked_sub(3), 1.0) }; let (Some(style_at), Some(style)) = (style_at, style_at.and_then(num)) else { return }; - let Some((color, named)) = stop_color(style, vals.get(..style_at).unwrap_or_default()) else { return }; + let Some((color, named)) = stop_color(style, vals.get(..style_at).unwrap_or_default(), names) else { return }; let mut stop = GradientStop::new((ramp / 100.0).clamp(0.0, 1.0) as f32, color); stop.opacity = opacity.clamp(0.0, 1.0) as f32; stop.midpoint = (mid / 100.0).clamp(0.13, 0.87) as f32; @@ -225,9 +226,16 @@ pub(super) struct ColorNames { vals: Vec, def: Option, spots: Vec, + /// Reads the names found (the reader reads them again, and keeps the notes on them). + decoder: Names, } impl ColorNames { + /// The names found to be read in `encoding`. + pub fn new(encoding: NameEncoding) -> Self { + Self { decoder: Names::new(encoding), ..Self::default() } + } + pub fn token(&mut self, tok: Tok<'_>) { self.take(tok, true); } @@ -252,7 +260,7 @@ impl ColorNames { }; let vals = std::mem::take(&mut self.vals); match w { - b"Bd" => self.def = GradientDef::begin(&vals), + b"Bd" => self.def = GradientDef::begin(&vals, &mut self.decoder), b"BD" => { if let Some((name, _)) = self.def.take().and_then(GradientDef::finish) { self.gradients.push(name); @@ -260,18 +268,18 @@ impl ColorNames { } b"Bs" => { if let Some(d) = self.def.as_mut() { - d.stop(&vals, &mut self.spots); + d.stop(&vals, &mut self.spots, &mut self.decoder); } } b"x" | b"X" | b"Xx" | b"XX" | b"Xk" | b"XK" => { - if let Some((_, Some(n))) = std::str::from_utf8(w).ok().and_then(|op| color_op(op, &vals)) { + if let Some((_, Some(n))) = std::str::from_utf8(w).ok().and_then(|op| color_op_in(op, &vals, &mut self.decoder)) { self.spots.push(n); } } b"@" | b"&" if outer => { for v in &vals { if let V::Str(s) = v { - let mut inner = ColorNames::default(); + let mut inner = ColorNames::new(self.decoder.encoding); let mut lex = Lexer::new(s); while let Some(t) = lex.next_token() { inner.take(t.tok, false); @@ -396,10 +404,10 @@ impl Instance { Self { stroke, length: 1.0, ..Self::default() } } - /// `flag (name) x y angle length a b c d tx ty Bg`. - pub fn bg(&mut self, vals: &[V]) { + /// `flag (name) x y angle length a b c d tx ty Bg`, the name read by `names`. + pub fn bg(&mut self, vals: &[V], names: &mut Names) { let Some(at) = vals.iter().rposition(|v| matches!(v, V::Str(_))) else { return }; - self.name = vals.get(at).and_then(V::text); + self.name = vals.get(at).and_then(|v| names.text(v)); let n: Vec = vals.get(at + 1..).unwrap_or_default().iter().filter_map(V::num).collect(); if let [x, y, angle, length, m @ ..] = n.as_slice() { self.origin = Point::new(*x, *y); @@ -520,6 +528,35 @@ mod tests { v.iter().map(|n| V::Num(*n)).collect() } + fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option)> { + color_op_in(op, vals, &mut Names::default()) + } + + /// A named colour, a gradient and its stop's colour, and the gradient an object names, read in + /// the encoding picked, so that names written alike match. + #[test] + fn colour_and_gradient_names_read_in_the_encoding() { + let mut names = Names::new(super::super::obj::NameEncoding::Korean); + let mut v = nums(&[0.0, 0.5, 1.0, 0.0]); + v.extend([V::Str(b"\xbb\xa1\xb0\xad".to_vec()), V::Num(0.0)]); + let (_, named) = color_op_in("x", &v, &mut names).unwrap(); + assert_eq!(named.unwrap().name, "빨강"); + let mut d = GradientDef::begin(&[V::Str(b"\xb1\xd7\xb6\xf3".to_vec()), V::Num(0.0)], &mut names).unwrap(); + let mut stop = nums(&[0.0, 0.5, 1.0, 0.0]); + stop.extend([V::Str(b"\xbb\xa1\xb0\xad".to_vec()), V::Num(0.0), V::Num(3.0), V::Num(50.0), V::Num(0.0)]); + let mut spots = vec![]; + d.stop(&stop, &mut spots, &mut names); + assert_eq!(spots.first().map(|s| s.name.as_str()), Some("빨강")); + let (name, g) = d.finish().unwrap(); + assert_eq!(name, "그라"); + let mut i = Instance::begin(&[]); + let mut bg = vec![V::Num(1.0), V::Str(b"\xb1\xd7\xb6\xf3".to_vec())]; + bg.extend(nums(&[0.0, 0.0, 0.0, 10.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0])); + i.bg(&bg, &mut names); + assert!(i.paint(&HashMap::from([(name, g)]), &ShownNames::default(), Affine::IDENTITY).is_some()); + assert_eq!(names.notes().len(), 2); + } + #[test] fn registration_paints_with_the_registration_swatch_at_its_tint() { let mut v = nums(&[0.75, 0.68, 0.67, 0.9, 0.0, 0.0, 0.0]); @@ -608,10 +645,11 @@ mod tests { #[test] fn gradient_definition_and_instance() { - let mut d = GradientDef::begin(&[V::Str(b"G".to_vec()), V::Num(1.0), V::Num(2.0)]).unwrap(); + let names = &mut Names::default(); + let mut d = GradientDef::begin(&[V::Str(b"G".to_vec()), V::Num(1.0), V::Num(2.0)], names).unwrap(); let mut spots = vec![]; - d.stop(&nums(&[0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.5, 6.0, 30.0, 100.0]), &mut spots); - d.stop(&nums(&[0.81, 0.0, 1.0, 6.0, 50.0, 0.0]), &mut spots); + d.stop(&nums(&[0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.5, 6.0, 30.0, 100.0]), &mut spots, names); + d.stop(&nums(&[0.81, 0.0, 1.0, 6.0, 50.0, 0.0]), &mut spots, names); let (name, g) = d.finish().unwrap(); assert_eq!(name, "G"); assert_eq!(g.kind, GradientKind::Radial); @@ -622,7 +660,7 @@ mod tests { let mut i = Instance::begin(&[]); let mut bg = vec![V::Num(1.0), V::Str(b"G".to_vec())]; bg.extend(nums(&[100.0, 100.0, 0.0, 50.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0])); - i.bg(&bg); + i.bg(&bg, names); i.bm(&nums(&[50.0, 0.0, 0.0, -50.0, 100.0, 100.0])); let shown = ShownNames::default(); let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() }; @@ -634,7 +672,7 @@ mod tests { assert!(i.paint(&HashMap::new(), &shown, Affine::IDENTITY).is_none()); // Newer files give the matrix with `Xm`; the `Bm`s after it are bands of a cached ramp. let mut i = Instance::begin(&[]); - i.bg(&bg); + i.bg(&bg, names); i.xm(&nums(&[40.0, 0.0, 0.0, -40.0, 10.0, 10.0])); i.bm(&nums(&[5.0, 0.0, 0.0, -40.0, 30.0, 10.0])); let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() }; @@ -679,11 +717,12 @@ mod tests { #[test] fn gradient_stops_beyond_the_cap_are_ignored() { - let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)]).unwrap(); + let names = &mut Names::default(); + let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)], names).unwrap(); let mut spots = vec![]; for i in 0..257 { let ramp = i as f64 / 256.0 * 100.0; - d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut spots); + d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut spots, names); } let (_, g) = d.finish().unwrap(); assert_eq!(g.stops.len(), MAX_STOPS); @@ -694,9 +733,10 @@ mod tests { #[test] fn stops_at_one_ramp_point_keep_their_sides() { for (kind, listed) in [(0.0, [0.75, 0.25]), (1.0, [0.25, 0.75])] { - let mut d = GradientDef::begin(&[V::Str(b"E".to_vec()), V::Num(kind), V::Num(2.0)]).unwrap(); + let names = &mut Names::default(); + let mut d = GradientDef::begin(&[V::Str(b"E".to_vec()), V::Num(kind), V::Num(2.0)], names).unwrap(); for (gray, mid) in listed.into_iter().zip([40.0, 70.0]) { - d.stop(&nums(&[gray, 0.0, 1.0, 6.0, mid, 30.0]), &mut vec![]); + d.stop(&nums(&[gray, 0.0, 1.0, 6.0, mid, 30.0]), &mut vec![], names); } let (_, g) = d.finish().unwrap(); let lightness: Vec = g.stops.iter().map(|s| (s.color.to_rgb()[0] * 100.0).round() / 100.0).collect(); @@ -711,16 +751,17 @@ mod tests { /// art space, `Xm` for the whole ramp and a `Bm` for each pair of stops. #[test] fn a_linear_gradient_runs_its_whole_ramp_not_its_last_part() { - let mut d = GradientDef::begin(&[V::Str(b"L".to_vec()), V::Num(0.0), V::Num(3.0)]).unwrap(); + let names = &mut Names::default(); + let mut d = GradientDef::begin(&[V::Str(b"L".to_vec()), V::Num(0.0), V::Num(3.0)], names).unwrap(); for ramp in [100.0, 60.0, 20.0] { - d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut vec![]); + d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut vec![], names); } let defs = HashMap::from([d.finish().unwrap()]); let instance = |xm: bool| { let mut i = Instance::begin(&[]); let mut bg = vec![V::Num(1.0), V::Str(b"L".to_vec())]; bg.extend(nums(&[0.3, -0.2, -90.0, 0.5, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0])); - i.bg(&bg); + i.bg(&bg, &mut Names::default()); if xm { i.xm(&nums(&[0.0, -200.0, -30.0, 0.0, 10.0, 300.0])); } diff --git a/crates/eps/src/import/ai/palette.rs b/crates/eps/src/import/ai/palette.rs index e816c5d6c..b4aba243d 100644 --- a/crates/eps/src/import/ai/palette.rs +++ b/crates/eps/src/import/ai/palette.rs @@ -26,7 +26,7 @@ use vectorcraft_color::Color; use vectorcraft_doc::swatches::color_name; use super::lex::{Lexer, Tok}; -use super::obj::V; +use super::obj::{Names, V}; use super::paint::{self, Named}; /// Largest number of swatches read from one palette. @@ -73,8 +73,9 @@ impl Palette { } } -/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end. -pub(super) fn palette(data: &[u8]) -> Palette { +/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end, its names read by +/// `names`. +pub(super) fn palette(data: &[u8], names: &mut Names) -> Palette { let mut lex = Lexer::new(data); let mut out = Palette::default(); let mut vals: Vec = Vec::new(); @@ -125,15 +126,15 @@ pub(super) fn palette(data: &[u8]) -> Palette { cell = None; } "Bg" if in_gradient => { - if let Some(name) = last_text(&vals) { + if let Some(name) = last_text(&vals, names) { cell = Some(Entry { cell: Cell::Gradient(name.clone()), name }); } } "BB" => in_gradient = false, - "p" => cell = first_text(&vals).map(|name| Entry { cell: Cell::Pattern(name.clone()), name }), + "p" => cell = first_text(&vals, names).map(|name| Entry { cell: Cell::Pattern(name.clone()), name }), "Xs" | "Xz" | "XS" | "XZ" => cell = None, "g" | "k" | "x" | "Xa" | "Xx" | "Xk" => { - cell = paint::color_op(w, &vals).and_then(|(p, named)| match named { + cell = paint::color_op_in(w, &vals, names).and_then(|(p, named)| match named { Some(Named { name, color, spot, .. }) => Some(Entry { name, cell: Cell::Color { color, global: true, spot } }), None => p.color().map(|color| Entry { name: color_name(color), cell: Cell::Color { color, global: false, spot: false } }), }); @@ -141,7 +142,7 @@ pub(super) fn palette(data: &[u8]) -> Palette { "Pg" => { cell = None; // Capped as swatches are: a file of many `Pg` would make as many empty groups. - if let Some(name) = first_text(&vals) + if let Some(name) = first_text(&vals, names) && out.groups.len() < MAX_SWATCHES { out.groups.push((name, Vec::new())); @@ -150,7 +151,7 @@ pub(super) fn palette(data: &[u8]) -> Palette { "Pc" => { // The name before `Pc`, or the one the paint gives; `[Registration]` and the other // built-in swatches every document has are left out. - let name = last_text(&vals); + let name = last_text(&vals, names); if let Some(mut e) = cell.take() && !name.as_ref().is_some_and(|n| n.starts_with('[')) && count < MAX_SWATCHES @@ -181,12 +182,12 @@ fn push(vals: &mut Vec, v: V) { } } -fn first_text(vals: &[V]) -> Option { - vals.iter().find(|v| matches!(v, V::Str(_))).and_then(V::text) +fn first_text(vals: &[V], names: &mut Names) -> Option { + vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v)) } -fn last_text(vals: &[V]) -> Option { - vals.iter().rev().find(|v| matches!(v, V::Str(_))).and_then(V::text) +fn last_text(vals: &[V], names: &mut Names) -> Option { + vals.iter().rev().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v)) } #[cfg(test)] @@ -197,6 +198,21 @@ mod tests { Entry { name: name.into(), cell: Cell::Color { color, global, spot } } } + fn palette(data: &[u8]) -> Palette { + super::palette(data, &mut Names::default()) + } + + /// Swatch, group and spot colour names that aren't UTF-8 are read in the encoding picked. + #[test] + fn palette_names_read_in_the_encoding() { + let mut names = Names::new(super::super::obj::NameEncoding::ShiftJis); + // 赤 (red) and 色 (color) in Shift_JIS. + let p = super::palette(b"0 0 Pb\n0 (\x90\x46) 1 Pg\n0 1 1 0 (\x90\xd4) 0 x\n(\x90\xd4)\nPc\nPB\n", &mut names); + assert_eq!(p.groups.first().map(|(n, s)| (n.as_str(), s.len())), Some(("色", 1))); + assert_eq!(p.groups.first().and_then(|(_, s)| s.first()), Some(&color("赤", Color::cmyk(0.0, 1.0, 1.0, 0.0), true, true))); + assert_eq!(names.notes().len(), 2); + } + #[test] fn a_palette_gives_its_swatches_in_order() { let p = palette( diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index b01ae54a1..cb0bc1909 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -125,7 +125,7 @@ use vectorcraft_geom::{AnchorKind, FillRule, PathData}; use super::super::ate::CANVAS_CENTRE; use super::super::graphics::HAIRLINE; use super::lex::{Lexer, Tok}; -use super::obj::{self, Obj, V}; +use super::obj::{self, NameEncoding, Obj, V}; use super::paint::{self, Global, GradientDef, Instance, Named, ShownNames}; use crate::ps; @@ -238,22 +238,30 @@ pub(super) fn remember_xml_id(xml_ids: &mut HashMap>, byt } } +/// [`read_in`], names that aren't UTF-8 read as Windows-1252. +#[cfg(test)] +pub fn read(data: &[u8], large_canvas: bool) -> Result { + read_in(data, large_canvas, NameEncoding::default()) +} + /// Read the structure of the editing data `data`, applying the file's `%AI24_LargeCanvasScale` only /// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may -/// keep the reduced one, which the PDF import reads as it is (#2031). -pub fn read(data: &[u8], large_canvas: bool) -> Result { - read_with(data, large_canvas, false) +/// keep the reduced one, which the PDF import reads as it is (#2031). Names that aren't UTF-8 +/// (layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, +/// symbols, variables and data sets) are read in `names`, each with a warning. +pub fn read_in(data: &[u8], large_canvas: bool, names: NameEncoding) -> Result { + read_with(data, large_canvas, false, names) } -/// [`read`] for a file whose page art replaces the objects the reader doesn't read (see +/// [`read_in`] for a file whose page art replaces the objects the reader doesn't read (see /// [`Reader::unread_box`]): an image past the hard limits ([`raster::Limits`]) is then such an -/// object, where [`read`] leaves it out with a note. -pub(crate) fn read_over_page(data: &[u8], large_canvas: bool) -> Result { - read_with(data, large_canvas, true) +/// object, where [`read_in`] leaves it out with a note. +pub(crate) fn read_over_page(data: &[u8], large_canvas: bool, names: NameEncoding) -> Result { + read_with(data, large_canvas, true, names) } -fn read_with(data: &[u8], large_canvas: bool, page_art: bool) -> Result { - let mut r = Reader::with_names(data); +fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result { + let mut r = Reader::with_names(data, names); r.large_canvas = large_canvas.then_some(1.0); r.page_art = page_art; r.run()?; @@ -263,17 +271,18 @@ fn read_with(data: &[u8], large_canvas: bool, page_art: bool) -> Result Result { - let mut r = Reader::with_names(data); + let mut r = Reader::with_names(data, NameEncoding::default()); r.run()?; Ok(r.nodes) } /// [`read`] for editing data kept apart from the program `ps` it came with (an EPS's private /// data): the program's header gives the author and the custom colors the editing data's header -/// doesn't. `page_art`: as [`read_over_page`] reads a file. -pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool) -> Result { - let header = Reader::header_of(ps); - let mut r = Reader::with_names(data); +/// doesn't. `page_art`: as [`read_over_page`] reads a file. `names`: the encoding of names that +/// aren't UTF-8. +pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool, names: NameEncoding) -> Result { + let header = Reader::header_of(ps, names); + let mut r = Reader::with_names(data, names); r.large_canvas = large_canvas.then_some(1.0); r.page_art = page_art; // The program's custom colors have swatches of their names, which no gradient or pattern takes. @@ -292,8 +301,8 @@ pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool) - /// and patterns. Names come only from what the reader reads: not from the alternates of versioned /// content, the prolog, resources, sections it skips (such as symbols and the text document) or /// what follows the trailer. The palette the reader keeps is the last one outside those and the -/// sections it reads on their own. -fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) { +/// sections it reads on their own. Names that aren't UTF-8 are read in `encoding`. +fn swatch_names(data: &[u8], encoding: NameEncoding) -> (ShownNames, Option<&[u8]>) { /// The sections the reader reads on their own whose definitions and colors it keeps. const READ_APART: &[&str] = &["BeginPattern", "BeginBrushPattern", "BeginPluginObject", "BeginArtStyles", "BeginArtStyleList"]; /// Whether the marker `m` ends what began with `begin` and ends with `end`, `depth` of its kind @@ -311,7 +320,9 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) { } let mut names = ShownNames::default(); names.reserve(vectorcraft_color::swatch::REGISTRATION); - let mut colors = paint::ColorNames::default(); + // The reader reads these names again, and keeps the notes on them. + let mut decoder = obj::Names::new(encoding); + let mut colors = paint::ColorNames::new(encoding); let mut lex = Lexer::new(data); // What the reader skips wherever it is (the marker of one of its kind nested in it, its end // marker, how many are open inside it, and where reading goes on when it doesn't end), the @@ -386,7 +397,7 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) { // A palette's names are those of the palette kept, as the palette reads them. _ if skip.is_some() || palette_at.is_some() || section.is_some_and(|s| !s.3) => continue, Tok::Str(s) | Tok::Hex(s) if std::mem::take(&mut head) => { - let text = obj::text(s); + let text = decoder.bytes(s); if !text.contains('\n') { names.reserve(&text); } @@ -396,7 +407,7 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) { colors.token(t.tok); } colors.gradients.iter().filter(|n| !n.contains('\n')).for_each(|n| names.reserve(n)); - let palette = palette.map(super::palette::palette).unwrap_or_default(); + let palette = palette.map(|p| super::palette::palette(p, &mut decoder)).unwrap_or_default(); if palette.is_empty() { for mode in [ColorMode::Rgb, ColorMode::Cmyk] { let (swatches, groups) = vectorcraft_color::default_swatches(mode.model()); @@ -631,6 +642,8 @@ struct Reader<'a> { /// The legacy text object being read (`To` … `TO`), and the modal type attributes. text: Option, type_state: legacy_text::TypeState, + /// How names that aren't UTF-8 are read, and the notes on them. + names: obj::Names, /// The file has brush objects (the brush manager's order or a brush). brush_objects: bool, /// The brush manager's order. @@ -668,13 +681,14 @@ pub(crate) fn is_look_slot(name: Option<&str>) -> bool { impl<'a> Reader<'a> { /// A reader of the editing data `data` as a whole, with the names its gradients and patterns /// get in the document and its art styles found first ([`swatch_names`]): the symbols defined - /// before the art styles section use them too. - fn with_names(data: &'a [u8]) -> Self { + /// before the art styles section use them too. Names that aren't UTF-8 are read in `encoding`. + fn with_names(data: &'a [u8], encoding: NameEncoding) -> Self { let mut r = Self::new(data); - let (names, styles) = swatch_names(data); + r.names = obj::Names::new(encoding); + let (names, styles) = swatch_names(data, encoding); r.global_names = names; if let Some(section) = styles { - r.styles = super::style::art_styles(section); + r.styles = super::style::art_styles_in(section, &mut r.names); } r } @@ -794,6 +808,7 @@ impl<'a> Reader<'a> { v7_patterns: HashSet::new(), text: None, type_state: legacy_text::TypeState::default(), + names: obj::Names::default(), brush_objects: false, brush_order: None, brush_drafts: Vec::new(), diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index a08f2ea1c..35c0f46ae 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -640,7 +640,7 @@ impl<'a> Reader<'a> { /// comes in as the page draws it, within the symbol's box placed by the matrix when the box is /// known. fn symbol_instance(&mut self, o: &Obj) -> Result<(), String> { - let name = o.text("SymbolRef"); + let name = self.names.entry(o, "SymbolRef"); let affine = |n: &[f64]| match n { [a, b, c, d, e, f] if [a, b, c, d, e, f].iter().all(|v| v.is_finite()) => Some(super::Affine::new([*a, *b, *c, *d, *e, *f])), _ => None, diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index ef27e5f74..e1fbe0d4d 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -123,7 +123,7 @@ impl<'a> Reader<'a> { // GUARDS: a definition's art takes the file's art styles and hands them back, so a // section inside it would replace the file's own. if self.subreader_depth == 0 { - self.styles = crate::import::ai::style::art_styles(section); + self.styles = crate::import::ai::style::art_styles_in(section, &mut self.names); } return Ok(()); } @@ -134,7 +134,7 @@ impl<'a> Reader<'a> { } if m == "BeginPalette" { let section = self.section("EndPalette")?; - self.palette = crate::import::ai::palette::palette(section); + self.palette = crate::import::ai::palette::palette(section, &mut self.names); return Ok(()); } if m == "BeginEncoding" { @@ -309,7 +309,7 @@ impl<'a> Reader<'a> { return; } let Some(open) = v.iter().position(|&b| b == b'(') else { return }; - let (Some(head), Some(name)) = (v.get(..open), v.get(open..).and_then(ps_string)) else { return }; + let (Some(head), Some(name)) = (v.get(..open), v.get(open..).and_then(ps_bytes)) else { return }; // One more than a color has, so that too many are seen. let parts: Vec> = head .split(u8::is_ascii_whitespace) @@ -322,6 +322,7 @@ impl<'a> Reader<'a> { [Some(r), Some(g), Some(b)] if !cmyk => vectorcraft_color::Color::rgb(*r, *g, *b), _ => return, }; + let name = self.names.bytes(&name); if !name.trim().is_empty() && !self.custom_colors.iter().any(|n| n.name == name) { // A gradient or pattern defined after it does not take its swatch's name. self.global_names.reserve(&name); @@ -378,6 +379,7 @@ impl<'a> Reader<'a> { Legacy::Yes(def) => return self.define_legacy_pattern(&def), } let mut sub = Reader::new(section); + sub.names = super::obj::Names::new(self.names.encoding); sub.subreader_depth = self.subreader_depth + 1; sub.pattern = Some(PatternHead::default()); // The gradients and art styles defined so far and the names given, lent (and the gradients @@ -392,6 +394,7 @@ impl<'a> Reader<'a> { self.take_back_budgets(&mut sub); self.nodes = sub.nodes; self.gradients = std::mem::take(&mut sub.gradients); + self.names.absorb(std::mem::take(&mut sub.names)); self.global_names = std::mem::take(&mut sub.global_names); self.styles = std::mem::take(&mut sub.styles); // Unread art in the definition, hidden or not, leaves the tile incomplete. @@ -409,8 +412,9 @@ impl<'a> Reader<'a> { /// tile's box (`_ &`). Each layer starts from the state the definition starts with. fn define_legacy_pattern(&mut self, def: &LegacyPattern) { let mut sub = Reader::new(b""); + sub.names = super::obj::Names::new(self.names.encoding); sub.subreader_depth = self.subreader_depth + 1; - sub.pattern = Some(PatternHead { taken: true, name: def.name.clone(), tile: Some(def.tile) }); + sub.pattern = Some(PatternHead { taken: true, name: self.names.bytes(&def.name), tile: Some(def.tile) }); sub.gradients = std::mem::take(&mut self.gradients); sub.global_names = std::mem::take(&mut self.global_names); sub.styles = std::mem::take(&mut self.styles); @@ -429,6 +433,7 @@ impl<'a> Reader<'a> { self.gradients = std::mem::take(&mut sub.gradients); self.global_names = std::mem::take(&mut sub.global_names); self.styles = std::mem::take(&mut sub.styles); + self.names.absorb(std::mem::take(&mut sub.names)); let whole = read.is_ok() && sub.unsupported.is_empty() && sub.unknown.is_empty() @@ -569,10 +574,11 @@ impl<'a> Reader<'a> { return; } let name = match Lexer::new(section).next_token().map(|t| t.tok) { - Some(Tok::Str(bytes)) => obj::text(&bytes), + Some(Tok::Str(bytes)) => self.names.bytes(&bytes), _ => return, }; let mut sub = Reader::new(section); + sub.names = super::obj::Names::new(self.names.encoding); sub.subreader_depth = self.subreader_depth + 1; sub.symbol = Some(SymbolHead::default()); sub.template = self.template; @@ -591,6 +597,7 @@ impl<'a> Reader<'a> { self.global_names = std::mem::take(&mut sub.global_names); self.styles = std::mem::take(&mut sub.styles); self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles); + self.names.absorb(std::mem::take(&mut sub.names)); // Art stopped by a trailer, or with an array, type, image, placed file, gradient mesh or // gradient still open at the end (as [`Reader::run_string`] checks), isn't whole. let open = sub.done @@ -709,7 +716,7 @@ impl<'a> Reader<'a> { if names.len() >= MAX_LISTED { break; } - names.push(obj::text(&bytes)); + names.push(self.names.quiet(&bytes)); } } self.symbol_list = Some(names); @@ -719,7 +726,7 @@ impl<'a> Reader<'a> { /// order or a brush ([`brush`]); other objects are left alone. A brush without a name is counted /// as not read. fn plugin_object(&mut self, section: &[u8]) { - let Some((kind, name, params)) = brush::plugin_strings(section) else { return }; + let Some((kind, name, params)) = brush::plugin_strings(section, &self.names) else { return }; if kind == brush::MANAGER { if name.is_some() { self.brush_objects = true; @@ -737,8 +744,12 @@ impl<'a> Reader<'a> { self.brushes_unread += 1; return; } - match brush::parse_tool_object(tool, &name, ¶ms) { - Ok(d) => self.brush_drafts.push(d), + match brush::parse_tool_object(tool, &self.names.quiet(&name), ¶ms) { + Ok(d) => { + // Only the name of a brush that is read gets a note. + self.names.bytes(&name); + self.brush_drafts.push(d); + } Err(_) => self.brushes_unread += 1, } } @@ -756,8 +767,9 @@ impl<'a> Reader<'a> { self.brush_art_sections += 1; let mut lex = Lexer::new(section); let Some(Tok::Str(name) | Tok::Hex(name)) = lex.next_token().map(|t| t.tok) else { return }; - let name = obj::text(&name); + let name = self.names.bytes(&name); let mut sub = Reader::new(lex.slice(lex.offset(), section.len())); + sub.names = obj::Names::new(self.names.encoding); // In a space of its own (y down), with no head to take. sub.pattern = Some(PatternHead { taken: true, ..PatternHead::default() }); sub.subreader_depth = self.subreader_depth + 1; @@ -779,6 +791,7 @@ impl<'a> Reader<'a> { self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles); self.v7_patterns = std::mem::take(&mut sub.v7_patterns); self.global_names = std::mem::take(&mut sub.global_names); + self.names.absorb(std::mem::take(&mut sub.names)); // Images aren't kept: the document's images don't count a brush's art among their users. Art // read with a note (something left out of it) isn't kept either. let whole = read.is_ok() @@ -834,7 +847,8 @@ impl<'a> Reader<'a> { head.taken = true; let Some(at) = vals.iter().position(|v| matches!(v, V::Str(_))) else { return vals }; let tile = Self::nums(vals.get(at + 1..at + 5).unwrap_or_default()); - let ([x0, y0, x1, y1], Some(name)) = (tile.as_slice(), vals.get(at).and_then(V::text)) else { return vals }; + let [x0, y0, x1, y1] = tile.as_slice() else { return vals }; + let Some(name) = vals.get(at).and_then(|v| self.names.text(v)) else { return vals }; head.name = name; head.tile = Some(Rect::new(*x0, *y0, *x1, *y1)); vals.split_off(at + 5) @@ -853,7 +867,7 @@ const MAX_LAYERS: usize = 65_536; /// A pattern's definition in the version 7 form: its head, then a list of layers painted in order, /// the first the lowest. struct LegacyPattern { - name: String, + name: Vec, /// The tile in the pattern's space (y up). tile: Rect, layers: Vec, @@ -892,7 +906,7 @@ fn legacy_pattern(section: &[u8]) -> Legacy { let mut lex = super::Lexer::new(section); let mut next = || next_operand(&mut lex).ok().flatten().map(|t| (t.tok, t.hidden)); let name = match next() { - Some((Tok::Str(s), _)) => super::obj::text(&s), + Some((Tok::Str(s), _)) => s, _ => return Legacy::No, }; let mut tile = [0.0; 4]; @@ -961,6 +975,11 @@ const MAX_STRING: usize = 4096; /// The PostScript string `v` starts with (after spaces), its escapes read, when it ends on the line /// and holds at most [`MAX_STRING`] bytes. fn ps_string(v: &[u8]) -> Option { + ps_bytes(v).map(|bytes| obj::text(&bytes)) +} + +/// [`ps_string`] as the string's bytes. +fn ps_bytes(v: &[u8]) -> Option> { let mut bytes = v.trim_ascii_start().iter().copied().peekable(); if bytes.next()? != b'(' { return None; @@ -997,7 +1016,7 @@ fn ps_string(v: &[u8]) -> Option { out.push(b); } b')' => { - let Some(d) = depth.checked_sub(1) else { return Some(super::obj::text(&out)) }; + let Some(d) = depth.checked_sub(1) else { return Some(out) }; depth = d; out.push(b); } diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 9277bcc62..c70486915 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -7,6 +7,7 @@ use super::{ Affine, Arc, Artboard, CANVAS_CENTRE, ColorMode, Document, Global, HashMap, Node, NodeId, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect, Structure, Swatch, Unit, V, obj, }; +use crate::import::ai::NameEncoding; use vectorcraft_doc::{DataSet, DataValue, GraphicStyle, Guide, ImageBlob, PatternDef, Symbol, TextWrap, Variable, VariableKind, Variables}; /// `%AI5_RulerUnits` → [`Unit`] (Illustrator 7 spec table 3; centimeters is 4; pixels is 6 in later files). @@ -141,21 +142,31 @@ pub(crate) const LOOK_SLOT: &str = "\u{0}text:look"; /// The artboards (names, and rects in art space) of the editing data `data` that [`read`] can't /// read as a whole, as far as it gets: where to put the pages of its PDF part instead, and what to -/// call them (#1068); and the index among them of the active artboard (`CropAreaActive`). -pub fn artboards(data: &[u8]) -> (Vec<(String, Rect)>, Option) { - let found = artboards_read(data); +/// call them (#1068); and the index among them of the active artboard (`CropAreaActive`). `names` +/// reads their names and keeps the notes on them. +pub fn artboards(data: &[u8], names: &mut obj::Names) -> (Vec<(String, Rect)>, Option) { + let mut from_start = obj::Names::new(names.encoding); + let found = artboards_read(data, &mut from_start); if !found.0.is_empty() && found.1.is_some() { + names.absorb(from_start); return found; } // Reading can stop before the artboards (art kept in the document's data comes first), or // between them and the active artboard's entry: read their array on its own. - let Some((boards, active)) = artboard_array_read(data) else { return found }; + let mut from_array = obj::Names::new(names.encoding); + let Some((boards, active)) = artboard_array_read(data, &mut from_array) else { + names.absorb(from_start); + return found; + }; if found.0.is_empty() { + names.absorb(from_array); (boards, active) } else if boards == found.0 { // The same array: its names and rects read alike. + names.absorb(from_start); (found.0, active) } else { + names.absorb(from_start); found } } @@ -163,7 +174,7 @@ pub fn artboards(data: &[u8]) -> (Vec<(String, Rect)>, Option) { /// The index of the active artboard (`CropAreaActive`) in the `ArtboardArray` of the editing data /// `data`, as [`artboards`] reads it, from that array and the entry beside it alone. pub fn active_artboard(data: &[u8]) -> Option { - artboard_array_read(data)?.1 + artboard_array_read(data, &mut obj::Names::default())?.1 } /// Artboards as [`artboards`] gives them: names and rects in art space, and the active one's index. @@ -171,7 +182,7 @@ type ArtboardsRead = (Vec<(String, Rect)>, Option); /// The `ArtboardArray` entry of the editing data `data` ([`artboard_array`]), with the /// `CropAreaActive` entry beside it, read on their own as [`artboards_read`] reads them. -fn artboard_array_read(data: &[u8]) -> Option { +fn artboard_array_read(data: &[u8], names: &mut obj::Names) -> Option { let (array, active) = artboard_array(data)?; let mut alone = b"%_/Document :\n%_/Dictionary :\n".to_vec(); alone.extend_from_slice(array); @@ -180,7 +191,7 @@ fn artboard_array_read(data: &[u8]) -> Option { alone.extend_from_slice(active); } alone.extend_from_slice(b"\n%_; /NotRecorded ,\n%_;\n"); - Some(artboards_read(&alone)) + Some(artboards_read(&alone, names)) } /// The key that ends the `ArtboardArray` entry of the editing data. @@ -278,6 +289,26 @@ fn beside(mut defined: Vec, have: &[PatternDef]) -> (Vec (defined, renamed) } +/// The names of the objects in `art` and what they hold, masks included. +fn art_names<'a>(art: impl IntoIterator) -> Vec<&'a str> { + let mut stack: Vec<&Node> = art.into_iter().collect(); + let mut names = vec![]; + while let Some(n) = stack.pop() { + stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| &**c)); + names.extend(n.name.as_deref()); + } + names +} + +/// The names read for the patterns `added` that the patterns don't show as their own: those +/// `renamed` replaced, and the names of the objects of their art. +fn pattern_names_read<'a>(added: &'a [PatternDef], renamed: &'a std::collections::BTreeMap) -> Vec<&'a str> { + let shown: std::collections::HashSet<&str> = added.iter().map(|p| p.name.as_str()).collect(); + let mut names: Vec<&str> = renamed.iter().filter(|(_, new)| shown.contains(new.as_str())).map(|(old, _)| old.as_str()).collect(); + names.extend(art_names(added.iter().flat_map(|p| p.art.iter().map(|a| &**a)))); + names +} + /// `n` and what it holds painting with the patterns `renamed` renames (old to new) by their new names. fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap) { for p in vectorcraft_doc::swatches::node_paints_mut(n) { @@ -301,9 +332,10 @@ fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap) { /// layers aren't read: the swatches its PDF part has are the defaults (#4036). They come as /// [`read`] makes them, with pattern swatches only for the patterns in `patterns` and those the /// editing data defines, which are named apart from `patterns` ([`beside`]); `None` when the -/// palette is empty or reading stops before it. -pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option { - let mut r = Reader::with_names(data); +/// palette is empty or reading stops before it. `names` reads their names and keeps the notes on +/// those the swatches take. +pub fn palette(data: &[u8], patterns: &[PatternDef], names: &mut obj::Names) -> Option { + let mut r = Reader::with_names(data, names.encoding); // The palette, the gradients and the patterns it names come before the art, so an error in the // art leaves them read. let _ = r.run(); @@ -331,6 +363,15 @@ pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option images.insert(im.key.clone(), blob.clone()); } }); + let used: Vec<&str> = swatches + .iter() + .chain(groups.iter().flat_map(|g| &g.swatches)) + .map(|s| s.name.as_str()) + .chain(groups.iter().map(|g| g.name.as_str())) + .chain(added.iter().map(|p| p.name.as_str())) + .chain(pattern_names_read(&added, &renamed)) + .collect(); + names.absorb_among(std::mem::take(&mut r.names), &used); Some(PaletteSwatches { swatches, groups, patterns: added, images }) } @@ -349,11 +390,12 @@ pub struct Brushes { /// The brushes of the editing data `data`, for a file whose layers aren't read (a brush library /// has none), as [`read`] makes them, with the patterns of the editing data their art paints with, /// named apart from `patterns` ([`beside`]) as [`palette`] names them. `None` when the file has no -/// brush objects or reading stops before them. -pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option { +/// brush objects or reading stops before them. `names` reads their names and keeps the notes on +/// those it brings in. +pub fn brushes(data: &[u8], patterns: &[PatternDef], names: &mut obj::Names) -> Option { // Brush objects are plug-in objects: data without one isn't read again. memchr::memmem::find(data, b"BeginPluginObject")?; - let mut r = Reader::with_names(data); + let mut r = Reader::with_names(data, names.encoding); // The brushes come before the art, so an error in the art leaves them read. let _ = r.run(); let (mut lib, unread) = r.brush_library()?; @@ -386,6 +428,14 @@ pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option { images.insert(im.key.clone(), blob.clone()); } }); + let used: Vec<&str> = lib + .iter() + .map(|b| b.name.as_str()) + .chain(added.iter().map(|p| p.name.as_str())) + .chain(pattern_names_read(&added, &renamed)) + .chain(art_names(lib.iter().flat_map(|b| b.art()))) + .collect(); + names.absorb_among(std::mem::take(&mut r.names), &used); Some(Brushes { brushes: lib, patterns: added, images, note: (unread > 0).then(|| brush::left_out_note(unread)) }) } @@ -393,16 +443,19 @@ pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option { /// style library has none), as [`read`] makes them, with ids from 1, and the data's declared large /// canvas scale (1 when it declares none). `large_canvas`: as [`read`] takes it. The styles are /// those the list names before reading stops, and none when the data has no style list or lists no -/// style this reads. -pub fn graphic_styles(data: &[u8], large_canvas: bool) -> (Vec, f64) { +/// style this reads. `names` reads their names and keeps the notes on them. +pub fn graphic_styles(data: &[u8], large_canvas: bool, names: &mut obj::Names) -> (Vec, f64) { if crate::import::ai::find(data, b"BeginArtStyleList").is_none() { return (vec![], 1.0); } - let mut r = Reader::with_names(data); + let mut r = Reader::with_names(data, names.encoding); r.large_canvas = large_canvas.then_some(1.0); // The styles come before the art, so an error in the art leaves them read. let _ = r.run(); - (r.graphic_styles().0, r.canvas_scale) + let styles = r.graphic_styles().0; + let used: Vec<&str> = styles.iter().map(|s| s.name.as_str()).collect(); + names.absorb_among(std::mem::take(&mut r.names), &used); + (styles, r.canvas_scale) } /// The symbols of a `.ai` file's editing data, as [`symbols`] reads them. @@ -421,10 +474,10 @@ pub struct Symbols { /// instance of a symbol the data doesn't define, and with each object of their art that is linked /// to a listed graphic style linked to it by the id [`graphic_styles`] gives that style. The /// symbols are those defined before reading stops and before the first layer; `None` when there -/// are none. -pub fn symbols(data: &[u8]) -> Option { +/// are none. `names` reads their names and keeps the notes on those it brings in. +pub fn symbols(data: &[u8], names: &mut obj::Names) -> Option { crate::import::ai::find(data, b"BeginSymbol")?; - let mut r = Reader::with_names(data); + let mut r = Reader::with_names(data, names.encoding); // The symbols come before the art, so an error in the art leaves them read. let _ = r.run(); r.check_symbols(); @@ -468,14 +521,22 @@ pub fn symbols(data: &[u8]) -> Option { let reg = r.doc.symbol_registration(&s.name); (s, reg) }) + .collect::>(); + let used: Vec<&str> = symbols + .iter() + .map(|(s, _)| s.name.as_str()) + .chain(swatches.iter().map(|s| s.name.as_str())) + .chain(art_names(symbols.iter().map(|(s, _)| &*s.art))) .collect(); + names.absorb_among(std::mem::take(&mut r.names), &used); Some(Symbols { symbols, images, swatches }) } /// [`artboards`] as far as reading `data` from its start gets: those read before an error, and /// those of a `/Document` still open when it stopped (its art can be what stopped it). -fn artboards_read(data: &[u8]) -> (Vec<(String, Rect)>, Option) { +fn artboards_read(data: &[u8], names: &mut obj::Names) -> (Vec<(String, Rect)>, Option) { let mut r = Reader::new(data); + r.names = obj::Names::new(names.encoding); let _ = r.run(); let open: Vec = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect(); for o in &open { @@ -484,6 +545,8 @@ fn artboards_read(data: &[u8]) -> (Vec<(String, Rect)>, Option) { } } let active = r.active_board.filter(|i| *i < r.artboards.len()); + let used: Vec<&str> = r.artboards.iter().map(|b| b.name.as_str()).collect(); + names.absorb_among(std::mem::take(&mut r.names), &used); (r.artboards.into_iter().map(|b| (b.name, b.rect)).collect(), active) } @@ -535,20 +598,27 @@ impl VariableRead { } /// The variable library of the editing data `data` ([`VariableRead`]), for a file whose layers -/// [`read`] can't read. Data without a `variableSets` element isn't read. -pub fn variables(data: &[u8]) -> VariableRead { +/// [`read`] can't read. Data without a `variableSets` element isn't read. `names` reads the +/// library's names and keeps the notes on them. +pub fn variables(data: &[u8], names: &mut obj::Names) -> VariableRead { if crate::import::ai::find(data, b"variableSets").is_none() { return VariableRead::default(); } let mut r = Reader::new(data); + r.names = obj::Names::new(names.encoding); let _ = r.run(); if r.variables.present { + absorb_library_names(&r.variables, r.names, names); return r.variables; } // A `/Document` still open when reading stopped. let open: Vec = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect(); - if let Some(found) = open.iter().filter(|o| o.ty == "Document").map(variable_library).find(|v| v.present) { - return found; + for o in open.iter().filter(|o| o.ty == "Document") { + let found = variable_library(o, &mut r.names); + if found.present { + absorb_library_names(&found, r.names, names); + return found; + } } // Reading can stop before the library: read its node on its own. variable_sets_node(data).map_or(r.variables, |node| { @@ -556,11 +626,20 @@ pub fn variables(data: &[u8]) -> VariableRead { alone.extend_from_slice(node); alone.extend_from_slice(b"\n%_; (xmlnode-children) ,\n%_; /Recorded ,\n%_;\n"); let mut r = Reader::new(&alone); + r.names = obj::Names::new(names.encoding); let _ = r.run(); + absorb_library_names(&r.variables, r.names, names); r.variables }) } +/// Take into `names` the notes of `read` on the names of the variables and data sets of `library`. +fn absorb_library_names(library: &VariableRead, read: obj::Names, names: &mut obj::Names) { + let used: Vec<&str> = + library.variables.variables.iter().map(|v| v.name.as_str()).chain(library.variables.datasets.iter().map(|d| d.name.as_str())).collect(); + names.absorb_among(read, &used); +} + impl<'a> Reader<'a> { /// Take the author and custom colors that the header of a program gives (`outer`, from /// [`Self::header_of`]), where the editing data's own header gives none: the program of an EPS @@ -569,6 +648,7 @@ impl<'a> Reader<'a> { if self.author.is_none() { self.author = outer.author; } + self.names.absorb(outer.names); for n in outer.custom_colors { if self.custom_colors.len() < super::directives::MAX_CUSTOM_COLORS && !self.custom_colors.iter().any(|c| c.name == n.name) { self.custom_colors.push(n); @@ -577,8 +657,8 @@ impl<'a> Reader<'a> { } /// A reader that has read the header of the program `ps` (its comment lines up to - /// `%%EndComments`). - pub(super) fn header_of(ps: &[u8]) -> Reader<'_> { + /// `%%EndComments`), its names that aren't UTF-8 read in `names`. + pub(super) fn header_of(ps: &[u8], names: NameEncoding) -> Reader<'_> { let mut end = 0; for line in ps.split_inclusive(|&b| b == b'\n' || b == b'\r') { if !line.starts_with(b"%") && !line.trim_ascii().is_empty() { @@ -590,6 +670,7 @@ impl<'a> Reader<'a> { } } let mut outer = Reader::new(ps.get(..end).unwrap_or_default()); + outer.names = obj::Names::new(names); let _ = outer.run(); outer } @@ -732,10 +813,10 @@ impl<'a> Reader<'a> { { super::remember_xml_id(&mut self.xml_ids, &mut self.xml_id_bytes, uid, id); } - let name = match o.text("AIArtName") { + let name = match self.names.entry(o, "AIArtName") { Some(name) => name, - None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) { - Some(id) => obj::xml_name(&id), + None => match o.obj("AI10_ArtUID").and_then(|id| self.names.xml_id_name(id)) { + Some(name) => name, None => return, }, }; @@ -753,7 +834,7 @@ impl<'a> Reader<'a> { /// Effects Settings, its variable library (the first one read), and its Objects on Path records. pub(super) fn document(&mut self, o: &Obj) { if !self.variables.present { - self.variables = variable_library(o); + self.variables = variable_library(o, &mut self.names); } self.constraint_data(o); if let Some(r) = find_entry(o, "AI Auto Rasterize", 0) { @@ -769,7 +850,7 @@ impl<'a> Reader<'a> { if !sane(r) || r.area() <= 0.0 { continue; } - let name = ab.text("Name").unwrap_or_else(|| format!("Artboard {}", i + 1)); + let name = self.names.entry(ab, "Name").unwrap_or_else(|| format!("Artboard {}", i + 1)); let mut board = AiArtboard::new(name, r); display_options(ab, &mut board); if active == Some(i as f64) { @@ -1057,7 +1138,7 @@ impl<'a> Reader<'a> { let mut styles: Vec = vec![]; let mut ids = HashMap::new(); for raw in &self.style_list { - let name = obj::text(raw); + let name = self.names.quiet(raw); if name.is_empty() || ids.contains_key(&name) { continue; } @@ -1242,6 +1323,7 @@ impl<'a> Reader<'a> { if !doc.symbols.is_empty() { doc.limit_depth_noting(&mut warnings); } + warnings.extend(self.names.notes()); Ok(Structure { doc, to_doc, @@ -1399,9 +1481,57 @@ const MAX_LIBRARY_BYTES: usize = 8 << 20; /// variable holds its value: `p` elements of text, one paragraph each; a `/Bool` text, whether its /// objects show; a linked file's URL (`file://`) or path. Graph values aren't read. What is /// malformed, repeated or past the caps (a declaration, a data set, a value, a setting, a second -/// `variableSet`) is left out and counted; a library past [`MAX_LIBRARY_NODES`] isn't read. -fn variable_library(o: &Obj) -> VariableRead { - find_variable_sets(o, 0).map_or_else(VariableRead::default, read_variable_sets) +/// `variableSet`) is left out and counted; a library past [`MAX_LIBRARY_NODES`] isn't read. Text +/// that isn't UTF-8 is read in the encoding of `names`, which gets a note on each such name of a +/// variable or a data set. +fn variable_library(o: &Obj, names: &mut obj::Names) -> VariableRead { + let Some(sets) = find_variable_sets(o, 0) else { return VariableRead::default() }; + let mut legacy = HashMap::new(); + let read = read_variable_sets(&library_in_utf8(sets, names.encoding, &mut legacy, 0)); + for name in read.variables.variables.iter().map(|v| &v.name).chain(read.variables.datasets.iter().map(|d| &d.name)) { + if let Some(replaced) = legacy.get(name) { + names.note(name, *replaced); + } + } + read +} + +/// The XML nodes of a variable library with their text (a `/String` or `/UnicodeString` entry, whose +/// strings a long one is written across are joined first: a string may end inside a character) in +/// UTF-8, so that an element named after a variable and that variable's name are alike whatever +/// the encoding. `legacy` gets the text that wasn't UTF-8, trimmed as [`library_name`] trims it, and +/// whether parts of it weren't valid in the encoding either. +fn library_in_utf8(o: &Obj, encoding: NameEncoding, legacy: &mut HashMap, depth: usize) -> Obj { + let mut out = o.clone(); + if depth > MAX_DEPTH { + return out; + } + for (_, values) in &mut out.entries { + match values.split_last_mut() { + Some((V::Name(ty), strings)) if matches!(ty.as_str(), "String" | "UnicodeString") && strings.iter().all(|v| matches!(v, V::Str(_))) => { + let bytes: Vec = strings.iter().flat_map(|v| if let V::Str(s) = v { s.as_slice() } else { &[] }).copied().collect(); + let (text, not_utf8, replaced) = encoding.decoded(&bytes); + if not_utf8 { + legacy.insert(text.trim().to_string(), replaced); + } + let ty = V::Name(ty.clone()); + *values = vec![V::Str(text.into_bytes()), ty]; + } + _ => { + for v in values.iter_mut() { + if let V::Obj(child) = v { + *child = std::rc::Rc::new(library_in_utf8(child, encoding, legacy, depth + 1)); + } + } + } + } + } + for v in &mut out.content { + if let V::Obj(child) = v { + *child = std::rc::Rc::new(library_in_utf8(child, encoding, legacy, depth + 1)); + } + } + out } /// The `variableSets` XML node in `o` or the dictionaries it holds. diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 81a76a31c..7605f3453 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -169,7 +169,7 @@ impl<'a> Reader<'a> { } if let Some(d) = self.gradient_def.as_mut() { match w { - "Bs" => d.stop(&vals, &mut self.stop_spots), + "Bs" => d.stop(&vals, &mut self.stop_spots, &mut self.names), "BD" => { if let Some((name, g)) = self.gradient_def.take().and_then(GradientDef::finish) { self.global_names.define(Global::Gradient, &name); @@ -227,7 +227,7 @@ impl<'a> Reader<'a> { "Lb" => self.layer_attrs(&vals), "Ln" => { if let Some(Frame { kind: Kind::Layer(a), .. }) = self.frames.last_mut() { - a.name = vals.iter().rev().find_map(V::text).unwrap_or_default(); + a.name = vals.iter().rev().find_map(|v| self.names.text(v)).unwrap_or_default(); } } "LB" => self.layer_end()?, @@ -319,12 +319,12 @@ impl<'a> Reader<'a> { "A" => self.gs.locked = Self::nums(&vals).last() == Some(&1.0), "XW" => self.state_marker(&vals, hidden), // Gradients. - "Bd" => self.gradient_def = GradientDef::begin(&vals), + "Bd" => self.gradient_def = GradientDef::begin(&vals, &mut self.names), "Bs" | "BD" => {} "Bb" => self.instance = Some(Instance::begin(&vals)), "Bg" => { if let Some(i) = self.instance.as_mut() { - i.bg(&vals); + i.bg(&vals, &mut self.names); } } "Bm" => { @@ -479,7 +479,7 @@ impl<'a> Reader<'a> { if self.frames.is_empty() { return; } - let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None }); + let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| self.names.text(v)); let Some(tile) = name.as_deref().and_then(|n| self.pattern_tiles.get(n)).copied() else { self.unread_pattern(w); return; @@ -523,7 +523,7 @@ impl<'a> Reader<'a> { } fn color(&mut self, w: &str, vals: &[V]) { - let Some((p, named)) = paint::color_op(w, vals) else { return }; + let Some((p, named)) = paint::color_op_in(w, vals, &mut self.names) else { return }; // Registration (`Xs`, `Xz`) says nothing of the document's colour model. let registration = matches!(w, "Xs" | "XS" | "Xz" | "XZ") || p.is_registration(); match p.color().filter(|_| !registration) { @@ -706,7 +706,7 @@ impl<'a> Reader<'a> { /// follows an object. fn style_marker(&mut self, vals: &[V], hidden: bool) { let code = Self::nums(vals).last().copied().unwrap_or(0.0); - let style = vals.iter().rev().find_map(V::text).unwrap_or_default(); + let style = vals.iter().rev().find_map(|v| self.names.text(v)).unwrap_or_default(); let after = std::mem::take(&mut self.after_object); // An object's own state block ends `6 () XW` (a layer's `7 () XW`): an `Xy` before it isn't the layer's. if code == 6.0 { diff --git a/crates/eps/src/import/ai/style.rs b/crates/eps/src/import/ai/style.rs index 603cee2b2..9e2bfbaa8 100644 --- a/crates/eps/src/import/ai/style.rs +++ b/crates/eps/src/import/ai/style.rs @@ -60,7 +60,7 @@ use vectorcraft_effects::is_pathfinder; use super::brush::PathBrush; use super::lex::{Lexer, Tok}; -use super::obj::{self, V}; +use super::obj::{self, Names, V}; use super::paint; const GLOW: &str = "Adobe Outer Glow"; // brand-ok: the filter's name in the files @@ -386,8 +386,14 @@ fn closes(data: &[u8]) -> bool { false } -/// The styles of the section `data` (between its markers), by name. +/// [`art_styles_in`], names read in the default encoding. pub(super) fn art_styles(data: &[u8]) -> HashMap { + art_styles_in(data, &mut Names::default()) +} + +/// The styles of the section `data` (between its markers), by name: the names of the styles and of +/// the colours, gradients and patterns they paint with read by `names`. +pub(super) fn art_styles_in(data: &[u8], names: &mut Names) -> HashMap { let mut styles = HashMap::new(); let mut name: Option = None; let mut filters: Vec = vec![]; @@ -443,7 +449,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap { } b"FillStyle" => style_entry = Some(Kind::Fill), b"StrokeStyle" | b"BlendStyle" => style_entry = Some(Kind::Other), - b"Name" if name.is_none() && filters.is_empty() => name = operands.iter().rev().find_map(V::text), + b"Name" if name.is_none() && filters.is_empty() => name = operands.iter().rev().find_map(|v| names.text(v)), b"All" | b"[Registration]" | b"Registration" => operands.push(V::Name(obj::text(n))), b"CompoundFilter" => pending = Some(true), b"BasicFilter" => pending = Some(false), @@ -470,7 +476,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap { } b"Array" => dict_array = Some(vec![]), b"PluginObject" if in_basic => { - let mut texts = operands.iter().rev().filter_map(V::text); + let mut texts = operands.iter().rev().filter_map(|v| names.text(v)); if let (Some(plugin), Some(name), Some(f)) = (texts.next(), texts.next(), filters.last_mut()) { f.plugin_object = Some((name, plugin)); } @@ -541,7 +547,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap { } } if let Some(f) = filters.last_mut() { - paint_op(f, op, &operands); + paint_op(f, op, &operands, names); } if op == "," || op == ";" || op.starts_with('X') || op.starts_with('B') || op.len() <= 2 { operands.clear(); @@ -614,13 +620,13 @@ fn block_data(lex: &mut Lexer, encoded: &[u8]) -> Option> { None } -/// A paint operator of a fill's or stroke's style. -fn paint_op(f: &mut Filter, op: &str, vals: &[V]) { +/// A paint operator of a fill's or stroke's style, the names it gives read by `names`. +fn paint_op(f: &mut Filter, op: &str, vals: &[V], names: &mut Names) { let last = vals.iter().rev().find_map(V::num); match op { "Xa" | "k" | "g" | "x" | "Xx" | "Xk" | "Xs" | "Xz" | "XA" | "K" | "G" | "X" | "XX" | "XK" | "XS" | "XZ" | "xa" | "xx" | "xk" | "xs" | "xz" => { - let Some((p, _)) = paint::color_op(op, vals) else { return }; + let Some((p, _)) = paint::color_op_in(op, vals, names) else { return }; if op.ends_with(|c: char| c.is_ascii_lowercase()) { f.fill = Some(p); } else { @@ -681,13 +687,13 @@ fn paint_op(f: &mut Filter, op: &str, vals: &[V]) { "Bg" => { f.gradient = true; let at = vals.iter().rposition(|v| matches!(v, V::Str(_))); - let name = at.and_then(|i| vals.get(i)).and_then(V::text); + let name = at.and_then(|i| vals.get(i)).and_then(|v| names.text(v)); let n: Vec = at.map_or(&[][..], |i| vals.get(i + 1..).unwrap_or_default()).iter().filter_map(V::num).collect(); let angle = n.get(2).copied().filter(|a| a.is_finite()).unwrap_or(0.0); f.gradient_of = Some((name, angle)); } "p" => { - let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None }); + let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v)); let matrix = vals.iter().rev().find_map(|v| match v { V::Arr(a) => { let n: Vec = a.iter().filter_map(V::num).collect(); diff --git a/crates/eps/src/import/ai/style/tests.rs b/crates/eps/src/import/ai/style/tests.rs index 929006856..2222eb0cc 100644 --- a/crates/eps/src/import/ai/style/tests.rs +++ b/crates/eps/src/import/ai/style/tests.rs @@ -1,6 +1,10 @@ use super::*; use vectorcraft_color::Color; +fn paint_op(f: &mut Filter, op: &str, vals: &[V]) { + super::paint_op(f, op, vals, &mut Names::default()); +} + #[test] fn a_variable_width_stroke_keeps_its_profile() { let data = b"/KnownStyle :\n(Tapered) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\ diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 9ba450cdb..14064cec5 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -783,7 +783,7 @@ fn pattern_names_are_shown_up_to_their_newline() { // Nor the name of a custom color the program of an EPS gives, or None's. let ps = b"%!PS-Adobe-3.0\n%%CMYKCustomColor: 0 0 1 0 (Outer Ink)\n%%EndComments\n"; let data = stream(&format!("{}{}{}", def("Outer Ink\\n1", 8), def("[None]\\n1", 8), layer(&filled("Outer Ink\\n1")))); - let doc = super::read_beside(data.as_bytes(), true, ps, false).unwrap().doc; + let doc = super::read_beside(data.as_bytes(), true, ps, false, NameEncoding::default()).unwrap().doc; assert_eq!(doc.patterns.iter().map(|p| p.name.as_str()).collect::>(), ["Outer Ink 2", "[None] 2"]); assert!(matches!(doc.swatch("Outer Ink").unwrap().paint, Paint::Solid { .. })); assert!(matches!(fill(&art(&doc)[0]), Paint::Pattern { pattern, .. } if pattern == "Outer Ink 2")); @@ -2510,6 +2510,7 @@ fn ai_variable_library_without_the_layers() { let library = variable_library_data(&sample_variable_sets("variableAndTypes", &[])); let unread = stream(&format!("{library}{}", layer("1 Zz\n"))); assert!(read(unread.as_bytes()).is_err()); + let variables = |data: &[u8]| variables(data, &mut Names::default()); let found = variables(unread.as_bytes()); assert!(found.present && found.variables.locked && found.skipped == 0); assert_eq!((found.variables.variables.len(), found.variables.datasets.len()), (5, 3)); @@ -5724,7 +5725,7 @@ fn artboards_after_what_stops_the_reading() { let array = format!("%_/Array :\n{}{}%_; (ArtboardArray) ,\n", board("one", 0), board("two", 200)); // Art in the document's own data that the reader can't follow comes before the artboards. let data = format!("%_/Document :\n%_/Dictionary :\n%_/Array :\n%_/Dictionary :\nLB\n{array}%_; /NotRecorded ,\n%_;\n"); - let boards: Vec<_> = super::artboards(data.as_bytes()).0.into_iter().map(|(n, r)| (n, r.x0)).collect(); + let boards: Vec<_> = super::artboards(data.as_bytes(), &mut Names::default()).0.into_iter().map(|(n, r)| (n, r.x0)).collect(); assert_eq!(boards, [("one".to_string(), 0.0), ("two".to_string(), 200.0)]); } @@ -6348,7 +6349,7 @@ fn a_symbol_library_without_layers_gives_its_symbols() { ); let list = "%AI24_BeginSymbolList\n(Tile)\n(Pair)\n(Lost)\n(Dot)\n%AI24_EndSymbolList\n"; let body = format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{styles}{defs}{list}"); - let read = super::symbols(stream(&body).as_bytes()).expect("the library's symbols"); + let read = super::symbols(stream(&body).as_bytes(), &mut Names::default()).expect("the library's symbols"); let names: Vec<&str> = read.symbols.iter().map(|(s, _)| s.name.as_str()).collect(); assert_eq!(names, ["Tile", "Pair", "Dot", "Pic"]); let registrations: Vec = read.symbols.iter().map(|(_, r)| r.0).collect(); @@ -6365,7 +6366,7 @@ fn a_symbol_library_without_layers_gives_its_symbols() { assert_eq!(keys.len(), 1); assert_eq!(read.images.keys().cloned().collect::>(), keys); // Without symbols there are none to give. - assert!(super::symbols(stream(&styles).as_bytes()).is_none()); + assert!(super::symbols(stream(&styles).as_bytes(), &mut Names::default()).is_none()); } #[test] @@ -10228,3 +10229,258 @@ fn copies_of_a_multi_layer_mask_count_toward_the_limit() { let e = read(mask_stream7(&filled).as_bytes()).unwrap_err(); assert!(e.contains("objects"), "{e}"); } + +// ---- names that aren't UTF-8 ---------------------------------------------------------------- + +/// `body` read with its names that aren't UTF-8 in `encoding`. +fn read_names(body: &str, encoding: &str) -> Structure { + let encoding = NameEncoding::from_label(encoding).unwrap(); + match super::read_in(stream(body).as_bytes(), true, encoding) { + Ok(s) => s, + Err(e) => panic!("{e}"), + } +} + +/// The notes on names that weren't UTF-8. +fn name_notes(s: &Structure) -> Vec<&str> { + s.warnings.iter().map(String::as_str).filter(|w| w.contains("isn't UTF-8") || w.contains("aren't UTF-8")).collect() +} + +/// A layer's name in a legacy code page (`(name) Ln`, the bytes as octal escapes) is read in the +/// encoding picked, with a note that names it and the encoding; one in UTF-8 is read as it is in +/// every encoding, without a note. +#[test] +fn layer_names_are_read_in_the_legacy_encoding_picked() { + let layer_named = |name: &str| { + format!( + "%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{RED}{}f\nLB\n%AI5_EndLayer--\n", + rect_path(0.0, 0.0, 10.0, 10.0) + ) + }; + for (encoding, bytes, want) in [ + ("windows-949", r"\267\271\300\314\276\356 1", "레이어 1"), + ("shift_jis", r"\203\214\203C\203\204\201[ 1", "レイヤー 1"), + ("gbk", r"\315\274\262\343 1", "图层 1"), + ("big5", r"\271\317\274h 1", "圖層 1"), + ("macintosh", r"Caf\216", "Café"), + ("windows-1252", r"Caf\351", "Café"), + ] { + let s = read_names(&layer_named(bytes), encoding); + assert_eq!(s.doc.layers[0].name.as_deref(), Some(want), "{encoding}"); + assert_eq!( + name_notes(&s), + [format!("the name `{want}` isn't UTF-8 and was read as {encoding}: if it looks wrong, open the file with another legacyNameEncoding")], + "{encoding}" + ); + let utf8 = read_names(&layer_named(r"\353\240\210\354\235\264\354\226\264 1"), encoding); + assert_eq!(utf8.doc.layers[0].name.as_deref(), Some("레이어 1"), "{encoding}"); + assert!(name_notes(&utf8).is_empty(), "{encoding}: {:?}", utf8.warnings); + } + // By default the Korean bytes come in as Windows-1252, as before, now with the note. A comment + // in UTF-8 doesn't change how the names are read. + let s = read_names(&format!("%%Title: 레이어\n{}", layer_named(r"\267\271\300\314\276\356 1")), "windows-1252"); + assert_eq!(s.doc.layers[0].name.as_deref(), Some("·¹À̾î 1")); + assert_eq!( + name_notes(&s), + ["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"] + ); + // Bytes the encoding doesn't define come in as U+FFFD, and the note says so. + let s = read_names(&layer_named(r"\267\271\300 1"), "windows-949"); + assert_eq!(s.doc.layers[0].name.as_deref(), Some("레\u{fffd} 1")); + assert_eq!( + name_notes(&s), + ["the name `레\u{fffd} 1` isn't UTF-8, and parts of it aren't valid windows-949 either: open the file with another legacyNameEncoding"] + ); +} + +/// An object's name, an artboard's, a spot colour's and a pattern's are read in the encoding +/// picked too, and a fill names the pattern as its definition does, whichever encoding is picked. +#[test] +fn object_artboard_colour_and_pattern_names_are_read_in_the_encoding_picked() { + // 그라 (a pattern and a group), 빨강 (an artboard and a spot colour), 레이어 1 (the layer). + let (pattern, red, layer_name) = (r"\261\327\266\363", r"\273\241\260\255", r"\267\271\300\314\276\356 1"); + let boards = format!( + "%_/Document :\n%_/Dictionary :\n%_/Array :\n\ + %_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_200 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_({red}) /UnicodeString (Name) ,\n%_; ,\n\ + %_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n" + ); + let tile = DOTS.replace("(Dots)", &format!("({pattern})")); + let art_ = format!( + "0 Ae\nu\n0 1 1 0 ({red}) 0 x\n{}f\n({pattern}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\nU\n%_/ArtDictionary :\n%_({pattern}) /String (AIArtName) ,\n%_;\n9 () XW\n", + rect_path(0.0, 0.0, 10.0, 10.0), + rect_path(20.0, 0.0, 30.0, 10.0), + ); + let body = format!("{boards}{tile}%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({layer_name}) Ln\n{art_}LB\n%AI5_EndLayer--\n"); + for (encoding, [pattern, red, layer_name]) in [("windows-949", ["그라", "빨강", "레이어 1"]), ("windows-1252", ["±×¶ó", "»¡°\u{ad}", "·¹À̾î 1"])] + { + let s = read_names(&body, encoding); + let doc = &s.doc; + assert_eq!(doc.layers[0].name.as_deref(), Some(layer_name), "{encoding}"); + assert_eq!(doc.artboards.first().map(|a| a.name.as_str()), Some(red), "{encoding}"); + let group = &art(doc)[0]; + assert_eq!(group.name.as_deref(), Some(pattern), "{encoding}"); + let kids = group.children().unwrap(); + assert!(matches!(fill(&kids[0]), Paint::Solid { swatch: Some(s), .. } if s == red), "{encoding}: {:?}", fill(&kids[0])); + assert!(doc.swatch(red).is_some_and(|s| s.spot), "{encoding}"); + assert!(matches!(fill(&kids[1]), Paint::Pattern { pattern: p, .. } if p == pattern), "{encoding}: {:?}", fill(&kids[1])); + assert!(doc.pattern(pattern).is_some(), "{encoding}"); + assert!(s.unread.is_empty(), "{encoding}: {:?}", s.unread); + assert_eq!(name_notes(&s).len(), 3, "{encoding}: {:?}", s.warnings); + } +} + +/// Past three names, the notes count the others in one. +#[test] +fn notes_on_names_that_arent_utf8_are_capped() { + let layers: String = (0..6) + .map(|i| { + format!( + "%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n(L{i} Caf\\216) Ln\n{RED}{}f\nLB\n%AI5_EndLayer--\n", + rect_path(0.0, 0.0, 10.0, 10.0) + ) + }) + .collect(); + let s = read_names(&layers, "macintosh"); + let names: Vec<_> = s.doc.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect(); + assert!(names.iter().all(|n| n.ends_with(" Café")), "{names:?}"); + let notes = name_notes(&s); + assert_eq!(notes.len(), 4, "{notes:?}"); + assert_eq!(notes[3], "more names that aren't UTF-8 were read as macintosh (3)"); +} + +/// 빨강 and 그라 in code page 949, as octal escapes. +const KO_RED: &str = r"\273\241\260\255"; +const KO_GRA: &str = r"\261\327\266\363"; + +/// A brush's name, the art it names and the brush manager's order are read in the encoding picked: +/// the brush finds its art and the order finds the brush, with a note on the brush and on its art. +#[test] +fn brush_names_are_read_in_the_encoding_picked() { + let params = SPRAY.replace("Dot art", KO_GRA); + let scatter = format!("%AI8_BeginPluginObject\n({SCATTER})\n({KO_RED})\n({params}) .\n%AI8_EndPluginObject\n"); + let manager = brush::MANAGER; + let order = format!("%AI8_BeginPluginObject\n({manager})\n({manager})\n( {SCATTER}/ {KO_RED}/) .\n%AI8_EndPluginObject\n"); + let body = format!("{}{order}{scatter}{}", brush_art(KO_GRA, 4.0), layer("")); + for (encoding, name) in [("windows-949", "빨강"), ("windows-1252", "»¡°\u{ad}")] { + let s = read_names(&body, encoding); + let lib = vectorcraft_brush::library(&s.doc); + assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::>(), [name], "{encoding}: {:?}", s.warnings); + assert!(matches!(brush_named(&lib, name), vectorcraft_brush::BrushKind::Scatter(_)), "{encoding}"); + assert_eq!(name_notes(&s).len(), 2, "{encoding}: {:?}", s.warnings); + } +} + +/// A symbol's name, the instances that name it and the symbol list are read in the encoding +/// picked, with a note on each symbol. +#[test] +fn symbol_names_are_read_in_the_encoding_picked() { + let list = format!("%AI24_BeginSymbolList\n({KO_GRA})\n({KO_RED})\n%AI24_EndSymbolList\n"); + let defs = symbols(&format!("{}{}{list}", symbol(KO_RED, &dot(), 5), symbol(KO_GRA, &dot(), 5))); + let s = read_names(&(defs + &layer(&instance(KO_RED, [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))), "windows-949"); + let names: Vec<&str> = s.doc.symbols.iter().map(|d| d.name.as_str()).collect(); + assert_eq!(names, ["그라", "빨강"], "{:?}", s.warnings); + assert!(matches!(&art(&s.doc)[0].kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "빨강"), "{:?}", art(&s.doc)[0].kind); + assert_eq!(name_notes(&s).len(), 2, "{:?}", s.warnings); +} + +/// A symbol library read without its layers has notes on the names of the objects in its art. +#[test] +fn a_symbol_librarys_art_has_notes_on_its_object_names() { + let inner = format!("{}{}", dot(), vectorcraft_testkit::ai::named(KO_GRA)); + let data = stream(&symbols(&symbol(KO_RED, &inner, 5))); + let mut names = obj::Names::new(NameEncoding::Korean); + assert!(super::symbols(data.as_bytes(), &mut names).is_some()); + let notes = names.notes(); + assert_eq!(notes.len(), 2, "{notes:?}"); + assert!(notes.iter().any(|n| n.contains("`그라`")), "{notes:?}"); +} + +/// An object without a name of its own takes the one its XML id gives it, read in the encoding +/// picked, with a note on that name. +#[test] +fn an_object_named_by_its_xml_id_is_read_in_the_encoding_picked() { + let square = format!("{RED}{}f\n{}", rect_path(0.0, 0.0, 10.0, 10.0), vectorcraft_testkit::ai::named(&format!("{KO_RED}_1"))); + let s = read_names(&layer(&square), "windows-949"); + assert_eq!(art(&s.doc)[0].name.as_deref(), Some("빨강 1"), "{:?}", s.warnings); + let notes = name_notes(&s); + assert_eq!(notes.len(), 1, "{notes:?}"); + assert!(notes[0].contains("`빨강 1`"), "{notes:?}"); +} + +/// The strings of a plug-in object that isn't a brush, and the name of a brush that isn't read, +/// have no notes. +#[test] +fn plug_in_objects_that_are_not_read_have_no_name_notes() { + let other = format!("%AI8_BeginPluginObject\n(Other Tool)\n({KO_RED})\n(x) .\n%AI8_EndPluginObject\n"); + let broken = format!("%AI8_BeginPluginObject\n({SCATTER})\n({KO_GRA})\n(not a brush) .\n%AI8_EndPluginObject\n"); + let s = read_names(&format!("{other}{broken}{}", layer("")), "windows-949"); + assert!(name_notes(&s).is_empty(), "{:?}", s.warnings); +} + +/// A graphic style's name, in the art styles and in the style list, and an object's link to it are +/// read in the encoding picked, with a note on the style. +#[test] +fn graphic_style_names_are_read_in_the_encoding_picked() { + let defs = [fill_style(KO_RED, "0 0 0 0 1 0 0 Xa\n")]; + let s = read_names(&format!("{}{}", styles_and_list(&defs, &[KO_RED]), layer(&styled_square(KO_RED))), "windows-949"); + assert_eq!(style_names(&s.doc), [("빨강".to_string(), 1)], "{:?}", s.warnings); + assert_eq!(art(&s.doc)[0].extras().graphic_style, Some(1), "{:?}", s.warnings); + assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings); +} + +/// A variable's and a data set's names are read in the encoding picked, each with a note, a name +/// written across two strings that end inside a character included. The element named after the +/// variable still names it, and the data set's text is read in the encoding too, without a note. +#[test] +fn variable_and_data_set_names_are_read_in_the_encoding_picked() { + let split_red = "\\273)\n%_(\\241\\260\\255"; + let library = vectorcraft_testkit::ai::variable_library(&["QVARQ"], &["QSETQ"]).replace("QVARQ", split_red).replace("QSETQ", KO_GRA); + let s = read_names(&format!("{library}{}", layer(&format!("{RED}{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)))), "windows-949"); + let vars = &s.doc.variables; + assert_eq!(vars.variables.iter().map(|v| v.name.as_str()).collect::>(), ["빨강"], "{:?}", s.warnings); + assert_eq!(vars.datasets.iter().map(|d| d.name.as_str()).collect::>(), ["그라"], "{:?}", s.warnings); + assert_eq!(vars.dataset("그라").and_then(|d| d.values.get("빨강")), Some(&vectorcraft_doc::DataValue::Text("그라 빨강".into()))); + assert_eq!(name_notes(&s).len(), 2, "{:?}", s.warnings); +} + +/// A binding names its variable by the name read in the encoding picked. +#[test] +fn a_bound_variables_name_is_read_in_the_encoding_picked() { + use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node}; + let attributes = + [("trait", XmlValue::Str("textcontent")), ("varName", XmlValue::Unicode("QVARQ")), ("docRef", XmlValue::Raw("/XMLUIDREF : (one) ;"))]; + let variable = xml_node(1, "variable", XmlValue::Str(""), &attributes, &[]); + let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &[variable])]); + let library = variable_library_data(&xml_element("variableSets", &[], &[set])).replace("QVARQ", KO_RED); + let square = layer(&format!("{RED}{}f\n{}", rect_path(0.0, 0.0, 10.0, 10.0), named("one"))); + let s = read_names(&format!("{library}{square}"), "windows-949"); + assert_eq!(s.bindings, [BindingRef { variable: "빨강".into(), target: Some("one".into()) }], "{:?}", s.warnings); + assert_eq!(s.doc.variables.variables.iter().map(|v| v.name.as_str()).collect::>(), ["빨강"]); + assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings); +} + +/// A custom color of the header is read in the encoding picked, so it is the color an object +/// names: one swatch comes in, with a note. +#[test] +fn a_custom_color_of_the_header_is_read_in_the_encoding_picked() { + let body = format!( + "%!PS-Adobe-3.0\n%%CMYKCustomColor: 0 1 1 0 ({KO_RED})\n%%EndComments\n{}", + layer(&format!("0 1 1 0 ({KO_RED}) 0 x\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0))) + ); + let s = super::read_in(body.as_bytes(), true, NameEncoding::Korean).unwrap(); + assert_eq!(s.doc.swatches.iter().filter(|w| w.spot).map(|w| w.name.as_str()).collect::>(), ["빨강"], "{:?}", s.warnings); + assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings); +} + +/// A pattern's name in the version 7 form is read in the encoding picked, with a note, and a fill +/// that names it finds it. +#[test] +fn a_version_7_pattern_name_is_read_in_the_encoding_picked() { + let layers = "(0 g) @ (2 2 m 9 2 L 9 9 L f) &\n"; + let fill_it = format!("({KO_RED}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)); + let stream = pattern_stream7(&pattern7(KO_RED, layers), &pattern_layer7("Art", &fill_it)); + let s = super::read_in(stream.as_bytes(), true, NameEncoding::Korean).unwrap(); + assert!(s.doc.pattern("빨강").is_some(), "{:?}", s.doc.patterns.iter().map(|p| &p.name).collect::>()); + assert!(matches!(fill(&art(&s.doc)[0]), Paint::Pattern { pattern, .. } if pattern == "빨강"), "{:?}", s.warnings); + assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings); +} diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index a7858e46b..eb5e815e8 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -54,7 +54,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::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, layered_ai, layered_ai_from}; +pub use layers::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, layered_ai, layered_ai_from}; pub(super) use layers::{layered, program_alone}; pub(crate) use slots::MAX_TEXT; pub use slots::is_loss; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 9400bd56e..e0d00ac82 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -49,7 +49,7 @@ pub(super) struct Frame<'a> { /// (a `.ai` saved without its PDF part), its type is made from the file's text document alone and /// nothing is compared. `outlined`: the page's type was read as outlines, so it has none to put in /// the text objects. `unit`: the size in points of a unit of the page's user space (see -/// [`layered_ai`]). +/// [`layered_ai`]). `names`: the encoding of names that aren't UTF-8. pub(super) fn build( data: &[u8], visible: Option<&Document>, @@ -57,11 +57,12 @@ pub(super) fn build( outlined: bool, trust: bool, unit: f64, + names: ai::NameEncoding, ) -> Result<(Document, Vec), String> { // An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art // space, as its PDF part may (#2031). let large_canvas = matches!(page, Page::Art); - let structure = if visible.is_some() { ai::read_over_page(data, large_canvas)? } else { ai::read(data, large_canvas)? }; + let structure = if visible.is_some() { ai::read_over_page(data, large_canvas, names)? } else { ai::read_in(data, large_canvas, names)? }; build_from(structure, data, visible, page, outlined, trust, unit) } @@ -69,9 +70,10 @@ pub(super) fn build( /// (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; else as many times as -/// large as its declared large canvas scale when it has the page's size at that scale. -pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64) -> Result<(Document, Vec), String> { - let structure = ai::read(data, false)?; +/// large as its declared large canvas scale when it has the page's size at that scale. `names`: the +/// encoding of names that aren't UTF-8. +pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec), String> { + let structure = ai::read_in(data, false, names)?; 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; diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index 9fd54cc25..c0331574c 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -11,10 +11,12 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect /// ([`use_symbols`]) and its variable library ([`use_variables`]) when it could be read, and with /// its layer tree, empty, below the page's layer when that could be read (#2033). A program in the /// legacy format without layers (`illustrator`, see [`program_alone`]) is read with its art on one -/// layer; when that can't be used, the file is `visible` as it was. -pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool) -> Imported { +/// layer; when that can't be used, the file is `visible` as it was. `names`: the encoding of names +/// that aren't UTF-8. +pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, names: ai::NameEncoding) -> Imported { let Some(data) = editing_copy(ps) else { - let read = illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0).ok()); + let read = + illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0, names).ok()); return match read { Some((document, warnings)) => with_page_notes(document, warnings, &visible), None => visible, @@ -24,7 +26,8 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus // can't be used. // Private data kept apart from the program: the program's own header gives the author and // custom colors the private data's header may lack. - let read = |d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true) } else { ai::read_over_page(d, true) }; + let read = + |d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true, names) } else { ai::read_over_page(d, true, names) }; let (tree, result) = match data.as_deref() { Ok(d) => match read(d) { Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)), @@ -37,12 +40,14 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus Err(why) => { let mut v = visible; if let Ok(data) = &data { + let mut read = ai::Names::new(names); let have = v.document.patterns.len(); - let own_palette = use_palette(data, &mut v.document); - use_brushes(data, &mut v.document, have, &mut v.warnings); - use_styles(data, &mut v.document, true, 1.0, None); - use_symbols(data, &mut v.document, own_palette, &mut v.warnings); - use_variables(data, &mut v.document, &mut v.warnings); + let own_palette = use_palette(data, &mut v.document, &mut read); + use_brushes(data, &mut v.document, have, &mut v.warnings, &mut read); + use_styles(data, &mut v.document, true, 1.0, None, &mut read); + use_symbols(data, &mut v.document, own_palette, &mut v.warnings, &mut read); + use_variables(data, &mut v.document, &mut v.warnings, &mut read); + add_notes(&mut v.warnings, read.notes()); } if tree.is_empty() { v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, in one layer")); @@ -112,12 +117,13 @@ fn layer_tree(nodes: &[Arc]) -> Vec> { /// `unit`: the size in points of a unit of the PDF part's user space, its first page's `UserUnit`, /// by which its art came in scaled. Editing data whose artboard matches the page only in those /// units comes in `unit` times as large, and so does the artboard layout `place_pages` takes from -/// it. +/// it. Names that aren't UTF-8 are read as Windows-1252 ([`layered_ai_from`]). pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec, outlined: bool, trust: bool, unit: f64) -> (Document, Vec, bool) { - layered_ai_from(AiLayers::decode(private), visible, warnings, outlined, trust, unit) + layered_ai_from(AiLayers::decode(private), visible, warnings, outlined, trust, unit, ai::NameEncoding::default()) } -/// [`layered_ai`] of the editing copy `layers`, as far as it was already decoded and read. +/// [`layered_ai`] of the editing copy `layers`, as far as it was already decoded and read, with the +/// names that aren't UTF-8 read in `names`. pub fn layered_ai_from( layers: AiLayers, visible: Document, @@ -125,6 +131,7 @@ pub fn layered_ai_from( outlined: bool, trust: bool, unit: f64, + names: ai::NameEncoding, ) -> (Document, Vec, bool) { let unread = |mut all: Vec, why: &str| { all.push(format!("{LAYERS_UNREAD} from its editing data ({why}): they come from its PDF part")); @@ -137,34 +144,41 @@ pub fn layered_ai_from( }; if !has_layers(&data) { let (mut visible, mut warnings) = (visible, warnings); - let (boards, active) = ai::artboards(&data); + let (boards, active) = ai::artboards(&data, &mut ai::Names::default()); if board_scale(&boards, &visible, unit).is_some() { visible.opened_artboard = active_page(active, &visible); } + let mut read = ai::Names::new(names); let have = visible.patterns.len(); - let own_palette = use_palette(&data, &mut visible); - use_brushes(&data, &mut visible, have, &mut warnings); - use_styles(&data, &mut visible, false, unit, Some(&boards)); - use_symbols(&data, &mut visible, own_palette, &mut warnings); - use_variables(&data, &mut visible, &mut warnings); + let own_palette = use_palette(&data, &mut visible, &mut read); + use_brushes(&data, &mut visible, have, &mut warnings, &mut read); + use_styles(&data, &mut visible, false, unit, Some(&boards), &mut read); + use_symbols(&data, &mut visible, own_palette, &mut warnings, &mut read); + use_variables(&data, &mut visible, &mut warnings, &mut read); + add_notes(&mut warnings, read.notes()); return (visible, warnings, false); } // The PDF part, its pages placed and named as the editing data's artboards are, when the // layers can't be used (`why`). let from_pdf_part = |mut visible: Document, mut warnings: Vec, why: &str| { - let (boards, active) = ai::artboards(&data); - place_pages(&boards, active, &mut visible, unit); + let mut read = ai::Names::new(names); + let mut board_names = ai::Names::new(names); + let (boards, active) = ai::artboards(&data, &mut board_names); + if place_pages(&boards, active, &mut visible, unit) { + read.absorb(board_names); + } let have = visible.patterns.len(); - let own_palette = use_palette(&data, &mut visible); - use_brushes(&data, &mut visible, have, &mut warnings); - use_styles(&data, &mut visible, false, unit, Some(&boards)); + let own_palette = use_palette(&data, &mut visible, &mut read); + use_brushes(&data, &mut visible, have, &mut warnings, &mut read); + use_styles(&data, &mut visible, false, unit, Some(&boards), &mut read); let mut all = unread(warnings, why); - use_symbols(&data, &mut visible, own_palette, &mut all); - use_variables(&data, &mut visible, &mut all); + use_symbols(&data, &mut visible, own_palette, &mut all, &mut read); + use_variables(&data, &mut visible, &mut all, &mut read); + add_notes(&mut all, read.notes()); (visible, all, false) }; let built = structure - .unwrap_or_else(|| ai::read_over_page(&data, false).map(Box::new)) + .unwrap_or_else(|| ai::read_over_page(&data, false, names).map(Box::new)) .and_then(|structure| build_from(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit)); match built { Ok((mut done, notes)) => { @@ -183,8 +197,8 @@ pub fn layered_ai_from( /// reading it, when the file's text document makes all of its type and nothing in its layers needs /// the PDF part (see `build_without_page`). Nothing is compared with the PDF part. Else, as the /// error, what was decoded and read of the editing copy, for [`layered_ai_from`] once the PDF part -/// is imported. -pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64) -> Result<(Document, Vec), AiLayers> { +/// is imported. `names`: the encoding of names that aren't UTF-8. +pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec), AiLayers> { let data = match ai::decode_private(private) { Ok(data) => data, Err(why) => return Err(AiLayers { data: Err(why), structure: None }), @@ -193,7 +207,7 @@ pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64) -> Resul return Err(AiLayers { data: Ok(data), structure: None }); } // An image past the hard limits is read as an unread box, which the PDF part's art replaces. - let structure = match ai::read_over_page(&data, false) { + let structure = match ai::read_over_page(&data, false, names) { Ok(structure) => structure, Err(why) => return Err(AiLayers { data: Ok(data), structure: Some(Err(why)) }), }; @@ -234,9 +248,10 @@ pub fn ai_opened_artboard(private: &[u8], doc: &Document) -> Option { /// sets, active data set and lock of the variable library of its editing data `data`, as [`build`] /// gives a document read from the layers, and the note about what of the library wasn't read /// added to `warnings`: its bindings aren't, as the objects they bind aren't the layers' objects. -/// Left as it is when the editing data has no library. -fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec) { - let read = ai::variables(data); +/// Left as it is when the editing data has no library. `names` reads the library's names and keeps +/// the notes on them. +fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec, names: &mut ai::Names) { + let read = ai::variables(data, names); warnings.extend(read.note()); warnings.extend(ai::bindings_note(read.bindings.len() + read.bindings_skipped)); if read.present { @@ -250,9 +265,10 @@ fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec) { /// palette doesn't have follow the palette's swatches. The patterns that the editing data defines come /// in for the palette's pattern swatches, with the images they show; one whose name a pattern of /// the PDF part already has takes a free variant of it ([`ai::palette`]). Left as -/// it is when the palette is empty. Whether the palette's swatches came in. -fn use_palette(data: &[u8], doc: &mut Document) -> bool { - let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns) else { return false }; +/// it is when the palette is empty. Whether the palette's swatches came in. `names` reads the +/// palette's names and keeps the notes on those the swatches take. +fn use_palette(data: &[u8], doc: &mut Document, names: &mut ai::Names) -> bool { + let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns, names) else { return false }; for (key, blob) in images { doc.images.entry(key).or_insert(blob); } @@ -269,15 +285,25 @@ fn use_palette(data: &[u8], doc: &mut Document) -> bool { true } +/// `notes` added to `warnings`, those it already has left out. +fn add_notes(warnings: &mut Vec, notes: Vec) { + for n in notes { + if !warnings.contains(&n) { + warnings.push(n); + } + } +} + /// `doc` (a file read as its page or PDF part, in place of its layers) with the brushes of its /// editing data `data` in place of the default ones, as [`build`] gives a document read from the /// layers, the patterns of the editing data their art paints with (named apart from the first /// `have` patterns of `doc`, those of the page or PDF part, as [`use_palette`] names them, which /// may have added some of them already), and the note for the brushes left out added to -/// `warnings`. Left as it is when the editing data has no brushes. -fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec) { +/// `warnings`. Left as it is when the editing data has no brushes. `names` reads the brushes' names +/// and keeps the notes on them. +fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec, names: &mut ai::Names) { let page = doc.patterns.get(..have).unwrap_or(&doc.patterns); - let Some(ai::Brushes { brushes, patterns, images, note }) = ai::brushes(data, page) else { return }; + let Some(ai::Brushes { brushes, patterns, images, note }) = ai::brushes(data, page, names) else { return }; for (key, blob) in images { doc.images.entry(key).or_insert(blob); } @@ -299,9 +325,10 @@ fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec< /// the size of `doc`'s pages in units of `unit` points, else as many times as large as the data's /// declared large canvas scale when they are the pages' size at that scale ([`board_scale`]). /// `boards`: the editing data's artboards when they were already read. Left as it is when the -/// editing data has no style this reads. -fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, boards: Option<&[(String, Rect)]>) { - let (mut styles, canvas) = ai::graphic_styles(data, large_canvas); +/// editing data has no style this reads. `names` reads the styles' names and keeps the notes on +/// them. +fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, boards: Option<&[(String, Rect)]>, names: &mut ai::Names) { + let (mut styles, canvas) = ai::graphic_styles(data, large_canvas, names); if styles.is_empty() { return; } @@ -310,7 +337,7 @@ fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, bo let boards = match boards { Some(b) => b, None => { - read = ai::artboards(data).0; + read = ai::artboards(data, &mut ai::Names::default()).0; &read } }; @@ -336,12 +363,12 @@ fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, bo /// symbols' art keeps the editing data's units, as in a document read from the layers: when /// [`use_styles`] draws the styles' strokes and effects larger (a file in larger units), linked art /// isn't scaled with them. Left as it is when `doc` has symbols already or the editing data has -/// none this reads. -fn use_symbols(data: &[u8], doc: &mut Document, own_palette: bool, warnings: &mut Vec) { +/// none this reads. `names` reads the symbols' names and keeps the notes on them. +fn use_symbols(data: &[u8], doc: &mut Document, own_palette: bool, warnings: &mut Vec, names: &mut ai::Names) { if !doc.symbols.is_empty() { return; } - let Some(ai::Symbols { symbols, images, swatches }) = ai::symbols(data) else { return }; + let Some(ai::Symbols { symbols, images, swatches }) = ai::symbols(data, names) else { return }; for (key, blob) in images { doc.images.entry(key).or_insert(blob); } @@ -414,14 +441,14 @@ fn board_scale(boards: &[(String, Rect)], doc: &Document, unit: f64) -> Option, doc: &mut Document, unit: f64) { - let Some(k) = board_scale(boards, doc, unit) else { return }; +/// is drawn `unit` times as large. Whether the pages took the artboards' names. +fn place_pages(boards: &[(String, Rect)], active: Option, doc: &mut Document, unit: f64) -> bool { + let Some(k) = board_scale(boards, doc, unit) else { return false }; for ((name, _), a) in boards.iter().zip(&mut doc.artboards) { a.name.clone_from(name); } doc.opened_artboard = active_page(active, doc); - let [(_, first), _, ..] = boards else { return }; + let [(_, first), _, ..] = boards else { return true }; // Art space (y up) → the document, the first artboard's top left at the origin, as `read` has it, // `k` points a unit. let to_doc = Affine::new([k, 0.0, 0.0, -k, -first.x0 * k, first.y1 * k]); @@ -433,6 +460,7 @@ fn place_pages(boards: &[(String, Rect)], active: Option, doc: &mut Docum for (a, v) in doc.artboards.iter_mut().zip(&moves) { a.rect = a.rect + *v; } + true } /// The id of the page of `doc` (a `.ai` read as its PDF part: a page per artboard) for the artboard @@ -464,19 +492,25 @@ fn place_art(layer: &mut Node, pages: &[Rect], moves: &[Vec2]) { /// A `.ai` saved without its PDF part (its pages show only a placeholder) from its editing copy /// alone (`private`, the joined `AIPrivateData` streams) → the document and the import's notes. /// Editing data written on a large canvas comes in at its declared scale: no page holds the size. +/// Names that aren't UTF-8 are read as Windows-1252 ([`ai_alone_with`]). pub fn ai_alone(private: &[u8]) -> Result<(Document, Vec), String> { + ai_alone_with(private, ai::NameEncoding::default()) +} + +/// [`ai_alone`], with the names that aren't UTF-8 read in `names`. +pub fn ai_alone_with(private: &[u8], names: ai::NameEncoding) -> Result<(Document, Vec), String> { let data = ai::decode_private(private)?; - build_from(ai::read(&data, true)?, &data, None, Page::Artboard, false, false, 1.0) + build_from(ai::read_in(&data, true, names)?, &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> { +/// way round. `names`: the encoding of names that aren't UTF-8. +pub fn ai_alone_on_page(private: &[u8], page: (f64, f64), unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec), String> { let data = ai::decode_private(private)?; - build_on_page(&data, page, unit) + build_on_page(&data, page, unit, names) } /// Does the editing data (or a program in the legacy format) have layers? @@ -499,15 +533,16 @@ fn editing_copy(ps: &[u8]) -> Option, String>> { /// but whose program has no layers (Illustrator 3 had none; image editors still export their /// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program /// has no layers and isn't in the format: other PostScript isn't read as the format's operators. -pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool) -> Option), String>> { +/// `names`: the encoding of names that aren't UTF-8. +pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool, names: ai::NameEncoding) -> Option), String>> { if ai::has_eps_data(ps) { return None; } if has_layers(ps) { - return Some(build(ps, None, Page::Art, false, false, 1.0)); + return Some(build(ps, None, Page::Art, false, false, 1.0, names)); } let program = illustrator.then(|| one_layer(ps)).flatten()?; - Some(build(&program, None, Page::Art, false, false, 1.0)) + Some(build(&program, None, Page::Art, false, false, 1.0, names)) } /// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 2b53d0d3f..e6e750207 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -1411,6 +1411,89 @@ fn an_ai_file_without_layers_keeps_the_gradients_of_its_palette_unlinked() { assert_eq!(g.gradient.stops.len(), 2); } +/// A file that comes in as its PDF part reads the palette's names that aren't UTF-8 in the encoding +/// picked, with the notes on them; a layer name that isn't UTF-8, in layers that aren't used, gets +/// none. +#[test] +fn an_ai_file_that_comes_in_as_its_pdf_part_notes_its_palette_names_in_a_legacy_encoding() { + let image = "%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceGray XN\n[ 1 0 0 1 0 100 ] 6000 6000 0 Xh\n\ + [ 1 0 0 1 0 100 ] 0 0 6000 6000 6000 6000 8 1 0 0 1 0 4 4 0 0\n%%BeginData: 1\rXI\n\u{0}%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN"; + // 빨강 (red) in code page 949, for the group and its ink; 그라 for the layer. + let palette = PALETTE.replace("Inks", r"\273\241\260\255").replace("Red ink", r"\273\241\260\255 ink"); + let art = native(&format!("{palette}{}", layer(r"\261\327\266\363", true, &format!("{}\n{image}", square(10, 10))))); + // An image past 2^25 pixels isn't read here, and the page has no art of its own in its box. + let (d, warnings, read) = crate::import::ai::with_image_limits(1 << 26, 1 << 25, 512 << 20, || { + crate::layered_ai_from(crate::AiLayers::decode(&private(&art)), pdf_part(10.0, 86.0), vec![], false, true, 1.0, crate::NameEncoding::Korean) + }); + assert!(!read, "the PDF part is used: {warnings:?}"); + let groups: Vec<&str> = d.swatch_groups.iter().map(|g| g.name.as_str()).collect(); + assert_eq!(groups, ["빨강"]); + assert!(d.swatch("빨강 ink").is_some_and(|s| s.spot)); + let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8")).collect(); + assert_eq!(notes.len(), 2, "{warnings:?}"); + assert!(notes.iter().all(|n| n.contains("windows-949") && !n.contains("그라")), "{notes:?}"); +} + +/// A library without layers reads the names of its brushes, graphic styles, symbols, variables and +/// data sets that aren't UTF-8 in the encoding picked, with a note on each name it brings in. +#[test] +fn an_ai_library_without_layers_reads_its_names_in_a_legacy_encoding() { + // 빨강 and 그라 in code page 949. + let (red, gra) = (r"\273\241\260\255", r"\261\327\266\363"); + let art = format!("%AI8_BeginBrushPattern\n({gra})\n0 0 m\n4 0 L\n4 4 L\nf\n%AI8_EndBrushPattern\n"); + let spray = format!( + "%AI8_BeginPluginObject\n(Adobe Scatter Brush Tool)\n({red})\n(2 /{gra}/ 5 1 0 0 / 0 0.5 1.5 1 1.2 2 0 -0.25 0.25 2 -45 30 1 1 0) .\n%AI8_EndPluginObject\n" + ); + let order = format!( + "%AI8_BeginPluginObject\n(Adobe Brush Manager Order)\n(Adobe Brush Manager Order)\n( Adobe Scatter Brush Tool/ {red}/) .\n%AI8_EndPluginObject\n" + ); + let styles = + format!("%AI9_BeginArtStyles\n{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n({red})\n%AI9_EndArtStyleList\n", stroke_art_style(red, 1.0, "")); + let symbols = format!("%AI5_Begin_NonPrinting\n{}%AI5_End_NonPrinting--\n", symbol_def(gra, &symbol_square(-2.0, -2.0, 2.0, 2.0), 1)); + let variables = vectorcraft_testkit::ai::variable_library(&["QVARQ"], &["QSETQ"]).replace("QVARQ", red).replace("QSETQ", gra); + let library = format!( + "%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%BeginSetup\n{art}{order}{spray}{styles}{symbols}{variables}%%EndSetup\n%%PageTrailer\n%%Trailer\n" + ); + let (d, warnings, read) = crate::layered_ai_from( + crate::AiLayers::decode(&private(&library)), + pdf_part(10.0, 86.0), + vec![], + false, + true, + 1.0, + crate::NameEncoding::Korean, + ); + assert!(!read, "{warnings:?}"); + assert_eq!(vectorcraft_brush::library(&d).iter().map(|b| b.name.as_str()).collect::>(), ["빨강"], "{warnings:?}"); + assert_eq!(d.graphic_styles.iter().map(|g| g.name.as_str()).collect::>(), ["빨강"], "{warnings:?}"); + assert_eq!(d.symbols.iter().map(|s| s.name.as_str()).collect::>(), ["그라"], "{warnings:?}"); + assert_eq!(d.variables.variables.iter().map(|v| v.name.as_str()).collect::>(), ["빨강"], "{warnings:?}"); + assert_eq!(d.variables.datasets.iter().map(|s| s.name.as_str()).collect::>(), ["그라"], "{warnings:?}"); + let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8") || w.contains("aren't UTF-8")).collect(); + assert_eq!(notes.len(), 2, "{warnings:?}"); + assert!(notes.iter().all(|n| n.contains("windows-949")), "{notes:?}"); +} + +/// A pattern of a file without layers that takes a free name beside a pattern of the PDF part has +/// a note on the name it was read under. +#[test] +fn a_renamed_pattern_of_a_file_without_layers_has_a_note_on_its_legacy_name() { + // 빨강 in code page 949. + let red = r"\273\241\260\255"; + let tile = format!("%AI3_BeginPattern: ({red})\n({red}) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n"); + let palette = format!("%AI5_BeginPalette\n0 0 Pb\n({red}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Tile)\nPc\nPB\n%AI5_EndPalette\n"); + let art = native(&format!("{tile}{palette}")); + let mut part = pdf_part(10.0, 86.0); + part.patterns.push(vectorcraft_doc::PatternDef::new("빨강", vec![])); + let (d, warnings, read) = + crate::layered_ai_from(crate::AiLayers::decode(&private(&art)), part, vec![], false, true, 1.0, crate::NameEncoding::Korean); + assert!(!read, "{warnings:?}"); + assert!(d.pattern("빨강 2").is_some(), "{:?}", d.patterns.iter().map(|p| &p.name).collect::>()); + let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8")).collect(); + assert_eq!(notes.len(), 1, "{warnings:?}"); + assert!(notes[0].contains("`빨강`"), "{notes:?}"); +} + /// A `.ai` pattern library (a palette of pattern swatches and no layers) whose PDF part defines no /// pattern gets the patterns its editing data defines with the palette's pattern swatches. #[test] diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index e8fd85764..586967234 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -44,8 +44,8 @@ mod tiff; pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data}; pub use import::{ - AiLayers, Imported, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, family_style, import, import_trusting, - import_with, is_loss, layered_ai, layered_ai_from, page, + AiLayers, Imported, NameEncoding, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, + family_style, import, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page, }; pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai, writes_image_mask}; pub use print::{PrintJob, PrintPage, print}; diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 5076822d8..99d07134f 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4619,6 +4619,9 @@ axis Both И двете @msg its section ending with `%{end}` doesn't end разделът му, завършващ на `%{end}`, няма край @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) четките, които VectorCraft все още не може да прочете, бяха пропуснати от панела „Четки“ ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet обектите с множество запълвания или щрихове, ефекти или четки се появиха във вид на изрисувания си изглед ({_1}): тяхната поява все още не подлежи на редактиране +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding името `{name}` не е в UTF-8 и е прочетено като {label}: ако изглежда грешно, отворете файла с друго legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding името `{name}` не е в UTF-8, а части от него не са валидни и в {label}: отворете файла с друго legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) още имена, които не са в UTF-8, са прочетени като {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиентът „{name}“ не е дефиниран във файла: заменен е с обикновен цвят @msg an image VectorCraft can't read was left out изображение, което VectorCraft не може да разчете, беше пропуснато @msg an image whose data is cut short was left out изображение с непълни данни беше пропуснато diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index a988ad207..8e931292f 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5015,6 +5015,9 @@ imageTrace Outline Obris @msg its section ending with `%{end}` doesn't end njen odjeljak koji završava s `%{end}` ne završava @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) četkice koje VectorCraft još ne može pročitati izostavljene su iz panela Četkice ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekti s više ispuna ili poteza, efekata ili četkica stigli su kao njihov iscrtani izgled ({_1}): njihov izgled se još ne može uređivati +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding naziv `{name}` nije u UTF-8 i pročitan je kao {label}: ako izgleda pogrešno, otvorite datoteku s drugim legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding naziv `{name}` nije u UTF-8, a dijelovi nisu ispravni ni u {label}: otvorite datoteku s drugim legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) još naziva koji nisu u UTF-8 pročitano je kao {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradijent „{name}” nije definisan u datoteci: umjesto njega koristi se obična boja @msg an image VectorCraft can't read was left out izostavljena je slika koju VectorCraft ne može pročitati @msg an image whose data is cut short was left out izostavljena je slika čiji su podaci skraćeni diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 9a413462f..88c5819a1 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5508,6 +5508,9 @@ digits Malayalam Malaiàlam @msg objects inside a blend, envelope, repeat or Intertwine group can't be attached to a path els objectes dins d’una fusió, un embolcall, una repetició o un grup Intertwine no es poden adjuntar a un traçat @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) els pinzells que VectorCraft encara no pot llegir s'han deixat fora del tauler Pinzells ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet els objectes amb diversos farciments o traços, efectes o pinzells han arribat amb el seu aspecte dibuixat ({_1}): la seva aparença encara no és editable +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding el nom `{name}` no és UTF-8 i s’ha llegit com a {label}: si sembla incorrecte, obriu el fitxer amb un altre legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding el nom `{name}` no és UTF-8, i algunes parts tampoc no són {label} vàlid: obriu el fitxer amb un altre legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) s’han llegit com a {label} més noms que no són UTF-8 ({count}) @msg Offset Path: select paths Desplaça el traçat: selecciona traçats @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Desplaça el traçat: el desplaçament no deixa res dels traçats seleccionats (una retracció més gran que les formes) @msg only a dock group above or below others collapses, not one beside another només es plega un grup d’acoblament situat a sobre o a sota d’altres, no un al costat d’un altre diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 09a17c1b9..9af7d8e50 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5090,6 +5090,9 @@ imageTrace Outline Obrys @msg its section ending with `%{end}` doesn't end jeho oddíl končící `%{end}` nemá konec @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) štětce, které VectorCraft zatím neumí načíst, byly vynechány z panelu Štětce ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekty s více výplněmi nebo tahy, efekty či štětci byly načteny jako vykreslený vzhled ({_1}): jejich vzhled zatím nelze upravovat +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding název `{name}` není v UTF-8 a byl načten jako {label}: pokud vypadá špatně, otevřete soubor s jiným legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding název `{name}` není v UTF-8 a jeho části nejsou platné ani v {label}: otevřete soubor s jiným legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) další názvy, které nejsou v UTF-8, byly načteny jako {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it přechod „{name}“ není v souboru definován: zastupuje ho plná barva @msg an image VectorCraft can't read was left out obrázek, který VectorCraft neumí přečíst, byl vynechán @msg graphs come in as plain art, without their data or settings grafy se načtou jako obyčejná grafika, bez svých dat a nastavení diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index ae7b04a20..5c6cac613 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4707,6 +4707,9 @@ imageTrace Outline Kontur @msg its section ending with `%{end}` doesn't end dens sektion, der ender med `%{end}`, ender ikke @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pensler, som VectorCraft endnu ikke kan læse, blev udeladt af panelet Pensler ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekter med flere fyld eller streger, effekter eller pensler kom ind, som de er tegnet ({_1}): deres udseende er endnu ikke redigerbart +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding navnet `{name}` er ikke UTF-8 og blev læst som {label}: hvis det ser forkert ud, så åbn filen med en anden legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding navnet `{name}` er ikke UTF-8, og dele af det er heller ikke gyldig {label}: åbn filen med en anden legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) flere navne, der ikke er UTF-8, blev læst som {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten »{name}« er ikke defineret i filen: der bruges en flad farve i stedet @msg an image VectorCraft can't read was left out et billede, som VectorCraft ikke kan læse, blev udeladt @msg an image whose data is cut short was left out et billede, hvis data er afkortet, blev udeladt diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 3d2c656c9..0c494018e 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4698,6 +4698,9 @@ theme Appearance Erscheinungsbild @msg its section ending with `%{end}` doesn't end sein Abschnitt, der mit `%{end}` enden soll, endet nicht @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) Pinsel, die VectorCraft noch nicht lesen kann, wurden im Bedienfeld „Pinsel“ ausgelassen ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet Objekte mit mehreren Flächen oder Konturen, Effekten oder Pinseln wurden so übernommen, wie sie gezeichnet aussehen ({_1}): Ihr Aussehen ist noch nicht bearbeitbar +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding Der Name `{name}` ist kein UTF-8 und wurde als {label} gelesen: Wenn er falsch aussieht, öffnen Sie die Datei mit einer anderen legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding Der Name `{name}` ist kein UTF-8, und Teile davon sind auch kein gültiges {label}: Öffnen Sie die Datei mit einer anderen legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) Weitere Namen, die kein UTF-8 sind, wurden als {label} gelesen ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it der Verlauf „{name}“ ist in der Datei nicht definiert: Eine einfache Farbe steht an seiner Stelle @msg an image VectorCraft can't read was left out ein Bild, das VectorCraft nicht lesen kann, wurde ausgelassen @msg graphs come in as plain art, without their data or settings Diagramme werden als einfache Grafik übernommen, ohne ihre Daten und Einstellungen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 32ac629b7..cc38cad02 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4730,6 +4730,9 @@ digits Malayalam Μαλαγιάλαμ @msg its section ending with `%{end}` doesn't end η ενότητά του που τελειώνει σε `%{end}` δεν τερματίζεται @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) τα πινέλα που το VectorCraft δεν μπορεί ακόμη να διαβάσει παραλείφθηκαν από τον πίνακα Πινέλα ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet τα αντικείμενα με πολλαπλά γεμίσματα, περιγράμματα, εφέ ή πινέλα εισήχθησαν ως απεικονίσεις ({_1}): η εμφάνισή τους δεν είναι ακόμη επεξεργάσιμη +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding το όνομα `{name}` δεν είναι UTF-8 και διαβάστηκε ως {label}: αν φαίνεται λάθος, ανοίξτε το αρχείο με άλλο legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding το όνομα `{name}` δεν είναι UTF-8 και τμήματά του δεν είναι έγκυρα ούτε σε {label}: ανοίξτε το αρχείο με άλλο legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) περισσότερα ονόματα που δεν είναι UTF-8 διαβάστηκαν ως {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it το ντεγκραντέ «{name}» δεν ορίζεται στο αρχείο: χρησιμοποιείται στη θέση του ένα συμπαγές χρώμα @msg an image VectorCraft can't read was left out παραλείφθηκε εικόνα που δεν μπορεί να διαβάσει το VectorCraft @msg graphs come in as plain art, without their data or settings τα γραφήματα εισάγονται ως απλή γραφική, χωρίς τα δεδομένα και τις ρυθμίσεις τους diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 757668070..fa3f9290d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4782,6 +4782,9 @@ axis Both Ambos @msg its section ending with `%{end}` doesn't end su sección que termina con `%{end}` no termina @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) los pinceles que VectorCraft aún no puede leer se omitieron del panel Pinceles ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet los objetos con varios rellenos o trazos, efectos o pinceles se importaron con su aspecto dibujado ({_1}): su aspecto aún no se puede editar +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding el nombre `{name}` no está en UTF-8 y se leyó como {label}: si no se ve bien, abre el archivo con otro legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding el nombre `{name}` no está en UTF-8 y partes de él tampoco son {label} válido: abre el archivo con otro legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) se leyeron como {label} más nombres que no están en UTF-8 ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it el degradado “{name}” no está definido en el archivo: lo sustituye un color sólido @msg an image VectorCraft can't read was left out se omitió una imagen que VectorCraft no puede leer @msg graphs come in as plain art, without their data or settings los gráficos se importan como ilustración normal, sin sus datos ni sus ajustes diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 332da5f80..eaf0c103d 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4992,6 +4992,9 @@ imageTrace Outline Ääriviiva @msg its section ending with `%{end}` doesn't end sen `%{end}`-merkintään päättyvä osio ei pääty @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) siveltimet, joita VectorCraft ei vielä osaa lukea, jätettiin pois Siveltimet-paneelista ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objektit, joilla on useita täyttöjä tai viivoja, tehosteita tai siveltimiä, tuotiin piirretyn näköisinä ({_1}): niiden ulkoasua ei voi vielä muokata +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nimi `{name}` ei ole UTF-8:aa, ja se luettiin koodauksella {label}: jos se näyttää väärältä, avaa tiedosto toisella legacyNameEncoding-arvolla +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nimi `{name}` ei ole UTF-8:aa, eivätkä kaikki sen osat ole kelvollista {label}-koodausta: avaa tiedosto toisella legacyNameEncoding-arvolla +@msg more names that aren't UTF-8 were read as {label} ({count}) muita nimiä, jotka eivät ole UTF-8:aa, luettiin koodauksella {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it liukuväriä “{name}” ei ole määritetty tiedostossa: sen tilalla on tasaväri @msg an image VectorCraft can't read was left out kuva, jota VectorCraft ei voi lukea, jätettiin pois @msg graphs come in as plain art, without their data or settings kaaviot tuodaan tavallisena grafiikkana ilman niiden tietoja ja asetuksia diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 8234baafe..b6d9da760 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4769,6 +4769,9 @@ axis Both Les deux @msg its section ending with `%{end}` doesn't end leur section qui se termine par `%{end}` ne se termine pas @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) les formes que VectorCraft ne sait pas encore lire ont été omises du panneau Formes ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet les objets à plusieurs fonds ou contours, effets ou formes ont été importés tels qu’ils sont dessinés ({_1}) : leur aspect n’est pas encore modifiable +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding le nom `{name}` n’est pas en UTF-8 et a été lu en {label} : s’il semble incorrect, ouvrez le fichier avec un autre legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding le nom `{name}` n’est pas en UTF-8, et certaines parties ne sont pas non plus du {label} valide : ouvrez le fichier avec un autre legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) d’autres noms qui ne sont pas en UTF-8 ont été lus en {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it le dégradé « {name} » n’est pas défini dans le fichier : une couleur unie le remplace @msg an image VectorCraft can't read was left out une image que VectorCraft ne peut pas lire a été ignorée @msg graphs come in as plain art, without their data or settings les graphes sont importés comme illustration ordinaire, sans leurs données ni leurs réglages diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 1635bec40..c972e3e79 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8074,6 +8074,9 @@ theme Appearance अपीयरेंस # Align to Pixel Grid Align to Pixel Grid पिक्सेल ग्रिड से संरेखित करें @msg only paths and compound paths can be aligned to the pixel grid केवल पाथ और कंपाउंड पाथ को पिक्सेल ग्रिड से संरेखित किया जा सकता है +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है और इसे {label} के रूप में पढ़ा गया: यदि यह गलत दिखे, तो फ़ाइल को किसी अन्य legacyNameEncoding से खोलें +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है, और इसके कुछ हिस्से {label} में भी मान्य नहीं हैं: फ़ाइल को किसी अन्य legacyNameEncoding से खोलें +@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 में न होने वाले और नाम {label} के रूप में पढ़े गए ({count}) Start Lists From Typed Markers टाइप किए गए चिह्नों से सूचियाँ शुरू करें Marquee Selection मार्की चयन Touching स्पर्श करते हुए diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 5f6356012..82de11af4 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5170,6 +5170,9 @@ imageTrace Outline Körvonal @msg object {id} is not a selected top-level object az objektum nem kijelölt legfelső szintű objektum: {id} @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) a VectorCraft által még nem olvasható ecsetek kimaradtak az Ecsetek panelről ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet a több kitöltésű vagy körvonalú, effektusos vagy ecsetes objektumok a megrajzolt kinézetükkel kerültek be ({_1}): a megjelenésük még nem szerkeszthető +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding a(z) `{name}` név nem UTF-8, ezért {label} kódolással lett beolvasva: ha hibásnak tűnik, nyissa meg a fájlt másik legacyNameEncoding értékkel +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding a(z) `{name}` név nem UTF-8, és egyes részei {label} kódolásban sem érvényesek: nyissa meg a fájlt másik legacyNameEncoding értékkel +@msg more names that aren't UTF-8 were read as {label} ({count}) további nem UTF-8 nevek lettek {label} kódolással beolvasva ({count}) @msg Offset Path: select paths Útvonal eltolása: jelölj ki útvonalakat @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Útvonal eltolása: az eltolás után semmi sem marad a kijelölt útvonalakból (a befelé eltolás nagyobb az alakzatoknál) @msg onto must name a floating panel az onto egy lebegő panelt kell megnevezzen diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 0ca9c8c1c..00b343156 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4707,6 +4707,9 @@ imageTrace Outline Kontur @msg its section ending with `%{end}` doesn't end bagian yang berakhir dengan `%{end}` tidak berakhir @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) kuas yang belum dapat dibaca VectorCraft tidak dimasukkan ke panel Kuas ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objek dengan beberapa isian atau goresan, efek, atau kuas masuk sebagai tampilan gambarnya ({_1}): penampilannya belum dapat diedit +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nama `{name}` bukan UTF-8 dan dibaca sebagai {label}: jika terlihat salah, buka file dengan legacyNameEncoding lain +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nama `{name}` bukan UTF-8, dan sebagiannya juga bukan {label} yang valid: buka file dengan legacyNameEncoding lain +@msg more names that aren't UTF-8 were read as {label} ({count}) nama lain yang bukan UTF-8 dibaca sebagai {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradien “{name}” tidak didefinisikan dalam berkas: warna polos dipakai sebagai pengganti @msg an image VectorCraft can't read was left out gambar yang tidak dapat dibaca VectorCraft tidak disertakan @msg an image whose data is cut short was left out gambar yang datanya terpotong tidak disertakan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 0f6d075fd..1610413fb 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4824,6 +4824,9 @@ axis Both Entrambi @msg its section ending with `%{end}` doesn't end la loro sezione che termina con `%{end}` non termina @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) i pennelli che VectorCraft non sa ancora leggere sono stati esclusi dal pannello Pennelli ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet gli oggetti con più riempimenti o tratti, effetti o pennelli sono stati importati come appaiono disegnati ({_1}): il loro aspetto non è ancora modificabile +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding il nome `{name}` non è in UTF-8 ed è stato letto come {label}: se sembra errato, apri il file con un altro legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding il nome `{name}` non è in UTF-8 e alcune sue parti non sono valide nemmeno in {label}: apri il file con un altro legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) altri nomi non in UTF-8 sono stati letti come {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it la sfumatura “{name}” non è definita nel file: la sostituisce un colore pieno @msg an image VectorCraft can't read was left out un'immagine che VectorCraft non può leggere è stata omessa @msg graphs come in as plain art, without their data or settings i grafici vengono importati come grafica semplice, senza i loro dati e le loro impostazioni diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2ae1a7e3a..1e76e79a8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4515,6 +4515,9 @@ swatch Bright Violet ビビッドバイオレット @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it グラデーション「{name}」はファイル内で定義されていません: 代わりに単色を使用しています @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft がまだ読み込めないブラシ {_1} 個はブラシパネルに追加されませんでした @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 塗りや線が複数ある、または効果やブラシを適用したオブジェクト {_1} 個は見た目のまま読み込まれました: アピアランスはまだ編集できません +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名前 `{name}` は UTF-8 ではないため {label} として読み込みました: 正しくない場合は別の legacyNameEncoding でファイルを開いてください +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名前 `{name}` は UTF-8 ではなく、一部は {label} としても無効です: 別の legacyNameEncoding でファイルを開いてください +@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 ではないほかの名前を {label} として読み込みました ({count}) @msg patterns nested too deeply are filled with mid-grey 入れ子が深すぎるパターンは中間のグレーで塗りつぶされています @msg shadings of an unknown type were left out 種類が不明なシェーディングは除外されました @msg shadings whose functions are of an unknown type are filled with their middle colour 関数の種類が不明なシェーディングは中間の色で塗りつぶされています diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 71b781922..bbde307d1 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4516,6 +4516,9 @@ font-technology Variable ცვლადი @msg its section ending with `%{end}` doesn't end მისი სექცია, რომელიც `%{end}`-ით მთავრდება, არ სრულდება @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) ფუნჯები, რომლებსაც VectorCraft ჯერ ვერ კითხულობს, ფუნჯების პანელში არ დაემატა ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet რამდენიმე შევსების ან კონტურის, ეფექტის ან ფუნჯის მქონე ობიექტები შემოვიდა დახატული სახით ({_1}): მათი იერსახე ჯერ არ რედაქტირდება +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding სახელი `{name}` არ არის UTF-8 და წაკითხულია როგორც {label}: თუ არასწორად გამოიყურება, გახსენით ფაილი სხვა legacyNameEncoding-ით +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding სახელი `{name}` არ არის UTF-8 და მისი ნაწილები არც {label}-ში არის სწორი: გახსენით ფაილი სხვა legacyNameEncoding-ით +@msg more names that aren't UTF-8 were read as {label} ({count}) სხვა სახელები, რომლებიც არ არის UTF-8, წაკითხულია როგორც {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it გრადიენტი „{name}“ ფაილში განსაზღვრული არ არის: მის ნაცვლად ერთგვაროვანი ფერია @msg an image VectorCraft can't read was left out გამოსახულება, რომელსაც VectorCraft ვერ კითხულობს, გამოტოვებულია @msg graphs come in as plain art, without their data or settings დიაგრამები შემოდის როგორც ჩვეულებრივი გრაფიკა, მათი მონაცემებისა და პარამეტრების გარეშე diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index d2ac7a629..c1da3da08 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4442,6 +4442,9 @@ theme Appearance 외관 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 그레이디언트 “{name}”이(가) 파일에 정의되어 있지 않아 단색으로 대체했습니다 @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft에서 아직 읽을 수 없는 브러시는 브러시 패널에서 제외되었습니다({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 칠이나 선이 여러 개인 개체, 효과, 브러시가 있는 개체는 그려진 모양({_1})으로 가져왔습니다: 모양새는 아직 편집할 수 없습니다 +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 이름 `{name}`은(는) UTF-8이 아니어서 {label}(으)로 읽었습니다: 잘못 보이면 다른 legacyNameEncoding으로 파일을 여세요 +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 이름 `{name}`은(는) UTF-8이 아니고 일부는 {label}에서도 올바르지 않습니다: 다른 legacyNameEncoding으로 파일을 여세요 +@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8이 아닌 다른 이름도 {label}(으)로 읽었습니다({count}) @msg patterns nested too deeply are filled with mid-grey 너무 깊게 중첩된 패턴은 중간 회색으로 채웁니다 @msg shadings of an unknown type were left out 알 수 없는 종류의 셰이딩은 제외했습니다 @msg shadings whose functions are of an unknown type are filled with their middle colour 함수 종류를 알 수 없는 셰이딩은 중간 색상으로 채웁니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 9f4d8ccaf..b5d8f750a 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4719,6 +4719,9 @@ imageTrace Outline Contour @msg its section ending with `%{end}` doesn't end zijn sectie die op `%{end}` eindigt, eindigt niet @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penselen die VectorCraft nog niet kan lezen, zijn weggelaten uit het deelvenster Penselen ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objecten met meerdere vullingen of lijnen, effecten of penselen zijn als hun getekende uiterlijk binnengekomen ({_1}): hun vormgeving is nog niet bewerkbaar +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding de naam `{name}` is geen UTF-8 en is gelezen als {label}: als die er verkeerd uitziet, open het bestand dan met een andere legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding de naam `{name}` is geen UTF-8, en delen ervan zijn ook geen geldige {label}: open het bestand met een andere legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) meer namen die geen UTF-8 zijn, zijn gelezen als {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it verloop „{name}” is niet gedefinieerd in het bestand: er is een effen kleur voor gebruikt @msg an image VectorCraft can't read was left out een afbeelding die VectorCraft niet kan lezen, is weggelaten @msg an image whose data is cut short was left out een afbeelding waarvan de gegevens zijn afgekapt, is weggelaten diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index a7f0ead2b..a78055e25 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4916,6 +4916,9 @@ imageTrace Outline Kontur @msg its section ending with `%{end}` doesn't end jego sekcja kończąca się na `%{end}` nie ma końca @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pędzle, których VectorCraft jeszcze nie odczytuje, pominięto w panelu Pędzle ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet obiekty z kilkoma wypełnieniami lub obrysami, efektami albo pędzlami wczytano jako ich narysowany wygląd ({_1}): ich wyglądu nie można jeszcze edytować +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nazwa `{name}` nie jest w UTF-8 i została odczytana jako {label}: jeśli wygląda źle, otwórz plik z innym legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nazwa `{name}` nie jest w UTF-8, a jej części nie są poprawne również w {label}: otwórz plik z innym legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) kolejne nazwy spoza UTF-8 odczytano jako {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradient „{name}” nie jest zdefiniowany w pliku: zastąpiono go jednolitym kolorem @msg an image VectorCraft can't read was left out pominięto obraz, którego VectorCraft nie potrafi odczytać @msg graphs come in as plain art, without their data or settings wykresy są wczytywane jako zwykła grafika, bez ich danych i ustawień diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e1c27f0e8..f0e40aeb9 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4351,6 +4351,9 @@ axis Both Ambos @msg its section ending with `%{end}` doesn't end sua seção que termina com `%{end}` não termina @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) os pincéis que o VectorCraft ainda não consegue ler ficaram fora do painel Pincéis ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objetos com vários preenchimentos ou traços, efeitos ou pincéis entraram com a aparência desenhada ({_1}): sua aparência ainda não é editável +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding o nome `{name}` não está em UTF-8 e foi lido como {label}: se parecer errado, abra o arquivo com outro legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding o nome `{name}` não está em UTF-8, e partes dele também não são {label} válido: abra o arquivo com outro legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) mais nomes que não estão em UTF-8 foram lidos como {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it o gradiente “{name}” não está definido no arquivo: uma cor sólida o substitui @msg an image VectorCraft can't read was left out uma imagem que o VectorCraft não consegue ler foi omitida @msg graphs come in as plain art, without their data or settings os gráficos são importados como arte comum, sem seus dados nem configurações diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8f995fcf9..9f5f728b0 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4818,6 +4818,9 @@ axis Both Оба @msg its section ending with `%{end}` doesn't end раздел, заканчивающийся на `%{end}`, не заканчивается @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) кисти, которые VectorCraft пока не умеет читать, не добавлены в панель «Кисти» ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet объекты с несколькими заливками или обводками, эффектами или кистями импортированы в нарисованном виде ({_1}): их оформление пока нельзя редактировать +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding имя `{name}` не в UTF-8 и прочитано как {label}: если оно выглядит неверно, откройте файл с другим legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding имя `{name}` не в UTF-8, и его части недопустимы и в {label}: откройте файл с другим legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) другие имена не в UTF-8 прочитаны как {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиент «{name}» не определён в файле: вместо него используется сплошной цвет @msg an image VectorCraft can't read was left out изображение, которое VectorCraft не может прочитать, пропущено @msg graphs come in as plain art, without their data or settings диаграммы импортируются как обычная графика, без их данных и настроек diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 44bd11181..7ef0f5e8d 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4680,6 +4680,9 @@ axis Both Båda @msg its section ending with `%{end}` doesn't end dess avsnitt som slutar med `%{end}` avslutas aldrig @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penslar som VectorCraft ännu inte kan läsa utelämnades från panelen Penslar ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekt med flera fyllningar eller konturer, effekter eller penslar importerades som sitt ritade utseende ({_1}): deras utseende går inte att redigera än +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding namnet `{name}` är inte UTF-8 och lästes som {label}: om det ser fel ut, öppna filen med en annan legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding namnet `{name}` är inte UTF-8, och delar av det är inte heller giltig {label}: öppna filen med en annan legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) fler namn som inte är UTF-8 lästes som {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten ”{name}” är inte definierad i filen: en enfärgad fyllning används i stället @msg an image VectorCraft can't read was left out en bild som VectorCraft inte kan läsa utelämnades @msg graphs come in as plain art, without their data or settings diagram läses in som vanlig grafik, utan sina data och inställningar diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index c9b118843..7c4df674d 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -2043,6 +2043,28 @@ fn saved_tracing_preset_errors_are_translated() { } } +/// The notes on names in a `.ai`'s editing data that aren't UTF-8 are translated in every language +/// that translates every message, keeping the name, the encoding and the param. +#[test] +fn legacy_name_notes_are_translated() { + let notes = [ + "the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding", + "the name `레\u{fffd} 1` isn't UTF-8, and parts of it aren't valid windows-949 either: open the file with another legacyNameEncoding", + "more names that aren't UTF-8 were read as macintosh (7)", + ]; + for code in COMPLETE_MESSAGES { + let lang = Lang::from_code(code).expect("registered"); + for note in notes { + let shown = message(lang, note); + assert_ne!(shown, note, "{code}"); + assert!(shown.contains("UTF-8"), "{code}: {shown}"); + } + let shown = message(lang, notes[0]); + assert!(shown.contains("`·¹À̾î 1`") && shown.contains("windows-1252") && shown.contains("legacyNameEncoding"), "{code}: {shown}"); + assert!(message(lang, notes[2]).contains("macintosh") && message(lang, notes[2]).contains('7'), "{code}"); + } +} + /// Export Data Sets' refusals of more data sets or variables than Import Data Sets reads are /// translated in every language that translates every message. #[test] diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index eab23536f..26264b594 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4517,6 +4517,9 @@ axis Both Her İkisi @msg its section ending with `%{end}` doesn't end `%{end}` ile biten bölümü bitmiyor @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft'ın henüz okuyamadığı fırçalar Fırçalar panelinin dışında bırakıldı ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet çeşitli dolgulara veya konturlara, efektlere veya fırçalara sahip nesneler çizilmiş görünümleri olarak geldi ({_1}): görünümleri henüz düzenlenemez +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding `{name}` adı UTF-8 değil ve {label} olarak okundu: yanlış görünüyorsa dosyayı başka bir legacyNameEncoding ile açın +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding `{name}` adı UTF-8 değil ve bazı bölümleri {label} olarak da geçerli değil: dosyayı başka bir legacyNameEncoding ile açın +@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 olmayan diğer adlar {label} olarak okundu ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it “{name}” degradesi dosyada tanımlı değil: onun yerine düz bir renk geliyor @msg an image VectorCraft can't read was left out VectorCraft'ın okuyamadığı bir resim dışarıda bırakıldı @msg graphs come in as plain art, without their data or settings grafikler, verileri ve ayarları olmadan düz çizim olarak içe aktarılır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 4b64bdf01..824475c52 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4808,6 +4808,9 @@ axis Both Обидві @msg its section ending with `%{end}` doesn't end розділ, що закінчується на `%{end}`, не закінчується @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) пензлі, які VectorCraft поки не вміє читати, не додано до панелі «Пензлі» ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet об’єкти з кількома заливками або обведеннями, ефектами або пензлями імпортовані в намальованому вигляді ({_1}): їх оформлення поки що не можна редагувати +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding назва `{name}` не в UTF-8 і прочитана як {label}: якщо вона виглядає неправильно, відкрийте файл з іншим legacyNameEncoding +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding назва `{name}` не в UTF-8, і її частини недійсні також у {label}: відкрийте файл з іншим legacyNameEncoding +@msg more names that aren't UTF-8 were read as {label} ({count}) інші назви не в UTF-8 прочитано як {label} ({count}) @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градієнт «{name}» не визначено у файлі: замість нього використовується суцільний колір @msg an image VectorCraft can't read was left out зображення, яке VectorCraft не може прочитати, пропущено @msg graphs come in as plain art, without their data or settings діаграми імпортуються як звичайна графіка, без їхніх даних і налаштувань diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 43c4e7652..6e2a9e33f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4825,6 +4825,9 @@ theme Appearance Giao diện @msg object {id} is not a selected top-level object đối tượng {id} không phải là đối tượng cấp cao nhất đã chọn @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) các cọ mà VectorCraft chưa đọc được đã bị bỏ khỏi bảng Cọ ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet đối tượng có nhiều tô hoặc nét, hiệu ứng hoặc cọ đã vào dưới dạng bề ngoài ({_1}): diện mạo chưa thể chỉnh sửa +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding tên `{name}` không phải UTF-8 và đã được đọc theo {label}: nếu trông sai, hãy mở tệp với legacyNameEncoding khác +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding tên `{name}` không phải UTF-8, và một số phần cũng không hợp lệ trong {label}: hãy mở tệp với legacyNameEncoding khác +@msg more names that aren't UTF-8 were read as {label} ({count}) thêm các tên không phải UTF-8 đã được đọc theo {label} ({count}) @msg Offset Path: select paths Đường dẫn độ lệch: hãy chọn đường dẫn @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Đường dẫn độ lệch: độ lệch không để lại gì từ các đường dẫn đã chọn (độ lệch vào trong lớn hơn hình) @msg onto must name a floating panel onto phải đặt tên bảng đang thả nổi diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 64cfee676..792919c6a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4228,6 +4228,9 @@ imageTrace Outline 轮廓 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 文件中未定义渐变“{name}”:已改用纯色代替 @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚无法读取的画笔未加入“画笔”面板 ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 带有多个填色或描边、效果或画笔的对象已按其绘制外观导入 ({_1}):其外观尚不可编辑 +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名称 `{name}` 不是 UTF-8,已按 {label} 读取:如果显示不正确,请使用其他 legacyNameEncoding 打开文件 +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名称 `{name}` 不是 UTF-8,且部分内容在 {label} 中也无效:请使用其他 legacyNameEncoding 打开文件 +@msg more names that aren't UTF-8 were read as {label} ({count}) 其他非 UTF-8 名称已按 {label} 读取 ({count}) @msg patterns nested too deeply are filled with mid-grey 嵌套过深的图案以中灰色填充 @msg shadings of an unknown type were left out 已略去未知类型的着色 @msg shadings whose functions are of an unknown type are filled with their middle colour 函数类型未知的着色以其中间颜色填充 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index da7e23598..569c72914 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2069,6 +2069,9 @@ file-filter Templates 範本 @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 檔案中未定義漸層「{name}」:已改用單色替代 @msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚無法讀取的筆刷未加入「筆刷」面板 ({_1}) @msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 具有多個填色或筆畫、效果或筆刷的物件已依其繪製外觀匯入 ({_1}):其外觀目前尚無法編輯 +@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名稱 `{name}` 不是 UTF-8,已依 {label} 讀取:若顯示不正確,請以其他 legacyNameEncoding 開啟檔案 +@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名稱 `{name}` 不是 UTF-8,且部分內容在 {label} 中也無效:請以其他 legacyNameEncoding 開啟檔案 +@msg more names that aren't UTF-8 were read as {label} ({count}) 其他非 UTF-8 名稱已依 {label} 讀取 ({count}) @msg patterns nested too deeply are filled with mid-grey 巢狀層級過深的圖樣以中灰色填滿 @msg shadings of an unknown type were left out 已略過類型未知的著色 @msg shadings whose functions are of an unknown type are filled with their middle colour 函數類型未知的著色以其中間顏色填滿 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 57a0e619a..cbd5c3788 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -266,6 +266,8 @@ A Scatter brush in the editing data whose last 15 numbers start with 1 comes int An object in a pattern's art or in a brush's art takes the file's art styles in the same way as an object on a layer: with a style VectorCraft reads, it comes in as itself with its effects live, not as the look it draws. An object in a pattern's art with a style in the file's style list keeps that graphic style's id. An art styles section inside a symbol's, a pattern's or a brush's art is skipped. +Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets) are read as Windows-1252 unless `legacyNameEncoding` (`convert --legacy-name-encoding`) picks `macintosh`, `windows-949`, `shift_jis`, `gbk` or `big5`; the open result's warnings list the first three such names as read and count the rest. The text values of data sets are read in the same encoding. + ## Format by format | Format | Illustrator | Ours: open/place | Ours: save/export | Fidelity and tests | diff --git a/docs/mcp.md b/docs/mcp.md index 40a1e237d..57efbe087 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -877,6 +877,12 @@ an EPS as its page, a `.ai` file from its PDF content (a `.ai` saved without PDF {"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/label.eps","editingData":false}}} ``` +Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles, +swatches, brushes, symbols, variables and data sets, and the text values of data sets) are read as Windows-1252 unless +`document.open` takes `legacyNameEncoding`: `macintosh`, `windows-949` (also `euc-kr`), `shift_jis`, `gbk` or `big5`. An +unknown label is refused. The `warnings` of the result name the first three such names as read and count the rest. +`vectorcraft-cli convert --legacy-name-encoding ENC` passes it on. + Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based), `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