From 3c463fa5fb03f1f7cb0c381f268b2794b5195a36 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 08:34:40 -0800 Subject: [PATCH 01/49] Brushes: strokes draw with the first of two brushes with one name When a document holds two brushes with the same name, the canvas drew a stroke that names it with the last of them, while brush.get, Brush Options, expanding, outlining and the SVG, PDF, EPS and .ai exports took the first. Document Info gave the last one's type, and every row of that name in the Brushes panel previewed and dragged the last one. The renderer, Document Info and the Brushes panel now take the first brush of a name, so the canvas, the panel and the exports agree. --- crates/engine/src/cmd/docinfo.rs | 11 ++++++---- crates/render/src/brush_fx.rs | 25 +++++++++++++++++++++- crates/ui-egui/src/panels/brushes.rs | 12 ++++++++++- crates/ui-egui/src/panels/brushes/tests.rs | 24 +++++++++++++++++++++ docs/target-app-parity.md | 2 ++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index a43325becd..a58ff8e1ee 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -633,10 +633,13 @@ fn info(s: &mut Session, p: &Value) -> Result { fonts.extend(super::fonts::art_texts_of(d).into_iter().flat_map(|(_, t, _)| t.runs.iter().map(|r| super::fonts::font_of(&r.style)))); } let font_names: Vec = fonts.iter().map(super::fonts::font_label).collect(); - // Each brush the art uses with its type, found by name in the Brushes panel as the renderer finds it (the last of two + // Each brush the art uses with its type, found by name in the Brushes panel as the renderer finds it (the first of two // brushes with one name); a brush the panel does not hold draws as a plain stroke and has no type. A row names the type // as the New Brush dialog does (`Art Brush`), and the panel translates these values when it shows them. - let kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = library.iter().map(|b| (b.name.as_str(), &b.kind)).collect(); + let mut kinds: HashMap<&str, &vectorcraft_brush::BrushKind> = HashMap::new(); + for b in &library { + kinds.entry(b.name.as_str()).or_insert(&b.kind); + } let brush_entries: Vec = brushes.iter().map(|name| json!({ "name": name, "type": kinds.get(name.as_str()).map(|k| k.type_id()) })).collect(); let brush_rows: Vec<(String, String)> = brushes @@ -910,7 +913,7 @@ mod tests { let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); d.node_mut(NodeId(missing)).unwrap().appearance.stroke_mut().unwrap().paint = Paint::None; assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([])); - // Two panel brushes with one name: the type is the one the renderer draws with, the last one's. + // Two panel brushes with one name: the type is the one the renderer draws with, the first one's. let twin = square(&mut s, 140.0); s.execute("brush.apply", &json!({"ids": [twin], "name": "Charcoal"})).unwrap(); let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); @@ -920,7 +923,7 @@ mod tests { lib.extend(kinds.into_iter().map(|kind| vectorcraft_brush::Brush { name: "Twin".into(), kind })); vectorcraft_brush::store(d, &lib); d.node_mut(NodeId(twin)).unwrap().appearance.stroke_mut().unwrap().brush = Some("Twin".into()); - assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([{"name": "Twin", "type": "scatter"}])); + assert_eq!(s.execute("document.info", &json!({"category": "brushes"})).unwrap()["brushes"], json!([{"name": "Twin", "type": "art"}])); // Type: a brushed stroke on drawn characters is listed, one on spaces alone is not. let blank = s.execute("text.create", &json!({"x": 10, "y": 60, "text": " "})).unwrap()["id"].as_u64().unwrap(); let word = s.execute("text.create", &json!({"x": 10, "y": 90, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); diff --git a/crates/render/src/brush_fx.rs b/crates/render/src/brush_fx.rs index b1b46b3327..3c6ea922d4 100644 --- a/crates/render/src/brush_fx.rs +++ b/crates/render/src/brush_fx.rs @@ -107,7 +107,12 @@ impl BrushCache { vectorcraft_brush::serde_json::to_string(v).unwrap_or_default().hash(&mut h); h.finish() }); - self.lib = vectorcraft_brush::library(doc).into_iter().map(|b| (b.name.clone(), Arc::new(b))).collect(); + // Of two brushes with one name, a stroke that names it draws with the first, as + // `vectorcraft_brush::find` and the exports take it. + self.lib.clear(); + for b in vectorcraft_brush::library(doc) { + self.lib.entry(b.name.clone()).or_insert_with(|| Arc::new(b)); + } } /// [`Self::key`] for the brush library `doc` holds (checked once a frame, `stamp`). @@ -388,6 +393,24 @@ mod tests { assert!(!dark(a.pixel(100, 58)) && dark(b.pixel(100, 58))); } + /// Of two brushes with one name, a stroke that names it draws with the first. + #[test] + fn a_stroke_draws_with_the_first_of_two_brushes_with_its_name() { + use vectorcraft_brush::{Brush, BrushKind, Calligraphic}; + let nib = |size: f64| Brush { name: "Twin".into(), kind: BrushKind::Calligraphic(Calligraphic { size, ..Default::default() }) }; + let doc = |lib: &[Brush]| { + let mut d = doc_with_line(Some("Twin"), 1.0); + vectorcraft_brush::store(&mut d, lib); + d + }; + let (thin, wide) = (nib(1.0), nib(20.0)); + let first = render(&mut Renderer::new(), &doc(std::slice::from_ref(&thin))).pixels; + let second = render(&mut Renderer::new(), &doc(std::slice::from_ref(&wide))).pixels; + assert_ne!(first, second, "the two nibs paint differently"); + let both = render(&mut Renderer::new(), &doc(&[thin, wide])).pixels; + assert!(both == first, "the stroke draws with the first brush named Twin"); + } + #[test] fn stained_symbol_instance() { let mut d = Document::new(100.0, 100.0); diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 8f29c70475..969fa14aeb 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -69,6 +69,16 @@ pub(crate) fn document_brushes(st: &DocState) -> Rc> { MEMO.with(|m| m.borrow_mut().get(&st.doc, st.revision, || vectorcraft_brush::library(&st.doc).iter().map(|b| Shown::of(&st.doc, b)).collect())) } +/// The brushes of `shown` by name: of two with one name, the first, the one a stroke that names +/// it draws with and `brush.get` gives. +fn by_name(shown: &[Shown]) -> HashMap<&str, &Shown> { + let mut map = HashMap::new(); + for b in shown { + map.entry(b.name.as_str()).or_insert(b); + } + map +} + /// The cache key of the stroke preview of brush definition `def` in `doc` (`art`: its art): the /// definition and every resource of `doc` the art uses ([`art_resources`]: patterns with their /// options, symbols, swatches and colour groups, gradients, images, styles…), so the preview @@ -298,7 +308,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // definitions and preview keys are the document version's, worked out once. let doc = app.session.active().map(|d| d.doc.clone()); let memo = app.session.active().map(document_brushes).unwrap_or_default(); - let defs: HashMap<&str, &Shown> = memo.iter().map(|b| (b.name.as_str(), b)).collect(); + let defs = by_name(&memo); let mut clicked: Option<(String, egui::Modifiers)> = None; let mut options: Option = None; let mut moved: Option = None; diff --git a/crates/ui-egui/src/panels/brushes/tests.rs b/crates/ui-egui/src/panels/brushes/tests.rs index fcab4373ce..2d9305fc52 100644 --- a/crates/ui-egui/src/panels/brushes/tests.rs +++ b/crates/ui-egui/src/panels/brushes/tests.rs @@ -821,3 +821,27 @@ fn previews_follow_the_swatches_images_and_pattern_options_their_art_uses() { assert_eq!(change(&mut app, what, f), (brushes, symbols, what.to_string())); } } + +/// Of two brushes with one name, both rows preview and drag the first, the one `brush.get` gives +/// and a stroke that names it draws with. +#[test] +fn rows_of_two_brushes_with_one_name_show_the_first() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + app.session + .edit("Brushes", |d, _| { + let mut lib = vectorcraft_brush::library(d); + let kinds: Vec<_> = ["Charcoal", "Dots"].iter().map(|n| lib.iter().find(|b| b.name == *n).unwrap().kind.clone()).collect(); + lib.extend(kinds.into_iter().map(|kind| Brush { name: "Twin".into(), kind })); + vectorcraft_brush::store(d, &lib); + Ok(()) + }) + .unwrap(); + let twins: Vec = brushes(&mut app).0.into_iter().filter(|b| b.0 == "Twin").map(|b| b.1).collect(); + assert_eq!(twins, ["art", "scatter"], "the panel lists both"); + let got = app.run("brush.get", json!({"name": "Twin"})).unwrap(); + let memo = document_brushes(app.session.active().unwrap()); + let shown = by_name(&memo)["Twin"]; + assert_eq!(shown.ty, "art"); + assert_eq!(shown.def, got, "the row shows and drags what brush.get gives"); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 65b58c6dc5..4cd01e8945 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1766,6 +1766,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Brush art of copies:** symbol instances, inline graphics and repeat copies of a brushed path keep their own brush art between frames, as the same path on the page does. The renderer also keeps the stroke outlines of brush art and the evaluation of a live object in it, such as an envelope that a scatter brush places. The brush art is made again when the path, its stroke or the brush changes, and when the art of a symbol or pattern that the brush art can show changes. On one thread at 1000 × 1000 pixels (the median frame, with the median fastest frame in brackets), 2,000 paths on the page whose scatter brush art is an envelope take 94 ms (80 ms) a frame instead of 400 ms (306 ms), and 2,000 instances of a symbol whose art is a brushed path take 13.4 ms (12.0 ms) instead of 19.3 ms (15.6 ms), with the same pixels. +- **Two brushes with one name:** when a document's Brushes panel holds two brushes with the same name, a stroke that names it draws on the canvas with the first of them, as `brush.get`, Brush Options, expanding, outlining and the SVG, PDF, EPS and `.ai` exports take it. Document Info gives that brush's type, and both rows of the Brushes panel preview and drag the first brush. + - **Live objects in symbols and masks:** symbol instances and inline graphics whose symbol is a blend, an envelope, a repeat, an Intertwine object or a gradient mesh, or a group that holds one, draw the evaluation the renderer keeps for that object between frames, as the same object on the page does. So do opacity masks whose art is such an object, and such objects in clip groups in Outline view. The object is evaluated again when the symbols, patterns, fonts or color settings it shows change. Symbol art with effects of its own, or that holds a symbol instance, an image, a placed file, type with an inline graphic or a raster effect, is still placed and evaluated again in every frame. At 1000 × 1000 pixels, a page of 2,000 instances of a symbol that holds an envelope takes 28 ms a frame instead of 126 ms, with the same pixels. - **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels. From 3791b5451c53d420608ddf2c47cfc22d9c596edd Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 09:25:50 -0800 Subject: [PATCH 02/49] i18n: translate the "select a path" reason in every complete-message language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Object › Path's commands that need a path (Reverse Path Direction, Join, Average, Simplify, Smooth, Add Anchor Points, Divide Objects Below, Split Into Grid) report "select a path" when the selection holds no path, and Select Path's stored selection reports it with nothing selected. Only the Hindi and Swedish catalogs had a row for it, so the status bar showed the reason in English in the other 21 languages that translate every message. - Add the row to cs, de, el, es, fr, hu, it, ja, ka, ko, pl, pt-br, ru, uk, vi, zh-hans, zh-hant, fi, tr, ca and nl, using each catalog's wording from its "Divide Objects Below: select a path" row. - Add a test that requires the exact row in every complete-message catalog and checks the translated reason inside the disabled-command error that path.reverse returns for a selection of type alone. --- crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/tests.rs | 31 +++++++++++++++++++++++++++++ crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + 22 files changed, 52 insertions(+) diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5516c9662f..146d3d959d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5540,6 +5540,7 @@ digits Malayalam Malaiàlam @msgplural Discarded {n} recovered document|Discarded {n} recovered documents S’ha descartat {n} document recuperat|S’han descartat {n} documents recuperats @msg divide into 1 to {MAX_PIECES} slices divideix en 1 a {MAX_PIECES} talls @msg Divide Objects Below: select a path Divideix els objectes de sota: seleccioneu un traçat +@msg select a path seleccioneu un traçat @msg Divide Objects Below: select exactly one path Divideix els objectes de sota: seleccioneu exactament un traçat @msg docking action contains an unknown panel or an excessive split path l’acció d’acoblament conté un panell desconegut o un camí de divisió excessiu @msg Downloaded {what} S’ha baixat {what} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index d03d192a3a..488e891b13 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4660,6 +4660,7 @@ imageTrace Outline Obrys @msg Plug-ins can't be loaded from {path}: {e} Zásuvné moduly nelze načíst z {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) nástrojem Přímý výběr vyberte kotevní body, kde se má cesta rozdělit (ne koncové body otevřených cest) @msg Divide Objects Below: select a path Rozdělit objekty pod: vyberte cestu +@msg select a path vyberte cestu @msg Divide Objects Below: select exactly one path Rozdělit objekty pod: vyberte právě jednu cestu @msg Downloaded {what} Staženo: {what} @msg Expand Appearance: the selection has a basic appearance Rozložit vzhled: výběr má základní vzhled diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 47337855aa..4d3f7cdc72 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4273,6 +4273,7 @@ theme Appearance Erscheinungsbild @msg Plug-ins can't be loaded from {path}: {e} Aus {path} können keine Plug-ins geladen werden: {e} @msg direct-select anchor points to cut at (not the end points of open paths) wählen Sie mit der Direktauswahl die Ankerpunkte aus, an denen geschnitten werden soll (nicht die Endpunkte offener Pfade) @msg Divide Objects Below: select a path Darunterliegende Objekte aufteilen: Wählen Sie einen Pfad aus +@msg select a path Wählen Sie einen Pfad aus @msg Divide Objects Below: select exactly one path Darunterliegende Objekte aufteilen: Wählen Sie genau einen Pfad aus @msg Downloaded {what} {what} heruntergeladen @msg Expand Appearance: the selection has a basic appearance Aussehen umwandeln: Die Auswahl hat ein einfaches Aussehen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c319729a71..9c526380e0 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2430,6 +2430,7 @@ axis Both Και τα δύο @msg Plug-ins can't be loaded from {path}: {e} Δεν είναι δυνατή η φόρτωση πρόσθετων από το {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) Επιλέξτε άμεσα τα σημεία αγκύρωσης όπου θα γίνει η αποκοπή (όχι τα τελικά σημεία ανοιχτών διαδρομών) @msg Divide Objects Below: select a path Διαίρεση αντικειμένων από κάτω: επιλέξτε μια διαδρομή +@msg select a path επιλέξτε μια διαδρομή @msg Divide Objects Below: select exactly one path Διαίρεση αντικειμένων από κάτω: επιλέξτε ακριβώς μία διαδρομή @msg Downloaded {what} Έγινε λήψη του {what} @msg Expand Appearance: the selection has a basic appearance Επέκταση εμφάνισης: η επιλογή έχει βασική εμφάνιση diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 5b203292a4..36a44acfb0 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4350,6 +4350,7 @@ axis Both Ambos @msg Plug-ins can't be loaded from {path}: {e} No se pueden cargar plugins desde {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) selecciona con Selección directa los puntos de ancla en los que cortar (no los puntos finales de trazados abiertos) @msg Divide Objects Below: select a path Dividir objetos inferiores: selecciona un trazado +@msg select a path selecciona un trazado @msg Divide Objects Below: select exactly one path Dividir objetos inferiores: selecciona exactamente un trazado @msg Downloaded {what} Se descargó {what} @msg Expand Appearance: the selection has a basic appearance Expandir apariencia: la selección tiene una apariencia básica diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index b2c6f48d54..3abcb066e8 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4574,6 +4574,7 @@ imageTrace Outline Ääriviiva @msg Plug-ins can't be loaded from {path}: {e} Laajennuksia ei voi ladata kansiosta {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) valitse suoravalinnalla ankkuripisteet, joista leikataan (ei avoimien polkujen päätepisteitä) @msg Divide Objects Below: select a path Jaa alla olevat objektit: valitse polku +@msg select a path valitse polku @msg Divide Objects Below: select exactly one path Jaa alla olevat objektit: valitse tasan yksi polku @msg Downloaded {what} Ladattiin {what} @msg Expand Appearance: the selection has a basic appearance Laajenna ulkoasu: valinnalla on perusulkoasu diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index e84f344060..6f89a0a697 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4339,6 +4339,7 @@ axis Both Les deux @msg Plug-ins can't be loaded from {path}: {e} Impossible de charger les plug-ins depuis {path} ({e}) @msg direct-select anchor points to cut at (not the end points of open paths) sélectionnez avec l’outil Sélection directe les points d’ancrage où couper (pas les extrémités des tracés ouverts) @msg Divide Objects Below: select a path Diviser les objets inférieurs : sélectionnez un tracé +@msg select a path sélectionnez un tracé @msg Divide Objects Below: select exactly one path Diviser les objets inférieurs : sélectionnez exactement un tracé @msg Downloaded {what} {what} téléchargé @msg Expand Appearance: the selection has a basic appearance Décomposer l’aspect : la sélection a un aspect de base diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index effb31e5d8..2348888313 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5161,6 +5161,7 @@ imageTrace Outline Körvonal @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Elvetett helyreállított dokumentumok: {n} @msg divide into 1 to {MAX_PIECES} slices a felosztás 1 és {MAX_PIECES} közötti szeletre lehetséges @msg Divide Objects Below: select a path Alatta lévő objektumok felosztása: jelölj ki egy útvonalat +@msg select a path jelölj ki egy útvonalat @msg Divide Objects Below: select exactly one path Alatta lévő objektumok felosztása: pontosan egy útvonalat jelölj ki @msg docking action contains an unknown panel or an excessive split path a dokkolási művelet ismeretlen panelt vagy túl hosszú felosztási útvonalat tartalmaz @msg Downloaded {what} Letöltve: {what} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2d6c7e2186..a5786589c7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4402,6 +4402,7 @@ axis Both Entrambi @msg Plug-ins can't be loaded from {path}: {e} Impossibile caricare plug-in da {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) seleziona con Selezione diretta i punti di ancoraggio in cui tagliare (non i punti finali dei tracciati aperti) @msg Divide Objects Below: select a path Dividi oggetti sottostanti: seleziona un tracciato +@msg select a path seleziona un tracciato @msg Divide Objects Below: select exactly one path Dividi oggetti sottostanti: seleziona esattamente un tracciato @msg Downloaded {what} Scaricato {what} @msg Expand Appearance: the selection has a basic appearance Espandi aspetto: la selezione ha un aspetto di base diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 17444d894e..c116765389 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4558,6 +4558,7 @@ swatch Bright Violet ビビッドバイオレット @msg direct-select anchor points to cut at (not the end points of open paths) ダイレクト選択ツールで、カットする位置のアンカーポイントを選択してください (オープンパスの端点を除く) @msg select paths or anchor points パスまたはアンカーポイントを選択してください @msg Divide Objects Below: select a path 背面のオブジェクトで型抜き: パスを選択してください +@msg select a path パスを選択してください @msg Divide Objects Below: select exactly one path 背面のオブジェクトで型抜き: パスを 1 つだけ選択してください @msg Offset Path: select paths パスのオフセット: パスを選択してください @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) パスのオフセット: オフセットすると選択したパスが何も残りません (内側へのオフセットがシェイプより大きい) diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index be8a847cb2..902b01c093 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4056,6 +4056,7 @@ font-technology Variable ცვლადი @msg Plug-ins can't be loaded from {path}: {e} პლაგინების ჩატვირთვა ვერ ხერხდება აქედან: {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) პირდაპირი მონიშვნით მონიშნეთ საყრდენი წერტილები, სადაც გაიჭრება (არა ღია ტრაექტორიების ბოლო წერტილები) @msg Divide Objects Below: select a path ქვედა ობიექტების დაყოფა: მონიშნეთ ტრაექტორია +@msg select a path მონიშნეთ ტრაექტორია @msg Divide Objects Below: select exactly one path ქვედა ობიექტების დაყოფა: მონიშნეთ ზუსტად ერთი ტრაექტორია @msg Downloaded {what} ჩამოტვირთულია: {what} @msg Expand Appearance: the selection has a basic appearance იერსახის გაშლა: მონიშნულს საბაზისო იერსახე აქვს diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 991daba6d0..c92b1cbc5b 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4485,6 +4485,7 @@ theme Appearance 외관 @msg direct-select anchor points to cut at (not the end points of open paths) 자를 앵커 포인트를 직접 선택하세요(열린 패스의 끝점 제외) @msg select paths or anchor points 패스 또는 앵커 포인트를 선택하세요 @msg Divide Objects Below: select a path 아래 개체 분할: 패스를 선택하세요 +@msg select a path 패스를 선택하세요 @msg Divide Objects Below: select exactly one path 아래 개체 분할: 패스를 하나만 선택하세요 @msg Offset Path: select paths 패스 오프셋: 패스를 선택하세요 @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) 패스 오프셋: 오프셋을 적용하면 선택한 패스가 남지 않습니다(안쪽 오프셋이 모양보다 큼) diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b8ca33858f..638bf0b597 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4770,6 +4770,7 @@ imageTrace Outline Contour @msg Data Recovery has nowhere to keep copies: set Preferences → File Handling → Data Recovery → Folder (recoveryFolder) Gegevensherstel heeft nergens ruimte om kopieën te bewaren: stel Voorkeuren → Bestandsafhandeling → Gegevensherstel → Map (recoveryFolder) in @msg direct-select anchor points to cut at (not the end points of open paths) selecteer met directe selectie ankerpunten om te knippen (niet de eindpunten van open paden) @msg Divide Objects Below: select a path Objecten eronder verdelen: selecteer een pad +@msg select a path selecteer een pad @msg Divide Objects Below: select exactly one path Objecten eronder verdelen: selecteer precies één pad @msg Downloaded {what} {what} gedownload @msg Expand Appearance: the selection has a basic appearance Vormgeving uitbreiden: de selectie heeft een basisvormgeving diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index eb5afe87cc..ead46a4d69 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4484,6 +4484,7 @@ imageTrace Outline Kontur @msg Plug-ins can't be loaded from {path}: {e} Nie można wczytać wtyczek z {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) zaznacz bezpośrednio punkty kontrolne, w których ma nastąpić cięcie (nie punkty końcowe otwartych ścieżek) @msg Divide Objects Below: select a path Podziel obiekty poniżej: zaznacz ścieżkę +@msg select a path zaznacz ścieżkę @msg Divide Objects Below: select exactly one path Podziel obiekty poniżej: zaznacz dokładnie jedną ścieżkę @msg Downloaded {what} Pobrano {what} @msg Expand Appearance: the selection has a basic appearance Rozszerz wygląd: zaznaczenie ma wygląd podstawowy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 80615d6370..6ef008a21e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3819,6 +3819,7 @@ axis Both Ambos @msg Plug-ins can't be loaded from {path}: {e} Não é possível carregar plug-ins de {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) selecione diretamente os pontos de ancoragem onde cortar (não as extremidades de traçados abertos) @msg Divide Objects Below: select a path Dividir objetos abaixo: selecione um traçado +@msg select a path selecione um traçado @msg Divide Objects Below: select exactly one path Dividir objetos abaixo: selecione exatamente um traçado @msg Downloaded {what} {what} baixado @msg Expand Appearance: the selection has a basic appearance Expandir aparência: a seleção tem uma aparência básica diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c830e69b37..d7c0b9ebf9 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4387,6 +4387,7 @@ axis Both Оба @msg Plug-ins can't be loaded from {path}: {e} Не удается загрузить модули из {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) выделите прямым выделением опорные точки для разреза (не конечные точки открытых контуров) @msg Divide Objects Below: select a path Разделение объектов ниже: выделите контур +@msg select a path выделите контур @msg Divide Objects Below: select exactly one path Разделение объектов ниже: выделите ровно один контур @msg Downloaded {what} Скачано: {what} @msg Expand Appearance: the selection has a basic appearance Разбор оформления: у выделения основное оформление diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index d7f2c8a2ef..1e1b7adb89 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -2576,6 +2576,37 @@ fn plugin_limit_errors_are_translated() { } } +/// The reason Object › Path's commands are unavailable while the selection holds no path has its +/// own row in every language that translates every message, and shows translated both alone and +/// inside the error a disabled command returns. +#[test] +fn selected_path_reason_is_translated() { + use serde_json::json; + use vectorcraft_engine::EngineError; + let reason = "select a path"; + assert!(message_literals().contains(reason)); + let mut s = vectorcraft_engine::Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + s.execute("text.create", &json!({"x": 10, "y": 10, "text": "Type"})).unwrap(); + s.execute("select.all", &json!({})).unwrap(); + let error = match s.execute("path.reverse", &json!({})) { + Err(e @ EngineError::Disabled(..)) => e.to_string(), + other => panic!("a selection of type alone: {other:?}"), + }; + assert_eq!(error, "command `path.reverse` is not available right now: select a path"); + assert_eq!(message(Lang::EN, &error), error); + for code in COMPLETE_MESSAGES { + let lang = Lang::from_code(code).expect("registered"); + let (entries, _) = parse_entries(lang.0.source); + let row = entries.iter().find(|(ctx, src, _)| ctx == "@msg" && src == reason).map(|(_, _, tr)| tr.as_str()); + let Some(row) = row else { panic!("{code}: no `@msg` row for {reason:?}") }; + assert_ne!(row, reason, "{code}"); + assert_eq!(message(lang, reason), row, "{code}"); + let shown = message(lang, &error); + assert!(shown.contains(row) && !shown.contains(reason) && shown.contains("`path.reverse`"), "{code}: {shown}"); + } +} + /// The errors a script's `File` and `Folder` objects, `vc.run` and `$.evalFile` raise are /// translated in every language of [`COMPLETE_MESSAGES`], and so is the status bar's line for a run /// that ends with one: the script's name, its place, the command ids and what the script itself diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f5fc694ec2..ba2108d59a 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4140,6 +4140,7 @@ axis Both Her İkisi @msg Plug-ins can't be loaded from {path}: {e} Eklentiler {path} konumundan yüklenemiyor: {e} @msg direct-select anchor points to cut at (not the end points of open paths) kesilecek bağlantı noktalarını doğrudan seçin (açık yolların bitiş noktalarını değil) @msg Divide Objects Below: select a path Aşağıdaki Nesneleri Böl: bir yol seçin +@msg select a path bir yol seçin @msg Divide Objects Below: select exactly one path Aşağıdaki Nesneleri Böl: tam olarak bir yol seçin @msg Downloaded {what} {what} indirildi @msg Expand Appearance: the selection has a basic appearance Görünümü Genişlet: seçimin temel bir görünümü vardır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5a36512db0..ac91097227 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4386,6 +4386,7 @@ axis Both Обидві @msg Plug-ins can't be loaded from {path}: {e} Не вдається завантажити модулі з {path}: {e} @msg direct-select anchor points to cut at (not the end points of open paths) виділіть прямим виділенням опорні точки для розрізу (не кінцеві точки відкритих контурів) @msg Divide Objects Below: select a path Поділ об’єктів нижче: виділіть контур +@msg select a path виділіть контур @msg Divide Objects Below: select exactly one path Поділ об’єктів нижче: виділіть рівно один контур @msg Downloaded {what} Завантажено: {what} @msg Expand Appearance: the selection has a basic appearance Розбір оформлення: виділення основне оформлення diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 839f5c7405..6b7ce6d1fc 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4819,6 +4819,7 @@ theme Appearance Giao diện @msgplural Discarded {n} recovered document|Discarded {n} recovered documents Đã loại bỏ {n} tài liệu đã phục hồi @msg divide into 1 to {MAX_PIECES} slices chia thành 1 đến {MAX_PIECES} lát cắt @msg Divide Objects Below: select a path Chia đối tượng bên dưới: hãy chọn một đường dẫn +@msg select a path hãy chọn một đường dẫn @msg Divide Objects Below: select exactly one path Chia đối tượng bên dưới: chọn đúng một đường dẫn @msg docking action contains an unknown panel or an excessive split path hành động neo chứa bảng không xác định hoặc đường dẫn tách quá dài @msg Downloaded {what} Đã tải xuống {what} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b0d6001e32..e485dcc835 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4256,6 +4256,7 @@ imageTrace Outline 轮廓 @msg direct-select anchor points to cut at (not the end points of open paths) 请直接选择要在其处切割的锚点 (不能是开放路径的端点) @msg select paths or anchor points 请选择路径或锚点 @msg Divide Objects Below: select a path 分割下方对象:请选择一条路径 +@msg select a path 请选择一条路径 @msg Divide Objects Below: select exactly one path 分割下方对象:请只选择一条路径 @msg Offset Path: select paths 偏移路径:请选择路径 @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) 偏移路径:偏移后所选路径不剩任何部分 (内缩量大于形状) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c24ae37609..020f6801e4 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2002,6 +2002,7 @@ file-filter Templates 範本 @msg direct-select anchor points to cut at (not the end points of open paths) 請直接選取要剪開的錨點 (不能是開放路徑的端點) @msg select paths or anchor points 請選取路徑或錨點 @msg Divide Objects Below: select a path 分割下方物件:請選取路徑 +@msg select a path 請選取路徑 @msg Divide Objects Below: select exactly one path 分割下方物件:請只選取一條路徑 @msg Offset Path: select paths 位移複製:請選取路徑 @msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) 位移複製:位移後所選路徑不留任何部分 (內縮量大於形狀) From 78d3f6e1a56bc1bf383008b458f60b698d791ac2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 10:12:03 -0800 Subject: [PATCH 03/49] PDF/.ai import: legacy type keeps manual kerning, discretionary hyphens and language Legacy .ai type (version 7) now reads the operators it used to skip: - `autoKern kernValue Tk` (and `TK` in overflow text) sets the manual kerning of the character before the pair, or at the start of a line the space before the line's first character; autoKern 1 sets the pair back to automatic. Values are kept to -1000..10000. - `T+` and `T-` (discretionary hyphens) become soft hyphens. - `languageID XL` sets the characters' language (ids 0 to 11). - A visible text object read while a line breaking value (`TG`) or Japanese layout rules (`Xu`, `Tu`) are set opens with a warning, once per file for each. Final-form positions (Tc, Tw, Tm, Td, T*, TR) stay ignored. Legacy .ai saves now write manual kerning as `0 value Tk` after each character that has it and before a paragraph's first character with space before it, and soft hyphens as `T+`; manual kerning is no longer listed among the attributes left out. The writer's prolog defines Tk and T+. Docs: docs/file-format-parity.md, docs/target-app-parity.md. --- crates/eps/src/import/ai/legacy_text.rs | 149 ++++++++++++++++++++- crates/eps/src/import/ai/read/ops.rs | 15 ++- crates/eps/src/import/ai/tests.rs | 103 ++++++++++++++ crates/eps/src/import/tests_illustrator.rs | 20 +-- crates/eps/src/legacy_ai.rs | 6 +- crates/eps/src/legacy_ai/encoding.rs | 1 - crates/eps/src/legacy_ai/prolog.rs | 1 + crates/eps/src/legacy_ai/tests.rs | 53 +++++++- crates/eps/src/legacy_ai/writer.rs | 48 ++++++- crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/bs.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hi.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + docs/file-format-parity.md | 5 + docs/target-app-parity.md | 3 +- 38 files changed, 424 insertions(+), 34 deletions(-) diff --git a/crates/eps/src/import/ai/legacy_text.rs b/crates/eps/src/import/ai/legacy_text.rs index 043d6f2e4d..61ac106e1a 100644 --- a/crates/eps/src/import/ai/legacy_text.rs +++ b/crates/eps/src/import/ai/legacy_text.rs @@ -3,8 +3,11 @@ //! type runs along, `TP`), the modal character and paragraph attributes //! (`Tr Tf Ts Tz Tt TA Tl Ta Ti`, and §15.4.7 and §22's letter and word spacing `TC TW`, hanging //! punctuation `Tq`, hyphenation `Th` and tab stops `Xb Xe XB`, and §15.3.4's writing direction `TV`, -//! Roman character rotation `Tv`, Kumi blocks `Ty TY` and binding punctuation `Tg`), the paint -//! style, and the characters (`Tx`, `Tj`, overflow `TX`; `\r` ends a paragraph). Type set in rendering modes 4 to 7 is a mask (§5.7): the +//! Roman character rotation `Tv`, Kumi blocks `Ty TY` and binding punctuation `Tg`, and §22.6's +//! hyphenation language `XL`), the paint style, the characters (`Tx`, `Tj`, overflow `TX`; `\r` ends +//! a paragraph), manual kerning pairs (`Tk`, overflow `TK`) and discretionary hyphens (`T+`, `T-`, +//! read as soft hyphens). The line breaking value `TG` and the Japanese layout rules `Xu` (`Tu`) are +//! not kept: a visible text object read while one is set gives a warning. Type set in rendering modes 4 to 7 is a mask (§5.7): the //! text object clips the objects of its clipping group. Final-form operators (the //! computed positions, §15.1) are skipped: the type is laid out again. Strings are decoded by the //! encoding pairs of the current font's `TZ`, then by the file's `TE` encoding unless that `TZ`'s @@ -59,6 +62,14 @@ const MASK_NOT_CLIPPED: &str = "type used as a mask in linked areas, wraparound /// The warning when `TV` changes after a text object's first character: the object keeps the /// direction of that character. const DIRECTION_CHANGE_LEFT_OUT: &str = "type that changes writing direction inside a text object opens in the direction of its first character"; +/// The warning when a visible text object is read while a line breaking value (`TG`) is set. +const LINE_BREAKING_LEFT_OUT: &str = "the Line Breaking setting of legacy type was left out"; +/// The warning when a visible text object is read while Japanese layout rules (`Xu`, `Tu`) are set. +const JAPANESE_LAYOUT_LEFT_OUT: &str = "the Japanese layout rules of legacy type were left out"; +/// The manual kerning range `text.setRangeStyle` takes, in 1/1000 em. +const KERNING_RANGE: (f64, f64) = (-1000.0, 10_000.0); +/// The soft hyphen a discretionary hyphen (`T+`, `T-`) is read as. +const SOFT_HYPHEN: char = '\u{ad}'; /// A text path (`Tp` … `TP`). #[derive(Debug, Default)] @@ -124,6 +135,9 @@ pub(super) struct LegacyText { vertical: bool, /// A later character was set in the other direction. direction_changed: bool, + /// A manual kerning value (`Tk`, `TK`) read at the start of a line, in 1/1000 em: the space + /// before the next character, which starts the line. + kern_before: Option, /// The state when it began (lock, visibility), and whether it began on a `%_` line. gs: GState, hidden: bool, @@ -148,6 +162,40 @@ impl LegacyText { } } +/// Add `text` in `style` to `runs`: to the last run when it has that style, else as a new run. +fn push_run(runs: &mut Vec, text: String, style: CharStyle) { + if text.is_empty() { + return; + } + match runs.last_mut() { + Some(last) if last.inline.is_none() && last.style == style => last.text.push_str(&text), + _ => runs.push(TextRun::new(text, style)), + } +} + +/// Give the last character of `runs` the manual kerning `kerning` (None: automatic), in a run of +/// its own unless its run has one character, which joins the run before it when their styles +/// match. False when there is no character or the last one ends a paragraph: the kerning is at +/// the start of a line. +fn set_last_kerning(runs: &mut Vec, kerning: Option) -> bool { + let Some(last) = runs.last_mut().filter(|r| r.inline.is_none()) else { return false }; + let Some(c) = last.text.chars().next_back().filter(|&c| c != '\n') else { return false }; + if last.style.kerning == kerning { + return true; + } + if last.text.len() > c.len_utf8() { + last.text.pop(); + let style = CharStyle { kerning, ..last.style.clone() }; + runs.push(TextRun::new(c.to_string(), style)); + return true; + } + last.style.kerning = kerning; + if let Some(one) = runs.pop() { + push_run(runs, one.text, one.style); + } + true +} + /// Are `a` and `b` the same, leaving their tab stops out? fn same_but_tabs(a: &mut ParaStyle, b: &mut ParaStyle) -> bool { let (a_tabs, b_tabs) = (std::mem::take(&mut a.tabs), std::mem::take(&mut b.tabs)); @@ -203,6 +251,10 @@ pub(super) struct TypeState { vertical: bool, /// `TY`: a Kumi block recorded for later use, which horizontal type keeps as its mark. kumi_recorded: bool, + /// A line breaking value (`TG`) was read: VectorCraft has no setting for it. + line_breaking: bool, + /// Japanese layout rules (`Xu`, `Tu`) were read: VectorCraft has no setting for them. + japanese_layout: bool, /// The encoding strings are in, by code (`TE`); None where it says nothing. encoding: Vec>, /// Re-encoded fonts' original names (`%AI3_BeginEncoding: _new old`, or `TZ`'s). @@ -232,6 +284,8 @@ impl Default for TypeState { render: 0, vertical: false, kumi_recorded: false, + line_breaking: false, + japanese_layout: false, encoding: vec![None; 256], fonts: vec![], font_encodings: vec![], @@ -684,6 +738,22 @@ impl Reader<'_> { st.tabs_version = st.tabs_version.wrapping_add(1); } } + // `languageID XL` (§22.6), the hyphenation language: 0 U.S. English (the default + // language, none), then U.K. English, French, German, Spanish, Dutch, Italian, Swedish, + // Norwegian, Finnish, Danish and Hungarian, as the Character panel's language tags. + "XL" => { + const TAGS: [&str; 11] = ["en-GB", "fr", "de", "es", "nl", "it", "sv", "nb", "fi", "da", "hu"]; + match n0 { + Some(0.0) => st.style.language = None, + Some(id) if id.fract() == 0.0 && (1.0..=11.0).contains(&id) => { + st.style.language = TAGS.get(id as usize - 1).map(|t| t.to_string()); + } + _ => {} + } + } + // `linebreak TG` and `prop cjkLayout cjkRoman cjkCjk Xu` (`Tu` the same, §22.6). + "TG" => st.line_breaking = true, + "Xu" | "Tu" => st.japanese_layout = true, _ => {} } } @@ -691,14 +761,59 @@ impl Reader<'_> { /// `(…) Tx` (`Tj`, overflow `TX`): characters in the current style and paint. pub(super) fn text_body(&mut self, w: &str, vals: &[V]) { let Some(bytes) = vals.iter().rev().find_map(|v| if let V::Str(s) = v { Some(s.clone()) } else { None }) else { return }; + if self.text.is_none() { + return; + } + let decoded = self.type_state.decode(&bytes); + self.add_chars(w == "TX", decoded); + } + + /// `autoKern kernValue Tk` (`TK` in overflow text): the space between the character before it + /// and the character after it. With `autoKern` 0 the pair takes `kernValue` in 1/1000 em as + /// manual kerning, kept to the range `text.setRangeStyle` takes; with 1 the pair takes the + /// automatic kerning. The value is the kerning after the character before it; at the start of + /// a line it is the space before the character after it. Operands that are missing, are not + /// numbers, or a flag other than 0 and 1 leave the type as it was, and so does a `Tk` outside a + /// text object. + pub(super) fn text_kern(&mut self, vals: &[V]) { + let Some([auto, value]) = nums::<2>(vals) else { return }; + let Some(auto) = flag(auto) else { return }; + let Some(t) = self.text.as_mut() else { return }; + let kerning = (!auto).then(|| value.clamp(KERNING_RANGE.0, KERNING_RANGE.1)); + let line_start = t.pending_break.is_some() || !set_last_kerning(&mut t.runs, kerning); + if line_start { + t.kern_before = kerning; + } + } + + /// `T+` (a discretionary hyphen that doesn't print) or `T-` (`printed`: the character before it + /// is a discretionary hyphen that prints, §15.4.10.1): a soft hyphen, which shows as a hyphen + /// only where a line breaks at it. A printed hyphen character (`-` or U+2010) before `T-` + /// becomes the soft hyphen; else one is added. + pub(super) fn discretionary_hyphen(&mut self, printed: bool) { + let Some(t) = self.text.as_mut() else { return }; + if printed + && t.pending_break.is_none() + && let Some(last) = t.runs.last_mut() + && last.inline.is_none() + && last.text.ends_with(['-', '\u{2010}']) + { + last.text.pop(); + last.text.push(SOFT_HYPHEN); + return; + } + self.add_chars(false, SOFT_HYPHEN.to_string()); + } + + /// Characters in the current style and paint (`overflow`: read by `TX`). + fn add_chars(&mut self, overflow: bool, decoded: String) { let st = &mut self.type_state; let Some(t) = self.text.as_mut() else { return }; - let decoded = st.decode(&bytes); if decoded.is_empty() { return; } // Overflow text ending the story with a paragraph break (`(\r) TX`) adds no paragraph. - if w == "TX" && decoded == "\n" { + if overflow && decoded == "\n" { if t.pending_break.is_none() { let para = (!t.para_taken).then(|| (st.para.clone(), st.tabs_version)); t.pending_break = Some(PendingBreak { vertical: st.vertical, para }); @@ -744,6 +859,8 @@ impl Reader<'_> { text.push('\n'); t.para_taken = false; } + // Where the characters start in `text`, after a break read before them. + let at = text.len(); for c in decoded.chars() { if c != '\n' && !t.para_taken { t.begin_para(&mut st.para, st.tabs_version); @@ -757,10 +874,19 @@ impl Reader<'_> { t.para_taken = false; } } - match t.runs.last_mut() { - Some(last) if last.style == style => last.text.push_str(&text), - _ => t.runs.push(TextRun::new(text, style)), + // Kerning read at the start of the line is the space before its first character; a + // paragraph break there leaves no character for it. + let first = decoded.chars().next().filter(|&c| c != '\n'); + if let (Some(k), Some(c)) = (t.kern_before.take(), first) { + let end = at + c.len_utf8(); + let (rest, after) = (text.split_off(end), CharStyle { kerning_before: k, ..style.clone() }); + let own = text.split_off(at); + push_run(&mut t.runs, text, style.clone()); + push_run(&mut t.runs, own, after); + push_run(&mut t.runs, rest, style); + return; } + push_run(&mut t.runs, text, style); } /// `TO`: the text object ends. Area type with several linked areas becomes one frame for each @@ -792,6 +918,15 @@ impl Reader<'_> { if t.direction_changed { self.warn(DIRECTION_CHANGE_LEFT_OUT); } + // A copy on `%_` lines is not the type the document shows. + if !(t.hidden || hidden) { + if self.type_state.line_breaking { + self.warn(LINE_BREAKING_LEFT_OUT); + } + if self.type_state.japanese_layout { + self.warn(JAPANESE_LAYOUT_LEFT_OUT); + } + } let mut paras = std::mem::take(&mut t.paras); if paras.is_empty() { paras.push(ParaRun { style: self.type_state.para.clone(), count: 1, tabs_version: 0 }); diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index f79e7b9108..261da63611 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -383,17 +383,20 @@ impl<'a> Reader<'a> { // `(text) justified TS`, special characters (§22.6): the reader adds its characters like `Tx`. "Tx" | "Tj" | "TX" | "TS" => self.text_body(w, &vals), "Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" | "TC" | "TW" | "Tq" | "Th" | "TV" | "Tv" | "Ty" | "TY" | "Tg" | "Xb" - | "Xe" | "XB" => { + | "Xe" | "XB" | "XL" | "TG" | "Xu" | "Tu" => { self.text_attr(w, &vals); } - // Attributes the reader doesn't keep (kerning pairs, line breaking and Japanese layout - // rules) and final-form positions (§15.3). - "Tk" | "TK" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TG" | "Tu" | "Xu" => {} + // A kerning pair (`Tk`, `TK` in overflow text, §15.4.6.1) and discretionary hyphens + // (`T+`, `T-`, §15.4.10.1). + "Tk" | "TK" => self.text_kern(&vals), + "T+" => self.discretionary_hyphen(false), + "T-" => self.discretionary_hyphen(true), + // Final-form positions (§15.4.7: `Tc Tw Tm Td T* TR`, printing only), which the type's + // layout computes again. + "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" => {} // `TM` (pop text matrix) and `Te` (end render), listed in §22.6, keep nothing either; the // reader takes `Te` with any number of operands. "TM" | "Te" => {} - // The hyphenation language (`XL`), which the reader doesn't keep either (§22). - "XL" => {} "TE" => self.te(&vals), "TZ" => self.tz(&vals), // An image whose samples follow in comment lines (read at `%AI5_EndRaster`). diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 9b86f38350..b3a01fddc1 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -4284,6 +4284,109 @@ fn legacy_end_render_and_text_matrix_pop_read() { } } +/// The text, manual kerning and space before each run of a legacy-format file's first text object. +fn legacy_kerning(art: &str) -> Vec<(String, Option, f64)> { + legacy_texts(art)[0].runs.iter().map(|r| (r.text.clone(), r.style.kerning, r.style.kerning_before)).collect() +} + +/// `autoKern kernValue Tk` (§15.4.6.1) spaces the character before it from the one after it: with +/// `autoKern` 0 by `kernValue` (1/1000 em, kept to -1000 to 10,000) as the manual kerning after the +/// character before it, with 1 by the automatic kerning; `TK` does the same in overflow text. At +/// the start of a line it is the space before the line's first character. +#[test] +fn legacy_manual_pair_kerning_is_kept() { + let s = |x: &str| x.to_string(); + let runs = legacy_kerning(&point_type("0 TA\n(A) Tx\n0 -80 Tk", "V")); + assert_eq!(runs, [(s("A"), Some(-80.0), 0.0), (s("V"), Some(0.0), 0.0)]); + // Inside a run, the character before it takes the value alone. + let runs = legacy_kerning(&point_type("(WAVE) Tx\n0 50 Tk", "S")); + assert_eq!(runs, [(s("WAV"), None, 0.0), (s("E"), Some(50.0), 0.0), (s("S"), None, 0.0)]); + // `autoKern` 1 gives the pair the automatic kerning, whatever its value. + let runs = legacy_kerning(&point_type("0 TA\n(AV) Tx\n1 -50 Tk", "A")); + assert_eq!(runs, [(s("A"), Some(0.0), 0.0), (s("V"), None, 0.0), (s("A"), Some(0.0), 0.0)]); + // Pairs with the same value in a row stay one run. + let runs = legacy_kerning(&point_type("0 TA\n(A) Tx\n0 50 Tk\n(B) Tx\n0 50 Tk", "C")); + assert_eq!(runs, [(s("AB"), Some(50.0), 0.0), (s("C"), Some(0.0), 0.0)]); + // At the start of the text and of a paragraph, the space before the line's first character. + let runs = legacy_kerning(&point_type("0 -30 Tk", "Hi")); + assert_eq!(runs, [(s("H"), None, -30.0), (s("i"), None, 0.0)]); + let runs = legacy_kerning(&point_type("(a\\r) Tx\n0 40 Tk", "b")); + assert_eq!(runs, [(s("a\n"), None, 0.0), (s("b"), None, 40.0)]); + let runs = legacy_kerning(&point_type("(a) Tx\n(\\r) TX\n0 40 TK", "b")); + assert_eq!(runs, [(s("a\n"), None, 0.0), (s("b"), None, 40.0)]); + // In overflow text. + let runs = legacy_kerning(&point_type("(ab) Tx\n0 25 TK", "c").replace("(c) Tx", "(c) TX")); + assert_eq!(runs, [(s("a"), None, 0.0), (s("b"), Some(25.0), 0.0), (s("c"), None, 0.0)]); + // Kept to the range `text.setRangeStyle` takes. + let runs = legacy_kerning(&point_type("(a) Tx\n0 -5000 Tk\n(b) Tx\n0 20000 Tk", "c")); + assert_eq!(runs, [(s("a"), Some(-1000.0), 0.0), (s("b"), Some(10_000.0), 0.0), (s("c"), None, 0.0)]); + // The pair is spaced as the kerning says. + let x = |art: &str| legacy_origins(&legacy_texts(art)[0])[1].0; + let (plain, kerned) = (x(&point_type("0 TA\n(A) Tx", "V")), x(&point_type("0 TA\n(A) Tx\n0 -80 Tk", "V"))); + assert!((plain - kerned - 0.96).abs() < 1e-6, "{plain} {kerned}"); +} + +/// A `Tk` whose operands are missing or are not numbers, or whose flag is not 0 or 1, and a `Tk` +/// outside a text object leave the type as it was. +#[test] +fn legacy_pair_kerning_with_bad_operands_leaves_the_type_as_it_was() { + let want = legacy_kerning(&point_type("(H) Tx", "i")); + for bad in ["0 Tk", "Tk", "/x 50 Tk", "0 /x Tk", "2 50 Tk", "0.5 50 Tk", "(0) 50 Tk"] { + assert_eq!(legacy_kerning(&point_type(&format!("(H) Tx\n{bad}"), "i")), want, "{bad}"); + } + let outside = legacy_kerning(&format!("0 50 Tk\n{}", point_type("(H) Tx", "i"))); + assert_eq!(outside, want); +} + +/// `T+` (a discretionary hyphen, §15.4.10.1) is a soft hyphen where it stands, in overflow text +/// too; `T-` makes the printed hyphen before it a soft hyphen, or adds one. Outside a text object +/// they add nothing. +#[test] +fn legacy_discretionary_hyphens_are_soft_hyphens() { + for (art, text) in [ + (point_type("(co) Tx\nT+", "operate"), "co\u{ad}operate"), + (point_type("(co-) Tx\nT-", "operate"), "co\u{ad}operate"), + (point_type("(co) Tx\nT-", "operate"), "co\u{ad}operate"), + (point_type("(co) Tx\nT+", "operate").replace("(operate) Tx", "(operate) TX"), "co\u{ad}operate"), + (format!("T+\nT-\n{}", point_type("", "co")), "co"), + ] { + let texts = legacy_texts(&art); + assert_eq!(texts[0].plain_text(), text, "{art}"); + } +} + +/// `languageID XL` (§22.6) is the characters' language, 0 (U.S. English) the default; it lasts into +/// the next text object, and an ID that is not 0 to 11 leaves the language as it was. +#[test] +fn legacy_hyphenation_language_is_kept() { + let lang = |t: &vectorcraft_doc::TextObject| t.runs.iter().map(|r| r.style.language.clone()).collect::>(); + let texts = legacy_texts(&format!("{}{}", point_type("%_ 3 XL\n(a) Tx\n%_ 0 XL\n(b) Tx\n%_ 8 XL", "c"), point_type("", "d"))); + assert_eq!(lang(&texts[0]), [Some("de".to_string()), None, Some("nb".to_string())]); + assert_eq!(lang(&texts[1]), [Some("nb".to_string())]); + for bad in ["12 XL", "-1 XL", "1.5 XL", "/x XL", "XL"] { + let texts = legacy_texts(&point_type(&format!("2 XL\n{bad}"), "a")); + assert_eq!(lang(&texts[0]), [Some("fr".to_string())], "{bad}"); + } +} + +/// The line breaking value (`TG`) and the Japanese layout rules (`Xu`, `Tu`, §22.6) are not kept: +/// a visible text object read while one is set gives a warning, once for each in a file. A copy of +/// a text object on `%_` lines gives none, and the final-form positions (§15.4.7) give none. +#[test] +fn legacy_line_breaking_and_japanese_layout_rules_are_warned_once() { + const LINE_BREAKING: &str = "the Line Breaking setting of legacy type was left out"; + const JAPANESE_LAYOUT: &str = "the Japanese layout rules of legacy type were left out"; + let r = legacy_read(&format!("{}{}", point_type("50 TG\n0 1 120 150 Xu", "a"), point_type("60 TG", "b"))); + assert_eq!(r.warnings, [LINE_BREAKING, JAPANESE_LAYOUT]); + assert_eq!(legacy_read(&point_type("0 1 120 150 Tu", "a")).warnings, [JAPANESE_LAYOUT]); + let hidden: String = point_type("50 TG", "a").lines().map(|l| format!("%_{l}\n")).collect(); + assert_eq!(legacy_read(&hidden).warnings, Vec::::new()); + let final_form = point_type("1 0 0 1 10 50 Tm\n0 0 Td\nT*\n0 Tc\n0 Tw\n1 0 0 1 0 0 TR\nTe\nTM", "Hi"); + let r = legacy_read(&final_form); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert_eq!(art_texts(&r.doc)[0].plain_text(), "Hi"); +} + /// Spacing is kept to the ranges the Justification options take, a Minimum, Desired and Maximum /// out of order go back to their defaults, and hyphenation's letter counts and limit to the ranges /// `text.setFormat` takes. diff --git a/crates/eps/src/import/tests_illustrator.rs b/crates/eps/src/import/tests_illustrator.rs index 70363dfaf7..19d89ad33f 100644 --- a/crates/eps/src/import/tests_illustrator.rs +++ b/crates/eps/src/import/tests_illustrator.rs @@ -208,15 +208,15 @@ fn a_postscript_ai_with_a_multi_layer_mask_opens_with_its_layers() { assert!(r.warnings.iter().any(|w| w.contains("clipping group in each layer")), "{:?}", r.warnings); } -/// A `.ai` in PostScript form (read through its layers) whose text carries hyphenation and -/// tab-ruler attributes: `XL` (hyphenation language), `Xb`/`Xe`/`XB` (a tab definition) and `Th` -/// (hyphen control). The reader drops `XL` and keeps the tab definition as the paragraph's tab -/// stops rather than failing the file the way it did before (the symptom seen on Illustrator 6/8 -/// EPS files whose `Adobe_level2_AI5` program can't be run). +/// A `.ai` in PostScript form (read through its layers) whose text carries hyphenation, tab-ruler +/// and kerning attributes: `XL` (hyphenation language), `Xb`/`Xe`/`XB` (a tab definition), `Th` +/// (hyphen control), `Tk` (a manual kerning pair) and `T+` (a discretionary hyphen). The reader +/// keeps the language, the tab definition as the paragraph's tab stops, the pair's kerning and the +/// soft hyphen. #[test] fn text_with_hyphenation_and_tab_attributes_opens_instead_of_failing() { let text = "0 To\n1 0 0 1 10 50 0 Tp\nTP\n0 Tr\n0 0 0 1 k\n/_Helvetica 9 Tf\n\ - %_ 0 XL\n36 1 Xb\n0 0 3 72 Xe\nXB\n0 1 2 2 99 Th\n0 Ta\n(hi) Tx\nTO\n"; + %_ 3 XL\n36 1 Xb\n0 0 3 72 Xe\nXB\n0 1 2 2 99 Th\n0 Ta\n(h) Tx\n0 -40 Tk\n(i) Tx\nT+\n(ho) Tx\nTO\n"; // The operators sit on a visible layer (so they'd be reported as unknown, not quietly skipped // on a hidden one), beside the square so the layer is never empty. let src = NAMED_PROLOG.replace("S\nLB\n%AI5_EndLayer--\n%AI5_BeginLayer", &format!("S\n{text}LB\n%AI5_EndLayer--\n%AI5_BeginLayer")); @@ -228,10 +228,14 @@ fn text_with_hyphenation_and_tab_attributes_opens_instead_of_failing() { let mut texts = vec![]; r.document.walk(|n| { if let NodeKind::Text(t) = &n.kind { - texts.push(t.para_at(0).clone()); + texts.push((**t).clone()); } }); - let [p] = texts.as_slice() else { panic!("{texts:?}") }; + let [t] = texts.as_slice() else { panic!("{texts:?}") }; + assert_eq!(t.plain_text(), "hi\u{ad}ho"); + let runs: Vec<_> = t.runs.iter().map(|r| (r.text.as_str(), r.style.kerning, r.style.language.as_deref())).collect(); + assert_eq!(runs, [("h", Some(-40.0), Some("de")), ("i\u{ad}ho", None, Some("de"))]); + let p = t.para_at(0); assert!(!p.hyphenate); assert_eq!((p.hyphenation.after_first, p.hyphenation.before_last, p.hyphenation.hyphen_limit), (2, 2, 25)); let tabs: Vec<_> = p.tabs.iter().map(|t| (t.position, t.align)).collect(); diff --git a/crates/eps/src/legacy_ai.rs b/crates/eps/src/legacy_ai.rs index 042a7ed2d2..d56769c2d7 100644 --- a/crates/eps/src/legacy_ai.rs +++ b/crates/eps/src/legacy_ai.rs @@ -33,7 +33,9 @@ //! - Type (§15): point type, area type and type on a path as text objects, `type To`, `a b c d tx //! ty startPt Tp` (the text matrix; the frame or path in art space between it and `TP`), each //! run's paint style, `Tr` (fill, stroke, both or neither), `/_font size ascent descent Tf`, -//! `Ts Tz Tt TA Tl Ta Ti` and its characters `(…) Tx` (`\r` ends a paragraph), `TO`. Standard +//! `Ts Tz Tt TA Tl Ta Ti` and its characters `(…) Tx` (`\r` ends a paragraph), with manual +//! kerning as `0 value Tk` after each character that has it and before a paragraph's first +//! character that has space before it (§15.4.6.1) and soft hyphens as `T+` (§15.4.10.1), `TO`. Standard //! Vertical Roman Alignment is written as `1 Tv` and `0 Tv`, and Tate-chu-yoko in horizontal //! type as a Kumi block recorded for later use, `1 TY` … `0 TY` (§15.3.4); neither operator is //! written for type that has neither mark. Only the @@ -99,7 +101,7 @@ const HEX_LINE: usize = 60; pub const SUBLAYERS: &str = "sublayers are written as groups in their top-level layer (legacy .ai files have no sublayers)"; pub const TYPE: &str = "type a legacy .ai can't hold as text (characters outside Windows Latin-1, vertical type, ruby, inline graphics, gradient or pattern fills) is written as outlines"; pub const ROTATED_TYPE: &str = "type with rotated characters is written as outlines, which keep the rotation, and is no longer editable text (legacy .ai text can't rotate characters)"; -pub const TYPE_ATTRIBUTES: &str = "type attributes legacy .ai files don't have (underline, strikethrough, manual kerning, optical kerning, small caps, superscript and subscript, OpenType features, Variable Font settings, optical margin alignment, System Layout, fractional widths off, area type options, type on a path's effect, alignment and spacing, an end bracket short of the path's end, subpaths after the first in an area type frame or a type path, space after paragraphs, tabs, bulleted and numbered lists) are left out"; +pub const TYPE_ATTRIBUTES: &str = "type attributes legacy .ai files don't have (underline, strikethrough, optical kerning, small caps, superscript and subscript, OpenType features, Variable Font settings, optical margin alignment, System Layout, fractional widths off, area type options, type on a path's effect, alignment and spacing, an end bracket short of the path's end, subpaths after the first in an area type frame or a type path, space after paragraphs, tabs, bulleted and numbered lists) are left out"; pub const DYNAMIC_TEXT: &str = "Dynamic Text is written as the sizes the type is drawn at (legacy .ai files don't have it)"; pub const HIDDEN: &str = "hidden objects are left out (hidden layers are kept, hidden)"; pub const HIDDEN_LAYER: &str = "hidden objects are moved to a hidden layer above their own (legacy .ai files can hide only layers)"; diff --git a/crates/eps/src/legacy_ai/encoding.rs b/crates/eps/src/legacy_ai/encoding.rs index 3882e37fe1..cb61e1e55b 100644 --- a/crates/eps/src/legacy_ai/encoding.rs +++ b/crates/eps/src/legacy_ai/encoding.rs @@ -119,7 +119,6 @@ pub(super) fn lost_attributes(t: &TextObject) -> bool { let run_lost = |s: &CharStyle| { s.underline || s.strikethrough - || s.kerning.is_some_and(|k| k != 0.0) || s.optical_kerning || s.small_caps.is_some() || s.position != CharPosition::Normal diff --git a/crates/eps/src/legacy_ai/prolog.rs b/crates/eps/src/legacy_ai/prolog.rs index 5ee77f89cf..38058e6abd 100644 --- a/crates/eps/src/legacy_ai/prolog.rs +++ b/crates/eps/src/legacy_ai/prolog.rs @@ -107,6 +107,7 @@ VCLegacy begin /To {pop} bd /Tp {7 {pop} repeat} bd /TP {} bd /TO {} bd /Tr {pop} bd /Tf {pop pop pop pop} bd /Ts {pop} bd /Tz {pop pop} bd /Tt {pop} bd /TA {pop} bd /Tl {pop pop} bd /Ta {pop} bd /Ti {pop pop pop} bd /Tx {pop} bd /Tv {pop} bd /TY {pop} bd +/Tk {pop pop} bd /T+ {} bd % The page trailer (3): annotatepage marks nothing. /annotatepage {} bd end diff --git a/crates/eps/src/legacy_ai/tests.rs b/crates/eps/src/legacy_ai/tests.rs index 76e96f5aa9..c6e3c05435 100644 --- a/crates/eps/src/legacy_ai/tests.rs +++ b/crates/eps/src/legacy_ai/tests.rs @@ -1454,12 +1454,12 @@ fn type_on_a_path_alignment_and_end_bracket_are_warned_as_left_out() { assert!(warned(&|t| t.kind = TextKind::OnPath { path: two(false), start: 0.25, end: None })); } -/// Kerning other than Auto or 0, optical kerning, Optical Margin Alignment, System Layout, -/// fractional widths off and an area type frame with a second subpath are not held by a legacy -/// `.ai` text object: type with any of them is written as text, with the warning that they are -/// left out. +/// Optical kerning, Optical Margin Alignment, System Layout, fractional widths off and an area +/// type frame with a second subpath are not held by a legacy `.ai` text object: type with any of +/// them is written as text, with the warning that they are left out. Manual kerning is held +/// (`Tk`), with no warning. #[test] -fn kerning_margins_widths_and_extra_frames_are_warned_as_left_out() { +fn optical_kerning_margins_widths_and_extra_frames_are_warned_as_left_out() { use vectorcraft_doc::{TextKind, TextObject}; let warned = |edit: &dyn Fn(&mut TextObject)| { let mut doc = typed(); @@ -1473,7 +1473,8 @@ fn kerning_margins_widths_and_extra_frames_are_warned_as_left_out() { }; assert!(!warned(&|_| {})); assert!(!warned(&|t| t.runs[0].style.kerning = Some(0.0))); - assert!(warned(&|t| t.runs[0].style.kerning = Some(50.0))); + assert!(!warned(&|t| t.runs[0].style.kerning = Some(50.0))); + assert!(!warned(&|t| t.runs[0].style.kerning_before = 50.0)); assert!(warned(&|t| t.runs[0].style.optical_kerning = true)); assert!(warned(&|t| t.optical_margin = true)); assert!(warned(&|t| t.system_layout = true)); @@ -1486,6 +1487,46 @@ fn kerning_margins_widths_and_extra_frames_are_warned_as_left_out() { assert!(warned(&|t| t.kind = TextKind::Area { frame: PathData::new(vec![square(10.0), square(120.0)]) })); } +/// Manual kerning is written as `0 value Tk` after each character that has it, the space before a +/// paragraph's first character as `Tk` before it, and a soft hyphen as a discretionary hyphen +/// (`T+`, §15.4.6.1 and §15.4.10.1); each reads back on the same characters. +#[test] +fn manual_kerning_and_soft_hyphens_are_written_and_read_back() { + use vectorcraft_doc::{CharStyle, TextObject, TextRun}; + let plain = style(12.0, Paint::solid(Color::BLACK)); + let kerned = CharStyle { kerning: Some(-80.0), ..plain.clone() }; + let before = CharStyle { kerning_before: 40.0, ..plain.clone() }; + let mut t = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), "AV", kerned.clone()); + for (text, st) in [("A co\u{ad}op\n", &plain), ("W", &before), ("ord", &plain), ("d\u{ad}", &kerned)] { + t.runs.push(TextRun::new(text, st.clone())); + } + let mut doc = Document::new(300.0, 200.0); + let layer = doc.layers[0].id; + let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t))); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + let out = write(&doc); + assert!(!out.warnings.iter().any(|w| w == TYPE || w == TYPE_ATTRIBUTES), "{:?}", out.warnings); + let text = String::from_utf8(out.bytes.clone()).unwrap(); + assert!(text.contains("(A) Tx\n0 -80 Tk\n(V) Tx\n0 -80 Tk\n"), "{text}"); + assert!(text.contains("(A co) Tx\nT+\n(op\\r) Tx\n"), "{text}"); + assert!(text.contains("0 40 Tk\n(W) Tx\n"), "{text}"); + // A soft hyphen with manual kerning keeps it too. + assert!(text.contains("(d) Tx\n0 -80 Tk\nT+\n0 -80 Tk\n"), "{text}"); + let read = texts(&crate::import(&out.bytes).unwrap().document); + let runs: Vec<(String, Option, f64)> = read[0].1.runs.iter().map(|r| (r.text.clone(), r.style.kerning, r.style.kerning_before)).collect(); + let s = |x: &str| x.to_string(); + assert_eq!( + runs, + [ + (s("AV"), Some(-80.0), 0.0), + (s("A co\u{ad}op\n"), None, 0.0), + (s("W"), None, 40.0), + (s("ord"), None, 0.0), + (s("d\u{ad}"), Some(-80.0), 0.0) + ] + ); +} + /// Type whose font size measures the cap height is written at the em size it is drawn at, with /// no warning: 14 pt measuring a 0.7 em cap height reads back as 20 pt type. #[test] diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index 06d9e0ddcc..5f749e15c8 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -833,6 +833,8 @@ impl<'a> Writer<'a> { self.line("TP"); let mut para = 0usize; let mut para_start = true; + // No character of the current paragraph has been written. + let mut line_start = true; for run in &t.runs { let text = if run.style.all_caps { run.text.to_uppercase() } else { run.text.clone() }; let mut pieces = text.split('\n').peekable(); @@ -846,14 +848,38 @@ impl<'a> Writer<'a> { para_start = false; } self.char_style(&run.style, t.para_at(para)); - let mut bytes: Vec = piece.chars().filter_map(latin::encode).collect(); + // The space before a line's first character, kerning before it (§15.4.6.1). + if line_start && !piece.is_empty() && run.style.kerning_before != 0.0 { + self.kern(run.style.kerning_before); + } + line_start = false; + // Manual kerning after each character, a soft hyphen as a discretionary hyphen + // (§15.4.10.1). + let kerning = run.style.kerning.filter(|k| *k != 0.0); + let mut bytes = vec![]; + for c in piece.chars() { + if c == '\u{ad}' { + self.text_string(&mut bytes); + self.line("T+"); + if let Some(k) = kerning { + self.kern(k); + } + continue; + } + let Some(b) = latin::encode(c) else { continue }; + bytes.push(b); + if let Some(k) = kerning { + self.text_string(&mut bytes); + self.kern(k); + } + } if ends_paragraph { bytes.push(b'\r'); para += 1; para_start = true; + line_start = true; } - self.body.push_str(&ps::bytes(&bytes)); - self.line(" Tx"); + self.text_string(&mut bytes); } } if self.kumi { @@ -863,6 +889,22 @@ impl<'a> Writer<'a> { self.line("TO"); } + /// `bytes` as a string of characters, `(…) Tx`, when there are any; `bytes` is left empty. + fn text_string(&mut self, bytes: &mut Vec) { + if !bytes.is_empty() { + self.body.push_str(&ps::bytes(bytes)); + self.line(" Tx"); + bytes.clear(); + } + } + + /// Manual kerning of `k` 1/1000 em between the characters before and after it: `0 k Tk`. + fn kern(&mut self, k: f64) { + self.body.push_str("0 "); + ps::push_num(&mut self.body, finite(k, 0.0)); + self.line(" Tk"); + } + /// A paragraph's attributes: alignment `Ta` and indents `Ti` (first line's, left, right). fn paragraph(&mut self, p: &ParaStyle) { let align = match p.justify { diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index ff72685662..106f83607a 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4818,6 +4818,8 @@ axis Both И двете @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers вградено изображение от {_1} × {_2} пиксела беше намалено до {_3} × {_4} пиксела, за да се запазят слоевете @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel свързаните картини показват записания във файла предварителен преглед с ниска резолюция, докато файловете им бъдат намерени: презакачете ги в панела „Връзки“ @msg linked type areas past VectorCraft's limits were left out свързаните текстови области извън ограниченията на VectorCraft бяха пропуснати +@msg the Line Breaking setting of legacy type was left out настройката за прекъсване на редовете на текста в стария формат беше пропусната +@msg the Japanese layout rules of legacy type were left out японските правила за оформление на текста в стария формат бяха пропуснати @msg type that changes writing direction inside a text object opens in the direction of its first character текст, който сменя посоката на писане в рамките на текстов обект, се отваря в посоката на първия си знак @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip текст, използван като маска в свързани области, обтичащ текст или съставен път, не изрязва @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel свързаните картини без предварителен преглед във файла показват заместител, докато файловете им бъдат намерени: презакачете ги в панела „Връзки“ diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 0c77f90ad8..a0674f5565 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5216,6 +5216,8 @@ imageTrace Outline Obris @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers ugrađena slika od {_1} × {_2} piksela smanjena je na {_3} × {_4} piksela kako bi se zadržali slojevi @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel povezane slike prikazuju pregled niske rezolucije sačuvan u datoteci dok se njihove datoteke ne pronađu: ponovo ih povežite u panelu Veze @msg linked type areas past VectorCraft's limits were left out povezana područja teksta izvan ograničenja programa VectorCraft su izostavljena +@msg the Line Breaking setting of legacy type was left out postavka prijeloma redova teksta u starom formatu je izostavljena +@msg the Japanese layout rules of legacy type were left out japanska pravila rasporeda teksta u starom formatu su izostavljena @msg type that changes writing direction inside a text object opens in the direction of its first character tekst koji mijenja smjer pisanja unutar tekstualnog objekta otvara se u smjeru svog prvog znaka @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip tekst korišten kao maska u povezanim područjima, omotanom tekstu ili složenoj putanji ne odsijeca @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel povezane slike bez pregleda u datoteci prikazuju čuvar mjesta dok se njihove datoteke ne pronađu: ponovo ih povežite u panelu Veze diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 6601948ba6..f373fbeed0 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5667,6 +5667,8 @@ digits Malayalam Malaiàlam @msg library panel `{given}` isn't open el panell de biblioteca `{given}` no està obert @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel les imatges enllaçades mostren la previsualització de baixa resolució desada al fitxer fins que es trobin els seus fitxers: torneu-les a enllaçar al panell Enllaços @msg linked type areas past VectorCraft's limits were left out s’han omès les àrees de text enllaçades que superen els límits de VectorCraft +@msg the Line Breaking setting of legacy type was left out s’ha omès l’ajust de salt de línia del text en format antic +@msg the Japanese layout rules of legacy type were left out s’han omès les regles de composició japonesa del text en format antic @msg type that changes writing direction inside a text object opens in the direction of its first character el text que canvia de direcció d’escriptura dins d’un objecte de text s’obre en la direcció del seu primer caràcter @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip el text usat com a màscara en àrees enllaçades, en text ajustat o en un traçat compost no retalla @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel les imatges enllaçades sense previsualització al fitxer mostren un marcador de posició fins que es trobin els seus fitxers: torneu-les a enllaçar al panell Enllaços diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index baef0a88e6..fde6979f71 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5414,6 +5414,8 @@ imageTrace Outline Obrys @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers vložený obrázek o rozměrech {_1} × {_2} pixelů byl zmenšen na {_3} × {_4} pixelů, aby se zachovaly vrstvy @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel propojené obrázky se zobrazují jako náhled v nízkém rozlišení uložený v souboru, dokud se nenajdou jejich soubory: znovu je propojte v panelu Propojení @msg linked type areas past VectorCraft's limits were left out propojené textové oblasti nad limity aplikace VectorCraft byly vynechány +@msg the Line Breaking setting of legacy type was left out nastavení zalamování řádků textu ve starém formátu bylo vynecháno +@msg the Japanese layout rules of legacy type were left out japonská pravidla sazby textu ve starém formátu byla vynechána @msg type that changes writing direction inside a text object opens in the direction of its first character text, který mění směr psaní uvnitř textového objektu, se otevře ve směru svého prvního znaku @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip text použitý jako maska v propojených oblastech, obtékaném textu nebo složené cestě neořezává @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel propojené obrázky bez náhledu v souboru se zobrazují jako zástupný symbol, dokud se nenajdou jejich soubory: znovu je propojte v panelu Propojení diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 9192974eec..f2c103aab4 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4903,6 +4903,8 @@ imageTrace Outline Kontur @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers et indlejret billede på {_1} × {_2} pixel blev formindsket til {_3} × {_4} pixel for at bevare lagene @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel linkede billeder viser den gemte forhåndsvisning i lav opløsning, indtil deres filer findes: link dem igen i Links-panelet @msg linked type areas past VectorCraft's limits were left out sammenkædede tekstområder ud over VectorCrafts grænser blev udeladt +@msg the Line Breaking setting of legacy type was left out indstillingen for linjeskift i tekst i det gamle format blev udeladt +@msg the Japanese layout rules of legacy type were left out de japanske layoutregler for tekst i det gamle format blev udeladt @msg type that changes writing direction inside a text object opens in the direction of its first character tekst, der skifter skriveretning inde i et tekstobjekt, åbnes i retningen for sit første tegn @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip tekst brugt som maske i sammenkædede områder, ombrudt tekst eller en sammensat kurve beskærer ikke @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel linkede billeder uden forhåndsvisning i filen viser en pladsholder, indtil deres filer findes: link dem igen i Links-panelet diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 78e7fe186b..9049e8ae7b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5030,6 +5030,8 @@ theme Appearance Erscheinungsbild @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers ein eingebettetes Bild mit {_1} × {_2} Pixeln wurde auf {_3} × {_4} Pixel verkleinert, damit die Ebenen erhalten bleiben @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel verknüpfte Bilder zeigen die in der Datei gespeicherte Vorschau in niedriger Auflösung, bis ihre Dateien gefunden werden: Verknüpfen Sie sie im Bedienfeld „Verknüpfungen“ neu @msg linked type areas past VectorCraft's limits were left out verkettete Textbereiche jenseits der Grenzen von VectorCraft wurden ausgelassen +@msg the Line Breaking setting of legacy type was left out die Einstellung für den Zeilenumbruch von Text im alten Format wurde ausgelassen +@msg the Japanese layout rules of legacy type were left out die japanischen Satzregeln von Text im alten Format wurden ausgelassen @msg type that changes writing direction inside a text object opens in the direction of its first character Text, der innerhalb eines Textobjekts die Schreibrichtung wechselt, wird in der Richtung seines ersten Zeichens geöffnet @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip als Maske verwendeter Text in verketteten Bereichen, umfließendem Text oder einem zusammengesetzten Pfad beschneidet nicht @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel verknüpfte Bilder ohne Vorschau in der Datei zeigen einen Platzhalter, bis ihre Dateien gefunden werden: Verknüpfen Sie sie im Bedienfeld „Verknüpfungen“ neu diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 82f265b702..1a837b192d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5053,6 +5053,8 @@ digits Malayalam Μαλαγιάλαμ @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers μια ενσωματωμένη εικόνα {_1} × {_2} pixel μειώθηκε σε {_3} × {_4} pixel για να διατηρηθούν τα επίπεδα @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel οι συνδεδεμένες εικόνες εμφανίζουν την προεπισκόπηση χαμηλής ανάλυσης που έχει αποθηκευτεί, μέχρι να βρεθούν τα αρχεία τους: επανασυνδέστε τις στον πίνακα Σύνδεσμοι @msg linked type areas past VectorCraft's limits were left out οι συνδεδεμένες περιοχές κειμένου πέρα από τα όρια του VectorCraft παραλείφθηκαν +@msg the Line Breaking setting of legacy type was left out η ρύθμιση αλλαγής γραμμής του κειμένου παλιάς μορφής παραλείφθηκε +@msg the Japanese layout rules of legacy type were left out οι ιαπωνικοί κανόνες διάταξης του κειμένου παλιάς μορφής παραλείφθηκαν @msg type that changes writing direction inside a text object opens in the direction of its first character το κείμενο που αλλάζει κατεύθυνση γραφής μέσα σε ένα αντικείμενο κειμένου ανοίγει με την κατεύθυνση του πρώτου χαρακτήρα του @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip το κείμενο που χρησιμοποιείται ως μάσκα σε συνδεδεμένες περιοχές, σε κείμενο με αναδίπλωση ή σε σύνθετη διαδρομή δεν αποκόπτει @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel οι συνδεδεμένες εικόνες χωρίς προεπισκόπηση στο αρχείο εμφανίζουν ένα σύμβολο κράτησης θέσης, μέχρι να βρεθούν τα αρχεία τους: επανασυνδέστε τις στον πίνακα Σύνδεσμοι diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e0aaffde87..b6a8ff7f74 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5106,6 +5106,8 @@ axis Both Ambos @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers una imagen incrustada de {_1} × {_2} píxeles se redujo a {_3} × {_4} píxeles para conservar las capas @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel las imágenes vinculadas muestran la vista previa de baja resolución guardada en el archivo hasta que se encuentren sus archivos: vuelva a vincularlas en el panel Vínculos @msg linked type areas past VectorCraft's limits were left out se omitieron las áreas de texto enlazadas que superan los límites de VectorCraft +@msg the Line Breaking setting of legacy type was left out se omitió el ajuste de salto de línea del texto en formato antiguo +@msg the Japanese layout rules of legacy type were left out se omitieron las reglas de composición japonesa del texto en formato antiguo @msg type that changes writing direction inside a text object opens in the direction of its first character el texto que cambia de dirección de escritura dentro de un objeto de texto se abre en la dirección de su primer carácter @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip el texto usado como máscara en áreas enlazadas, texto ceñido o un trazado compuesto no recorta @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel las imágenes vinculadas sin vista previa en el archivo muestran un marcador de posición hasta que se encuentren sus archivos: vuelva a vincularlas en el panel Vínculos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 1ea537adbe..1da2d999f4 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5313,6 +5313,8 @@ imageTrace Outline Ääriviiva @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers upotettu {_1} × {_2} kuvapisteen kuva pienennettiin kokoon {_3} × {_4} kuvapistettä, jotta tasot säilyvät @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel linkitetyt kuvat näyttävät tiedostoon tallennetun matalan tarkkuuden esikatselun, kunnes niiden tiedostot löytyvät: linkitä ne uudelleen Linkit-paneelissa @msg linked type areas past VectorCraft's limits were left out VectorCraftin rajat ylittävät linkitetyt tekstialueet jätettiin pois +@msg the Line Breaking setting of legacy type was left out vanhan muodon tekstin rivinvaihtoasetus jätettiin pois +@msg the Japanese layout rules of legacy type were left out vanhan muodon tekstin japanilaiset asettelusäännöt jätettiin pois @msg type that changes writing direction inside a text object opens in the direction of its first character tekstiobjektin sisällä kirjoitussuuntaa vaihtava teksti avautuu ensimmäisen merkkinsä suuntaan @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip maskina käytetty teksti linkitetyissä alueissa, rivitetyssä tekstissä tai yhdistelmäpolussa ei rajaa @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel linkitetyt kuvat, joilla ei ole esikatselua tiedostossa, näkyvät paikkamerkkinä, kunnes niiden tiedostot löytyvät: linkitä ne uudelleen Linkit-paneelissa diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 96d6f7dd13..57f6e66f8c 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5093,6 +5093,8 @@ axis Both Les deux @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers une image incorporée de {_1} × {_2} pixels a été réduite à {_3} × {_4} pixels pour conserver les calques @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel les images liées affichent l’aperçu basse résolution enregistré dans le fichier tant que leurs fichiers ne sont pas trouvés : liez-les à nouveau dans le panneau Liens @msg linked type areas past VectorCraft's limits were left out les zones de texte liées au-delà des limites de VectorCraft ont été omises +@msg the Line Breaking setting of legacy type was left out le réglage de coupure de ligne du texte à l’ancien format a été omis +@msg the Japanese layout rules of legacy type were left out les règles de composition japonaise du texte à l’ancien format ont été omises @msg type that changes writing direction inside a text object opens in the direction of its first character le texte qui change de sens d’écriture dans un objet texte s’ouvre dans le sens de son premier caractère @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip le texte utilisé comme masque dans des zones liées, un habillage de texte ou un tracé transparent ne découpe pas @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel les images liées sans aperçu dans le fichier affichent un espace réservé tant que leurs fichiers ne sont pas trouvés : liez-les à nouveau dans le panneau Liens diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index b9401b2b77..7f9a43ca6f 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8309,6 +8309,8 @@ antiAlias Sharp शार्प @msg type dimension must be finite टाइप का आयाम परिमित होना चाहिए @msg type must be static or dynamic, not {v} type: प्रकार स्थिर या गतिशील होना चाहिए, न कि {v} @msg type objects can't be isolated टाइप ऑब्जेक्ट्स को आइसोलेट नहीं किया जा सकता +@msg the Line Breaking setting of legacy type was left out पुराने प्रारूप के टेक्स्ट की लाइन ब्रेकिंग सेटिंग छोड़ दी गई +@msg the Japanese layout rules of legacy type were left out पुराने प्रारूप के टेक्स्ट के जापानी लेआउट नियम छोड़ दिए गए @msg type that changes writing direction inside a text object opens in the direction of its first character टेक्स्ट ऑब्जेक्ट के भीतर लेखन दिशा बदलने वाला टेक्स्ट अपने पहले वर्ण की दिशा में खुलता है @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip लिंक्ड एरिया, रैपअराउंड टेक्स्ट या कंपाउंड पाथ में मास्क के रूप में इस्तेमाल किया गया टाइप क्लिप नहीं करता @msg Unembedded to {_1} एम्बेड हटाकर {_1} में रखा गया diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 89b517cdb2..9f1dd9c469 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4959,6 +4959,8 @@ imageTrace Outline Körvonal @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers egy {_1} × {_2} képpontos beágyazott kép {_3} × {_4} képpontosra lett kicsinyítve, hogy a rétegek megmaradjanak @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel a csatolt képek a fájlban mentett kis felbontású előnézetet mutatják, amíg a fájljaik elő nem kerülnek: csatold őket újra a Csatolások panelen @msg linked type areas past VectorCraft's limits were left out a VectorCraft korlátain túli csatolt szövegterületek kimaradtak +@msg the Line Breaking setting of legacy type was left out a régi formátumú szöveg sortörési beállítása kimaradt +@msg the Japanese layout rules of legacy type were left out a régi formátumú szöveg japán tördelési szabályai kimaradtak @msg type that changes writing direction inside a text object opens in the direction of its first character a szövegobjektumon belül írásirányt váltó szöveg az első karakterének irányában nyílik meg @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip a csatolt területeken, körbefuttatott szövegben vagy összetett útvonalban maszkként használt szöveg nem vág ki @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel a fájlban előnézet nélküli csatolt képek helyőrzőt mutatnak, amíg a fájljaik elő nem kerülnek: csatold őket újra a Csatolások panelen diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 543723217f..56891395ad 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4903,6 +4903,8 @@ imageTrace Outline Kontur @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers gambar tersemat berukuran {_1} × {_2} piksel diperkecil menjadi {_3} × {_4} piksel agar lapisannya tetap terjaga @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel gambar tertaut menampilkan pratinjau resolusi rendah yang tersimpan dalam berkas sampai berkasnya ditemukan: tautkan ulang di panel Tautan @msg linked type areas past VectorCraft's limits were left out area teks tertaut di luar batas VectorCraft dihilangkan +@msg the Line Breaking setting of legacy type was left out pengaturan pemisah baris teks format lama dihilangkan +@msg the Japanese layout rules of legacy type were left out aturan tata letak Jepang teks format lama dihilangkan @msg type that changes writing direction inside a text object opens in the direction of its first character teks yang mengubah arah penulisan di dalam objek teks dibuka dengan arah karakter pertamanya @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip teks yang digunakan sebagai masker di area tertaut, teks terbungkus, atau jalur majemuk tidak memotong @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel gambar tertaut tanpa pratinjau dalam berkas menampilkan penanda tempat sampai berkasnya ditemukan: tautkan ulang di panel Tautan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2b17326766..2f70a56bae 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5158,6 +5158,8 @@ axis Both Entrambi @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers un'immagine incorporata di {_1} × {_2} pixel è stata ridotta a {_3} × {_4} pixel per mantenere i livelli @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel le immagini collegate mostrano l'anteprima a bassa risoluzione salvata nel file finché i loro file non vengono trovati: ricollegale nel pannello Collegamenti @msg linked type areas past VectorCraft's limits were left out le aree di testo concatenate oltre i limiti di VectorCraft sono state omesse +@msg the Line Breaking setting of legacy type was left out l’impostazione di interruzione di riga del testo in formato precedente è stata omessa +@msg the Japanese layout rules of legacy type were left out le regole di composizione giapponese del testo in formato precedente sono state omesse @msg type that changes writing direction inside a text object opens in the direction of its first character il testo che cambia direzione di scrittura all’interno di un oggetto di testo si apre nella direzione del suo primo carattere @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip il testo usato come maschera in aree concatenate, testo attorno o un tracciato composto non ritaglia @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel le immagini collegate senza anteprima nel file mostrano un segnaposto finché i loro file non vengono trovati: ricollegale nel pannello Collegamenti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3d71e2eb0e..09d3dce67a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4700,6 +4700,8 @@ swatch Bright Violet ビビッドバイオレット @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers レイヤーを保持するため、{_1} × {_2} ピクセルの埋め込み画像を {_3} × {_4} ピクセルに縮小しました @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel リンク画像は、元のファイルが見つかるまで、ファイルに保存された低解像度のプレビューで表示されます:リンクパネルで再リンクしてください @msg linked type areas past VectorCraft's limits were left out VectorCraft の上限を超えるリンクされたテキストエリアは省略されました +@msg the Line Breaking setting of legacy type was left out 旧形式のテキストの行分割の設定は読み込まれませんでした +@msg the Japanese layout rules of legacy type were left out 旧形式のテキストの日本語組版ルールは読み込まれませんでした @msg type that changes writing direction inside a text object opens in the direction of its first character テキストオブジェクト内で書字方向が変わる文字は、最初の文字の方向で開きます @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip リンクされたエリア、テキストの回り込み、複合パス内でマスクとして使われた文字はクリップしません @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel ファイルにプレビューのないリンク画像は、元のファイルが見つかるまでプレースホルダーで表示されます:リンクパネルで再リンクしてください diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 0c228dfb32..f085feb92f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5139,6 +5139,8 @@ font-technology Variable ცვლადი Stacked დასტად @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel დაკავშირებული გამოსახულებები ფაილში შენახულ დაბალი გარჩევადობის მინიატურას აჩვენებს, სანამ მათი ფაილები არ მოიძებნება: დაუკავშირეთ ისინი ხელახლა ბმულების პანელში @msg linked type areas past VectorCraft's limits were left out VectorCraft-ის ლიმიტების მიღმა დაკავშირებული ტექსტის არეები გამოტოვებულია +@msg the Line Breaking setting of legacy type was left out ძველი ფორმატის ტექსტის სტრიქონის გადატანის პარამეტრი გამოტოვებულია +@msg the Japanese layout rules of legacy type were left out ძველი ფორმატის ტექსტის იაპონური განლაგების წესები გამოტოვებულია @msg type that changes writing direction inside a text object opens in the direction of its first character ტექსტი, რომელიც ტექსტური ობიექტის შიგნით ცვლის წერის მიმართულებას, იხსნება მისი პირველი სიმბოლოს მიმართულებით @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip დაკავშირებულ არეებში, გარსშემოვლებულ ტექსტში ან შედგენილ ტრაექტორიაში ნიღბად გამოყენებული ტექსტი არ ჭრის @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel დაკავშირებული გამოსახულებები, რომელთა მინიატურაც ფაილში არ არის, ადგილის შემნახველს აჩვენებს, სანამ მათი ფაილები არ მოიძებნება: დაუკავშირეთ ისინი ხელახლა ბმულების პანელში diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 53b8b5ed47..dbe0cced49 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4627,6 +4627,8 @@ theme Appearance 외관 @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers 레이어를 유지하기 위해 {_1} × {_2} 픽셀의 포함된 이미지를 {_3} × {_4} 픽셀로 줄였습니다 @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel 연결된 그림은 해당 파일을 찾을 때까지 파일에 저장된 저해상도 미리 보기로 표시됩니다: 링크 패널에서 다시 연결하세요 @msg linked type areas past VectorCraft's limits were left out VectorCraft 한도를 넘는 연결된 문자 영역은 제외되었습니다 +@msg the Line Breaking setting of legacy type was left out 이전 형식 문자의 줄 바꿈 설정이 생략되었습니다 +@msg the Japanese layout rules of legacy type were left out 이전 형식 문자의 일본어 조판 규칙이 생략되었습니다 @msg type that changes writing direction inside a text object opens in the direction of its first character 문자 개체 안에서 쓰기 방향이 바뀌는 문자는 첫 글자의 방향으로 열립니다 @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip 연결된 영역, 문자 감싸기 또는 컴파운드 패스에서 마스크로 사용된 문자는 클리핑하지 않습니다 @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel 파일에 미리 보기가 없는 연결된 그림은 해당 파일을 찾을 때까지 자리 표시자로 표시됩니다: 링크 패널에서 다시 연결하세요 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d38e0c113f..3d395404ee 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5418,6 +5418,8 @@ imageTrace Outline Contour @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers een ingesloten afbeelding van {_1} × {_2} pixels is verkleind tot {_3} × {_4} pixels om de lagen te behouden @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel gekoppelde afbeeldingen tonen het in het bestand opgeslagen voorbeeld met lage resolutie totdat hun bestanden zijn gevonden: koppel ze opnieuw in het paneel Koppelingen @msg linked type areas past VectorCraft's limits were left out gekoppelde tekstvlakken buiten de limieten van VectorCraft zijn weggelaten +@msg the Line Breaking setting of legacy type was left out de instelling voor regelafbreking van tekst in de oude indeling is weggelaten +@msg the Japanese layout rules of legacy type were left out de Japanse opmaakregels van tekst in de oude indeling zijn weggelaten @msg type that changes writing direction inside a text object opens in the direction of its first character tekst die binnen een tekstobject van schrijfrichting verandert, wordt geopend in de richting van het eerste teken @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip tekst die als masker wordt gebruikt in gekoppelde vlakken, tekstomloop of een samengesteld pad knipt niet uit @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel gekoppelde afbeeldingen zonder voorbeeld in het bestand tonen een tijdelijke aanduiding totdat hun bestanden zijn gevonden: koppel ze opnieuw in het paneel Koppelingen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 0ae5431a81..f371dcb99d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5241,6 +5241,8 @@ imageTrace Outline Kontur @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers osadzony obraz o wymiarach {_1} × {_2} pikseli zmniejszono do {_3} × {_4} pikseli, aby zachować warstwy @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel połączone obrazy pokazują zapisany w pliku podgląd niskiej rozdzielczości, dopóki ich pliki nie zostaną znalezione: połącz je ponownie w panelu Łącza @msg linked type areas past VectorCraft's limits were left out połączone obszary tekstu poza limitami VectorCraft zostały pominięte +@msg the Line Breaking setting of legacy type was left out pominięto ustawienie łamania wierszy tekstu w starym formacie +@msg the Japanese layout rules of legacy type were left out pominięto japońskie reguły składu tekstu w starym formacie @msg type that changes writing direction inside a text object opens in the direction of its first character tekst zmieniający kierunek pisania wewnątrz obiektu tekstowego otwiera się w kierunku swojego pierwszego znaku @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip tekst użyty jako maska w połączonych obszarach, oblewanym tekście lub ścieżce złożonej nie przycina @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel połączone obrazy bez podglądu w pliku pokazują symbol zastępczy, dopóki ich pliki nie zostaną znalezione: połącz je ponownie w panelu Łącza diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 854894cb68..79b3651cdf 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5361,6 +5361,8 @@ axis Both Ambos @msg {path}: not a file {path}: não é um arquivo @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel as imagens vinculadas mostram a visualização de baixa resolução salva no arquivo até que seus arquivos sejam encontrados: revincule-as no painel Links @msg linked type areas past VectorCraft's limits were left out as áreas de texto encadeadas além dos limites do VectorCraft foram omitidas +@msg the Line Breaking setting of legacy type was left out a configuração de quebra de linha do texto no formato antigo foi omitida +@msg the Japanese layout rules of legacy type were left out as regras de composição japonesa do texto no formato antigo foram omitidas @msg type that changes writing direction inside a text object opens in the direction of its first character o texto que muda de direção de escrita dentro de um objeto de texto abre na direção do primeiro caractere @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip o texto usado como máscara em áreas encadeadas, texto com quebra ou um caminho composto não recorta @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel as imagens vinculadas sem visualização no arquivo mostram um espaço reservado até que seus arquivos sejam encontrados: revincule-as no painel Links diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9af5f4ceb9..653808aebb 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5143,6 +5143,8 @@ axis Both Оба @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers встроенное изображение {_1} × {_2} пикселей уменьшено до {_3} × {_4} пикселей, чтобы сохранить слои @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel связанные изображения показываются как сохранённый в файле просмотр низкого разрешения, пока их файлы не найдены: восстановите связи на панели «Связи» @msg linked type areas past VectorCraft's limits were left out связанные текстовые области за пределами ограничений VectorCraft пропущены +@msg the Line Breaking setting of legacy type was left out настройка разрыва строк текста старого формата пропущена +@msg the Japanese layout rules of legacy type were left out японские правила вёрстки текста старого формата пропущены @msg type that changes writing direction inside a text object opens in the direction of its first character текст, меняющий направление письма внутри текстового объекта, открывается в направлении своего первого символа @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip текст, используемый как маска в связанных областях, обтекающем тексте или составном контуре, не обрезает @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel связанные изображения без просмотра в файле показываются как заполнитель, пока их файлы не найдены: восстановите связи на панели «Связи» diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7c9d977648..7947001481 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5000,6 +5000,8 @@ axis Both Båda @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers en inbäddad bild på {_1} × {_2} pixlar förminskades till {_3} × {_4} pixlar för att behålla lagren @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel länkade bilder visar förhandsgranskningen med låg upplösning som sparats i filen tills deras filer hittas: länka om dem i panelen Länkar @msg linked type areas past VectorCraft's limits were left out länkade textområden utöver VectorCrafts gränser utelämnades +@msg the Line Breaking setting of legacy type was left out inställningen för radbrytning i text i det gamla formatet utelämnades +@msg the Japanese layout rules of legacy type were left out de japanska layoutreglerna för text i det gamla formatet utelämnades @msg type that changes writing direction inside a text object opens in the direction of its first character text som byter skrivriktning inuti ett textobjekt öppnas i riktningen för sitt första tecken @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip text som används som mask i länkade områden, ombruten text eller en sammansatt bana beskär inte @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel länkade bilder utan förhandsgranskning i filen visar en platshållare tills deras filer hittas: länka om dem i panelen Länkar diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 6a9f8d51fc..3de6c44ab8 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4836,6 +4836,8 @@ axis Both Her İkisi @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers katmanları korumak için {_1} × {_2} piksellik gömülü bir resim {_3} × {_4} piksele küçültüldü @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel bağlantılı resimler, dosyaları bulunana kadar dosyaya kaydedilen düşük çözünürlüklü önizlemeyi gösterir: bunları Bağlantılar panelinde yeniden bağlayın @msg linked type areas past VectorCraft's limits were left out VectorCraft sınırlarını aşan bağlantılı metin alanları dışarıda bırakıldı +@msg the Line Breaking setting of legacy type was left out eski biçimdeki metnin satır kesme ayarı atlandı +@msg the Japanese layout rules of legacy type were left out eski biçimdeki metnin Japonca dizgi kuralları atlandı @msg type that changes writing direction inside a text object opens in the direction of its first character bir metin nesnesi içinde yazı yönünü değiştiren metin, ilk karakterinin yönünde açılır @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip bağlantılı alanlarda, kaydırılan metinde veya bileşik yolda maske olarak kullanılan metin kırpmaz @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel dosyada önizlemesi olmayan bağlantılı resimler, dosyaları bulunana kadar bir yer tutucu gösterir: bunları Bağlantılar panelinde yeniden bağlayın diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 8681297977..21d0890caa 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5142,6 +5142,8 @@ axis Both Обидві @msg an embedded image of {_1} × {_2} pixels was reduced to {_3} × {_4} pixels to keep the layers вбудоване зображення {_1} × {_2} пікселів зменшено до {_3} × {_4} пікселів, щоб зберегти шари @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel пов’язані зображення показуються як збережений у файлі перегляд низької роздільності, доки їхні файли не знайдено: відновіть зв’язки на панелі «Зв’язки» @msg linked type areas past VectorCraft's limits were left out пов’язані текстові області понад обмеження VectorCraft пропущено +@msg the Line Breaking setting of legacy type was left out налаштування розриву рядків тексту старого формату пропущено +@msg the Japanese layout rules of legacy type were left out японські правила верстки тексту старого формату пропущено @msg type that changes writing direction inside a text object opens in the direction of its first character текст, що змінює напрямок письма всередині текстового об’єкта, відкривається в напрямку свого першого символу @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip текст, використаний як маска у пов’язаних областях, тексті з обтіканням або складеному контурі, не обрізає @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel пов’язані зображення без перегляду у файлі показуються як заповнювач, доки їхні файли не знайдено: відновіть зв’язки на панелі «Зв’язки» diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 516ba15672..5768410653 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4930,6 +4930,8 @@ theme Appearance Giao diện @msg line {line}: value for `{var}` is longer than {MAX_GRAPH_CSV} bytes dòng {line}: giá trị của `{var}` dài hơn {MAX_GRAPH_CSV} byte @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel ảnh liên kết hiển thị bản xem trước độ phân giải thấp đã lưu trong tệp cho đến khi tìm thấy tệp gốc: liên kết lại trong bảng Liên kết @msg linked type areas past VectorCraft's limits were left out các vùng chữ liên kết vượt quá giới hạn của VectorCraft đã bị bỏ qua +@msg the Line Breaking setting of legacy type was left out thiết lập ngắt dòng của chữ định dạng cũ đã bị bỏ qua +@msg the Japanese layout rules of legacy type were left out các quy tắc dàn trang tiếng Nhật của chữ định dạng cũ đã bị bỏ qua @msg type that changes writing direction inside a text object opens in the direction of its first character chữ đổi hướng viết bên trong một đối tượng văn bản sẽ mở theo hướng của ký tự đầu tiên @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip chữ dùng làm mặt nạ trong các vùng liên kết, chữ bao quanh hoặc đường dẫn phức không cắt xén @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel ảnh liên kết không có bản xem trước trong tệp hiển thị ô giữ chỗ cho đến khi tìm thấy tệp gốc: liên kết lại trong bảng Liên kết diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 35d92d6434..7d1d8e65a5 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3701,6 +3701,8 @@ digits Malayalam 马拉雅拉姆数字 @plural {n} linked image changed on disk. Show the new version?|{n} linked images changed on disk. Show the new versions? 有 {n} 个链接图像已在磁盘上更改。要显示新版本吗? @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel 链接的图片在找到其文件之前显示文件中存储的低分辨率预览:请在“链接”面板中重新链接 @msg linked type areas past VectorCraft's limits were left out 超出 VectorCraft 限制的串接文字区域已省略 +@msg the Line Breaking setting of legacy type was left out 旧格式文字的换行设置已省略 +@msg the Japanese layout rules of legacy type were left out 旧格式文字的日文排版规则已省略 @msg type that changes writing direction inside a text object opens in the direction of its first character 在文字对象内更改书写方向的文字按其第一个字符的方向打开 @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip 在串接区域、绕排文字或复合路径中用作蒙版的文字不会剪切 @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel 文件中没有预览的链接图片在找到其文件之前显示占位符:请在“链接”面板中重新链接 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index dafc57aa29..1503cde356 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4946,6 +4946,8 @@ digits Malayalam 馬來亞拉姆數字 @plural {n} linked image changed on disk. Show the new version?|{n} linked images changed on disk. Show the new versions? 有 {n} 個連結影像已在磁碟上變更。要顯示新版本嗎? @msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel 連結的圖片在找到其檔案之前顯示檔案中儲存的低解析度預覽:請在「連結」面板中重新連結 @msg linked type areas past VectorCraft's limits were left out 超出 VectorCraft 限制的串連文字區域已略過 +@msg the Line Breaking setting of legacy type was left out 已略過舊格式文字的換行設定 +@msg the Japanese layout rules of legacy type were left out 已略過舊格式文字的日文排版規則 @msg type that changes writing direction inside a text object opens in the direction of its first character 在文字物件內變更書寫方向的文字會依其第一個字元的方向開啟 @msg type used as a mask in linked areas, wraparound text or a compound path doesn't clip 在串連區域、繞圖文字或複合路徑中用作遮色片的文字不會剪裁 @msg linked pictures with no preview in the file show a placeholder until their files are found: relink them in the Links panel 檔案中沒有預覽的連結圖片在找到其檔案之前顯示預留位置:請在「連結」面板中重新連結 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index f752a8973b..25016def36 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -196,6 +196,11 @@ private editing data. VectorCraft: as that art, with one note that counts them. Type stays live without a comparison when the saved art paints nothing or draws type of its own, in frames of threaded type, when a text variable binds it, and when its text object doesn't come in as one text object. + Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`: the kerning after the character before the + pair, or at a line's start the space before its first character), its discretionary hyphens (`T+`, `T-`) as + soft hyphens and its hyphenation language (`XL`); a line breaking value (`TG`) and Japanese layout rules + (`Xu`, `Tu`) are not kept, with a warning. Legacy `.ai` saves write manual kerning as `Tk` and soft hyphens + as `T+`. Legacy `.ai` type keeps its paragraphs' letter and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`); its tab definitions (`Xb` … `XB`) set its paragraphs' tab stops. Paragraphs in a row with the same attributes share one copy of them while the text object is read, and those copies hold at most diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c50a45b639..73c6bef754 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -513,7 +513,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 | | View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 | | Guides, grids, smart guides, snapping, rulers | 3 | 85% | global/video rulers, Spacing Guides' distance readouts and spacing for resized or drawn art, Alt swapping a ruler guide's orientation, ruler guides selected together with art (marquee, Shift-click), ruler guides as Layers rows with their own lock and hide (#451: Lock Guides locks them all; Make Guides paths are Layers rows already) (dragging existing guides is done, #414; new guides snapping to side midpoints and artboards, and artboard guides, #451; the drawing tools snapping, with Construction Guides, #506; Spacing Guides for moves, #394; moves snapping to the grid, #740; ruler guides pulling drawn points with Smart Guides on, and "center" and "anchor" labels while hovering with the Selection tool, #812; distance and angle labels while drawing, #2983) | 2–4; status-bar X/Y uses the active ruler origin and General units | -| File formats | 6 | 85% | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); Explicit editing-data oversized single-artboard import fixed (#6865); scores and estimates unchanged. Affinity export, and the rest of Affinity import (adjustments, blur and bevel effects, brushes, live symbol instances, text threading), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), legacy `.ai` files saved without the prolog that defines their operators (their own operator set isn't read yet), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | +| File formats | 6 | 85% | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); Explicit editing-data oversized single-artboard import fixed (#6865); scores and estimates unchanged. Legacy `.ai` files that name but omit their prolog open with their layers, or with their art on one layer when the format has no layers; graph data and settings in these files still come in only as drawn art. Affinity export, and the rest of Affinity import (adjustments, blur and bevel effects, brushes, live symbol instances, text threading), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done, as are the Japanese papers in its Media list: JIS B4/B5/B6, はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判, beside the ISO B4/B5 that settings saved before them keep) | 1–2 | | Automation | 3 | 65% | scripting surface, batch (MCP drops malformed app replies without replay; Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 | @@ -1585,6 +1585,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur To find those characters, SVG export lays a type object out a second time only when the second layout can differ from the first: point type with Line-end Punctuation Half Width and no Japanese punctuation or letters, the setting new type gets, is laid out once. Exporting 5,000 such objects takes a median of 109.4 ms instead of 171.5 ms on Windows 11, with the same SVG (`crates/svg/src/export/text/tests.rs`). SVG export (live text), legacy `.ai` saves (text objects), DXF export (`TEXT` entities) and the CSS Properties panel write area type that Shrink Text to Fit scales down at the size it draws at, and scale its tracking, kerning, leading and baseline shifts the same way where the format holds them. Legacy `.ai` saves (version 7) write type with rotated characters (Character Rotation, Touch Type turns) as its outlines, with a warning that it is no longer editable text (unless `outlineText` already asks for every type object as outlines): a version 7 text object has no character rotation ([Adobe Illustrator File Format Specification 7.0](https://www.nationalarchives.gov.uk/PRONOM/fmt/423), §15), and the outlines keep each character's rotation, baseline shift and scale (`legacy_ai::tests::type_with_rotated_characters_is_outlines_that_keep_the_rotation`). + Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), its discretionary hyphens (`T+`, `T-`) as soft hyphens and its hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` ([file format specification, version 7](https://web.archive.org/web/20050126105313/https://partners.adobe.com/public/developer/en/illustrator/sdk/AI7FileFormat.pdf), §15.4.6.1, §15.4.10.1 and §22.6). A file whose type sets a line breaking value (`TG`) or Japanese layout rules (`Xu`, `Tu`), which VectorCraft doesn't keep, opens with a warning for each (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_line_breaking_and_japanese_layout_rules_are_warned_once`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`). A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304). In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). Where the art of a `.ai` or EPS opened from its editing data differs a little from the file's page, the warning gives the share of the page that differs away from type. When that share is 0.2% or less, the warning names the type and gives the share of the page that differs where the type is; when all the type is made from the text document, the type's share has a warning of its own beside the art's (`crates/eps/src/import/tests_native.rs`). In a `.ai` or EPS opened from its editing data, where a layer's or group's drawn look (an effect can be attached to a group or a layer, [Vector and raster effects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/vector-and-raster-effects.html)) draws a copy of art with type, the import puts the page's type that lies over the copy's other art into the copy's group, instead of giving it to other text objects by painting order. Type beside the copy's art, and every copy's type in a file past the import's text-object limits, still goes by painting order. Without a page, such a copy has no type, and no warning counts it as left out. How the page's type is matched with a copy is VectorCraft's own rule (`crates/eps/src/import/tests_native.rs`). From 453d869c984694c91e1c3af9d292ac0df3229ed4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 10:18:09 -0800 Subject: [PATCH 04/49] Tests: brush art tests replace the default Dots brush The two brush art tests in render's memory tests added a scatter brush named Dots next to the default library's own Dots. Strokes now draw with the first brush of a name, so the stroke drew the default brush and the edits to the symbols and patterns its art shows no longer changed the frame. The tests now remove the default Dots before adding their own. --- crates/render/src/tests/memory.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/render/src/tests/memory.rs b/crates/render/src/tests/memory.rs index 011f9fd7fe..d286fa7e3c 100644 --- a/crates/render/src/tests/memory.rs +++ b/crates/render/src/tests/memory.rs @@ -931,6 +931,8 @@ fn brush_art_draws_edits_of_the_symbols_and_patterns_it_shows() { painted.transform(Affine::translate((0.0, 8.0)), true); let scatter = Node::group(d.alloc_id(), vec![Arc::new(instance), Arc::new(painted)]); let mut lib = vectorcraft_brush::library(&d); + // The default library has its own `Dots`, which a stroke would draw with first. + lib.retain(|b| b.name != "Dots"); lib.push(Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: scatter, spacing: (150.0, 150.0), ..Default::default() }) }); vectorcraft_brush::store(&mut d, &lib); let mut ap = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0); @@ -997,6 +999,8 @@ fn brush_art_draws_renamed_symbols_and_patterns_it_shows() { painted.transform(Affine::translate((0.0, 8.0)), true); let scatter = Node::group(d.alloc_id(), vec![Arc::new(instance), Arc::new(painted)]); let mut lib = vectorcraft_brush::library(&d); + // The default library has its own `Dots`, which a stroke would draw with first. + lib.retain(|b| b.name != "Dots"); lib.push(Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: scatter, spacing: (150.0, 150.0), ..Default::default() }) }); vectorcraft_brush::store(&mut d, &lib); let mut ap = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0); From 1dff3264f00bfdeadd75588cb769a815e841ce01 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 10:42:02 -0800 Subject: [PATCH 05/49] Docs: File formats area rows list the legacy .ai kerning, hyphen and language evidence The current File formats rows of docs/target-app-parity.md now name the legacy .ai manual kerning, discretionary hyphen and hyphenation language reading and writing, with their tests. Score, gaps and hours are unchanged. --- docs/target-app-parity.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 73c6bef754..fae46f4010 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -248,9 +248,9 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); DXF export's Include Placed Images and Include Fill Hatches options, off in CNC / CAM (`images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`; score, gaps and hours unchanged); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Save refusal continuation (#6146): `READ_ONLY_LOCATION`, Save As on ordinary UI Save only; regression execution pending; score/hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); DXF export's Include Placed Images and Include Fill Hatches options, off in CNC / CAM (`images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`; score, gaps and hours unchanged); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Save refusal continuation (#6146): `READ_ONLY_LOCATION`, Save As on ordinary UI Save only; regression execution pending; score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged) | | Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658) | 4–7 | estimated (lowered from 85%: target moved); Control bar snapping quick access includes Snap to Glyph and Alignment Guides (#6164); new ruler-guide Alt orientation implemented; global/artboard ruler modes (`view.globalRulers`) and video rulers (View › Rulers › Show Video Rulers, #2849) done; status-bar X/Y uses the active ruler origin and General units; score unchanged | From 6643d5cf2ad40337fc5e70b8f553071230d4bf89 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:02:01 -0800 Subject: [PATCH 06/49] PDF/.ai import: legacy TG, Xu and Tu with bad operands give no warning A line breaking value (TG) or Japanese layout rules (Xu, Tu) with missing or non-numeric operands no longer mark the setting as set, so the next visible text object gives no warning about a setting the file did not specify. --- crates/eps/src/import/ai/legacy_text.rs | 7 ++++--- crates/eps/src/import/ai/tests.rs | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/ai/legacy_text.rs b/crates/eps/src/import/ai/legacy_text.rs index 61ac106e1a..a3864b43d0 100644 --- a/crates/eps/src/import/ai/legacy_text.rs +++ b/crates/eps/src/import/ai/legacy_text.rs @@ -751,9 +751,10 @@ impl Reader<'_> { _ => {} } } - // `linebreak TG` and `prop cjkLayout cjkRoman cjkCjk Xu` (`Tu` the same, §22.6). - "TG" => st.line_breaking = true, - "Xu" | "Tu" => st.japanese_layout = true, + // `linebreak TG` and `prop cjkLayout cjkRoman cjkCjk Xu` (`Tu` the same, §22.6). Either + // one is set only when its operands are numbers. + "TG" => st.line_breaking |= n0.is_some(), + "Xu" | "Tu" => st.japanese_layout |= nums::<4>(vals).is_some(), _ => {} } } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index b3a01fddc1..a0ade95c5e 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -4387,6 +4387,16 @@ fn legacy_line_breaking_and_japanese_layout_rules_are_warned_once() { assert_eq!(art_texts(&r.doc)[0].plain_text(), "Hi"); } +/// `TG`, `Xu` and `Tu` with missing or non-numeric operands set nothing, so they give no warning. +#[test] +fn legacy_line_breaking_and_japanese_layout_rules_with_bad_operands_are_not_warned() { + for attrs in ["TG", "(a) TG", "1 120 150 Xu", "0 1 (a) 150 Xu", "Tu", "0 1 120 (a) Tu"] { + let r = legacy_read(&point_type(attrs, "a")); + assert!(r.warnings.is_empty(), "{attrs}: {:?}", r.warnings); + assert_eq!(art_texts(&r.doc)[0].plain_text(), "a", "{attrs}"); + } +} + /// Spacing is kept to the ranges the Justification options take, a Minimum, Desired and Maximum /// out of order go back to their defaults, and hyphenation's letter counts and limit to the ranges /// `text.setFormat` takes. From 0c20d09051fd99e6f7ee36603785aa5352dd7baa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:12:23 -0800 Subject: [PATCH 07/49] Legacy .ai save: manual kerning of 0 is written as Tk A pair with manual kerning of 0 was saved with no Tk and opened with automatic kerning. It is now written as 0 0 Tk and reads back as manual kerning of 0. --- crates/eps/src/legacy_ai/tests.rs | 21 +++++++++++++++++++++ crates/eps/src/legacy_ai/writer.rs | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/legacy_ai/tests.rs b/crates/eps/src/legacy_ai/tests.rs index c6e3c05435..7cec7c50f3 100644 --- a/crates/eps/src/legacy_ai/tests.rs +++ b/crates/eps/src/legacy_ai/tests.rs @@ -1527,6 +1527,27 @@ fn manual_kerning_and_soft_hyphens_are_written_and_read_back() { ); } +/// Manual kerning of 0 is written as `0 0 Tk` and reads back as manual kerning, not as the +/// automatic kerning of a pair with no `Tk`. +#[test] +fn manual_kerning_of_zero_is_written_and_read_back() { + use vectorcraft_doc::{CharStyle, TextObject, TextRun}; + let plain = style(12.0, Paint::solid(Color::BLACK)); + let zero = CharStyle { kerning: Some(0.0), ..plain.clone() }; + let mut t = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), "AV", zero); + t.runs.push(TextRun::new("To", plain)); + let mut doc = Document::new(300.0, 200.0); + let layer = doc.layers[0].id; + let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t))); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + let out = write(&doc); + let text = String::from_utf8(out.bytes.clone()).unwrap(); + assert!(text.contains("(A) Tx\n0 0 Tk\n(V) Tx\n0 0 Tk\n"), "{text}"); + let read = texts(&crate::import(&out.bytes).unwrap().document); + let runs: Vec<(String, Option)> = read[0].1.runs.iter().map(|r| (r.text.clone(), r.style.kerning)).collect(); + assert_eq!(runs, [("AV".to_string(), Some(0.0)), ("To".to_string(), None)]); +} + /// Type whose font size measures the cap height is written at the em size it is drawn at, with /// no warning: 14 pt measuring a 0.7 em cap height reads back as 20 pt type. #[test] diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index 5f749e15c8..19f8ce654f 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -853,9 +853,9 @@ impl<'a> Writer<'a> { self.kern(run.style.kerning_before); } line_start = false; - // Manual kerning after each character, a soft hyphen as a discretionary hyphen - // (§15.4.10.1). - let kerning = run.style.kerning.filter(|k| *k != 0.0); + // Manual kerning after each character (0 included, which differs from automatic + // kerning), a soft hyphen as a discretionary hyphen (§15.4.10.1). + let kerning = run.style.kerning; let mut bytes = vec![]; for c in piece.chars() { if c == '\u{ad}' { From 1ed7785bc5d677a8b66aa0241ebd0f277ae5fcea Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:37:49 -0800 Subject: [PATCH 08/49] Actions: keep folder preferences out of saved sets and confined replies A prefs.set step, recorded inside a command.batch or loaded from a .vcactions file, can hold a folder preference such as scratchPrimary. - ui.inspect under automation read roots now checks such a step's value for {key, value} and each entry of values, also in a batch step's commands at any depth, as written and trimmed, including bare names, subfolders and file: URLs. An empty value and the scratch disk choices Startup and None stay. - Any other step text written as an absolute path or a file: URL outside the read root is left out also when whitespace surrounds it. - Save Actions refuses a set with a prefs.set step that holds a folder value and names the action and the preference instead of writing the file. An empty folder and the scratch disk choices save. - prefs.export leaves out scratchPrimary and scratchSecondary, like the other folder preferences. prefs.import refuses a scratch disk set to a folder and still reads Startup, None and an empty value. The folder list, the scratch disk choices and the folder check now live in one place in the engine. Docs: docs/mcp.md, params text and a progress line in docs/target-app-parity.md. --- crates/engine/src/cmd/prefscmds.rs | 34 +++++---- crates/engine/src/tests/prefs_file.rs | 11 ++- crates/ui-egui/src/control.rs | 84 +++++++++++++++------- crates/ui-egui/src/control/tests.rs | 18 +++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/panels/actions.rs | 42 +++++++++++ crates/ui-egui/src/panels/actions/files.rs | 33 ++++++++- crates/ui-egui/src/tests/automation.rs | 56 +++++++++++++++ docs/mcp.md | 23 +++--- docs/target-app-parity.md | 1 + 37 files changed, 283 insertions(+), 48 deletions(-) diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 748e0a125a..8f7b90156f 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -792,7 +792,7 @@ pub fn specs() -> Vec { "Export Preferences", [], None, - "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", + "{path?} write the preferences as a JSON file {format: \"vectorcraft-preferences\", version: 1, prefs: {key: value, …}}: every preference prefs.list lists and the eyedropper, perspectiveWidget, pathfinder, glyphSnap and pixelSnap groups, without the folder preferences fontsFolder, pluginsFolder, scratchPrimary, scratchSecondary, recoveryFolder and templatesFolder and without saved presets; a file over 1 MB, which prefs.import would refuse, is an error and is not written → {path, count}; without path → {data: the file's text, count}", always, export ), @@ -801,7 +801,7 @@ pub fn specs() -> Vec { "Import Preferences", [], None, - "{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}", + "{path? | data?: the file's text} apply the preferences of a file prefs.export wrote (at most 1 MB). Every value is checked first, as prefs.set checks it: another format, a newer version, an unknown preference, a value of the wrong type or out of range, or a folder preference (for scratchPrimary and scratchSecondary, a folder; the choices Startup and None are read) is an error, and nothing changes. Preferences the file leaves out keep their values; the open document's units stay as they are; no undo step; not a command.batch step → {imported: count}", always, import ), @@ -817,8 +817,20 @@ pub const PREFS_VERSION: u64 = 1; /// The largest preferences file `prefs.import` reads. const MAX_PREFS_FILE_BYTES: usize = 1 << 20; -/// Folder preferences, which name folders on one computer: a preferences file leaves them out. -const FOLDER_PREFS: &[&str] = &["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"]; +/// Folder preferences, which name folders on one computer: a preferences file leaves them out, and +/// under automation read roots one outside them is not shown. +pub const FOLDER_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; + +/// The scratch disk choices that are not folders. +pub const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; + +/// Does `value`, set as preference `key`, name a folder: is `key` a folder preference +/// ([`FOLDER_PREFS`]) and `value`, trimmed as the app uses it, neither empty (the default folder) +/// nor a scratch disk choice ([`SCRATCH_CHOICES`])? +pub fn names_a_folder(key: &str, value: &str) -> bool { + let path = value.trim(); + FOLDER_PREFS.contains(&key) && !path.is_empty() && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path)) +} /// The preferences `prefs.export` writes: every [`PREF_SPECS`] key but the [`FOLDER_PREFS`], and /// the [`PREF_GROUPS`]. @@ -867,7 +879,9 @@ fn read_prefs_file(bytes: &[u8]) -> std::result::Result, Stri if prefs.is_empty() { return Err("the file holds no preferences".into()); } - if let Some(key) = prefs.keys().find(|k| FOLDER_PREFS.contains(&k.as_str())) { + // A scratch disk choice or an empty scratch disk names no folder, and is read. + let folder = |k: &str, v: &Value| FOLDER_PREFS.contains(&k) && (!k.starts_with("scratch") || v.as_str().is_some_and(|v| names_a_folder(k, v))); + if let Some(key) = prefs.iter().find(|(k, v)| folder(k, v)).map(|(k, _)| k) { return Err(format!("`{key}` names a folder, which preferences files leave out")); } Ok(prefs.clone()) @@ -892,14 +906,8 @@ fn import(s: &mut Session, p: &Value) -> Result { Ok(json!({ "imported": values.len() })) } -/// The preferences that name a folder: the [`FOLDER_PREFS`] and the scratch disks. -const PATH_PREFS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; - -/// The scratch disk choices that are not folders. -const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; - /// The preferences as `prefs.get`, `prefs.list`, `prefs.set` and `prefs.reset` show them to the -/// request being served. Under automation read roots, a folder preference ([`PATH_PREFS`]) outside +/// request being served. Under automation read roots, a folder preference ([`FOLDER_PREFS`]) outside /// them is `null`, as `ui.inspect` leaves it out, and an export preset's `folder` outside them is /// left out. An empty value (the default folder) and a scratch /// disk choice ([`SCRATCH_CHOICES`]) are shown. Without roots the preferences are shown as they are. @@ -910,7 +918,7 @@ fn shown_prefs(prefs: &Prefs) -> Value { } // The app trims a folder before it uses it. let hidden = |path: &str| !path.trim().is_empty() && crate::file_access::check_read(path.trim()).is_err(); - for key in PATH_PREFS { + for key in FOLDER_PREFS { let Some(path) = all[key].as_str() else { continue }; if hidden(path) && !(key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path.trim())) { all[key] = Value::Null; diff --git a/crates/engine/src/tests/prefs_file.rs b/crates/engine/src/tests/prefs_file.rs index 7693ad0d5c..c2822ba937 100644 --- a/crates/engine/src/tests/prefs_file.rs +++ b/crates/engine/src/tests/prefs_file.rs @@ -12,7 +12,7 @@ fn is_bad_params(r: Result) -> bool { } /// Folder preferences, which a preferences file leaves out. -const FOLDERS: [&str; 4] = ["fontsFolder", "pluginsFolder", "recoveryFolder", "templatesFolder"]; +const FOLDERS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; /// A preferences file holding `prefs`, as `prefs.export` writes one. fn file(prefs: Value) -> String { @@ -46,7 +46,9 @@ fn in_file(s: &Session) -> Value { fn exported_preferences_import_unchanged() { let mut s = changed(); s.prefs.fonts_folder = "/fonts/here".into(); + s.prefs.scratch_primary = "/scratch/here".into(); let r = s.execute("prefs.export", &json!({})).unwrap(); + assert!(!r["data"].as_str().unwrap().contains("/scratch/here"), "a scratch disk folder is not written"); let text = r["data"].as_str().unwrap().to_string(); let written: Value = serde_json::from_str(&text).unwrap(); assert_eq!((written["format"].as_str(), written["version"].as_u64()), (Some(PREFS_FORMAT), Some(1))); @@ -71,6 +73,7 @@ fn exported_preferences_import_unchanged() { assert_eq!(t.doc().unwrap().doc.units, Unit::Millimeters, "the document's units stay as they are"); assert_eq!(t.doc().unwrap().history.undo.len(), undo, "no undo step"); assert_eq!(t.prefs.fonts_folder, "", "folders are not carried over"); + assert_eq!(t.prefs.scratch_primary, "Startup", "nor is a scratch disk"); // Through a file, too. let path = std::env::temp_dir().join(format!("vc-prefs-{}.json", std::process::id())).to_string_lossy().to_string(); @@ -96,6 +99,10 @@ fn a_hand_written_file_sets_what_it_names_and_leaves_the_rest() { assert_eq!(s.prefs.keyboard_increment, 3.0); assert_eq!(s.prefs.guide_color, "#abcdef"); assert_eq!(s.prefs.history_states, 77, "a preference the file leaves out keeps its value"); + // A scratch disk choice names no folder, and is read. + let choices = file(json!({"scratchPrimary": "None", "scratchSecondary": "Startup"})); + assert_eq!(s.execute("prefs.import", &json!({"data": choices})).unwrap(), json!({"imported": 2})); + assert_eq!((s.prefs.scratch_primary.as_str(), s.prefs.scratch_secondary.as_str()), ("None", "Startup")); } #[test] @@ -119,6 +126,8 @@ fn a_bad_preferences_file_is_refused_and_nothing_changes() { (json!({"data": file(json!({"uiAccent": "blue", "historyStates": 5000}))}), "`historyStates` must be a whole number between 5 and 1000"), (json!({"data": file(json!({"uiAccent": "blue", "eyedropper": 5}))}), "`eyedropper` must be an object"), (json!({"data": file(json!({"uiAccent": "blue", "fontsFolder": "/fonts"}))}), "`fontsFolder` names a folder"), + (json!({"data": file(json!({"uiAccent": "blue", "scratchPrimary": "/scratch"}))}), "`scratchPrimary` names a folder"), + (json!({"data": file(json!({"uiAccent": "blue", "scratchSecondary": " scratch "}))}), "`scratchSecondary` names a folder"), (json!({"data": too_large}), "larger than 1 MB"), (json!({"data": 5}), "`data` must be a string"), (json!({"path": "/a.json", "data": file(json!({"uiAccent": "blue"}))}), "give `path` or `data`"), diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 89ab7e37b4..cc5ed91cb6 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -944,12 +944,6 @@ fn names_a_loaded_library(v: &Value) -> bool { } } -/// The preferences that hold a folder, by their keys in `ui.engine_prefs` and the Preferences dialog. -const PREF_FOLDERS: [&str; 6] = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; - -/// The scratch disk choices that are not paths. -const SCRATCH_CHOICES: [&str; 2] = ["Startup", "None"]; - /// May the request being served read `path`? Under an automation read root (a control client's /// request), `ui.inspect` leaves out a path outside the root, as `ui.menu.list` leaves out a recent /// file or a listed script outside it. Another platform's absolute path @@ -972,11 +966,11 @@ fn unreadable_absolute(s: &str) -> bool { } /// Is `s` written as a path that the request may not read: an absolute path or a `file:` URL -/// ([`unreadable_absolute`]), `~` or a path under it, or a path that starts with `.` or `..` and a -/// separator? It is checked as the file access check reads it, relative to the working folder. Other -/// text with a separator (`a/b`) stays. +/// ([`unreadable_absolute`]), as written or with surrounding whitespace trimmed, `~` or a path under +/// it, or a path that starts with `.` or `..` and a separator? It is checked as the file access check +/// reads it, relative to the working folder. Other text with a separator (`a/b`) stays. fn unreadable_path_like(s: &str) -> bool { - if unreadable_absolute(s) { + if unreadable_absolute(s) || unreadable_absolute(s.trim()) { return true; } let t = s.trim_start(); @@ -1045,11 +1039,49 @@ fn percent_decoded(s: &str) -> String { String::from_utf8_lossy(&out).into_owned() } -/// The params of each step of each action of action set `set` (`ui.action_sets`). -fn steps_mut(set: &mut Value) -> impl Iterator> { +/// The command and params of each step of each action of action set `set` (`ui.action_sets`). +fn steps_mut(set: &mut Value) -> impl Iterator)> { let actions = set.get_mut("actions").and_then(Value::as_array_mut).into_iter().flatten(); let steps = actions.filter_map(|action| action.get_mut("steps").and_then(Value::as_array_mut)).flatten(); - steps.filter_map(|step| step.get_mut(1).and_then(Value::as_object_mut)) + steps.filter_map(|step| { + let command = step.get(0).and_then(Value::as_str).unwrap_or_default().to_string(); + step.get_mut(1).and_then(Value::as_object_mut).map(|params| (command, params)) + }) +} + +/// Takes out of the params of a recorded step running `command` the paths the request may not read: +/// the folder preferences' values [`leave_out_unreadable_pref_steps`] takes out, and what +/// [`leave_out_unreadable_params`] takes out, each in one pass over the params. +fn leave_out_unreadable_step(command: &str, params: &mut serde_json::Map) { + leave_out_unreadable_pref_steps(command, params); + leave_out_unreadable_params(params); +} + +/// Takes out of the params of a step running `command` the folder preferences' values the request +/// may not read ([`shows_folder`]): in a `prefs.set` step, given as `{key, value}` or in `values`; in +/// a `command.batch` step, the same in each of its commands, at any depth. +fn leave_out_unreadable_pref_steps(command: &str, params: &mut serde_json::Map) { + match command { + "prefs.set" => { + if let Some(Value::Object(values)) = params.get_mut("values") { + leave_out_unreadable_folders(values); + } + if let (Some(key), Some(value)) = (params.get("key").and_then(Value::as_str), params.get("value").and_then(Value::as_str)) + && !shows_folder(key, value) + { + params.remove("value"); + } + } + "command.batch" => { + for step in params.get_mut("commands").and_then(Value::as_array_mut).into_iter().flatten().filter_map(Value::as_object_mut) { + let command = step.get("command").and_then(Value::as_str).unwrap_or_default().to_string(); + if let Some(Value::Object(params)) = step.get_mut("params") { + leave_out_unreadable_pref_steps(&command, params); + } + } + } + _ => {} + } } /// Takes out of a recorded step's params, at any depth (the commands of a `command.batch` step), the @@ -1103,8 +1135,8 @@ fn leave_out_unreadable_paths(ui: &mut serde_json::Map) { } } } - for step in ui.get_mut("action_sets").and_then(Value::as_array_mut).into_iter().flatten().flat_map(steps_mut) { - leave_out_unreadable_params(step); + for (command, params) in ui.get_mut("action_sets").and_then(Value::as_array_mut).into_iter().flatten().flat_map(steps_mut) { + leave_out_unreadable_step(&command, params); } if let Some(Value::Object(prefs)) = ui.get_mut("engine_prefs") { leave_out_unreadable_folders(prefs); @@ -1117,20 +1149,24 @@ fn leave_out_unreadable_paths(ui: &mut serde_json::Map) { } } -/// Takes out of `prefs` the folder preferences ([`PREF_FOLDERS`]) the request may not read, as written -/// or trimmed as the app uses it. An empty value (no folder) and a scratch disk choice -/// ([`SCRATCH_CHOICES`]) stay. +/// Takes out of `prefs` the folder preferences +/// ([`vectorcraft_engine::cmd::prefscmds::FOLDER_PREFS`]) the request may not read ([`shows_folder`]). fn leave_out_unreadable_folders(prefs: &mut serde_json::Map) { - for key in PREF_FOLDERS { - let Some(written) = prefs.get(key).and_then(Value::as_str) else { continue }; - let path = written.trim(); - if path.is_empty() || (key.starts_with("scratch") && SCRATCH_CHOICES.contains(&path)) || (readable(written) && readable(path)) { - continue; + for key in vectorcraft_engine::cmd::prefscmds::FOLDER_PREFS { + if prefs.get(key).and_then(Value::as_str).is_some_and(|written| !shows_folder(key, written)) { + prefs.remove(key); } - prefs.remove(key); } } +/// May the request see `written` as the value of preference `key`? Yes unless it names a folder +/// ([`vectorcraft_engine::cmd::prefscmds::names_a_folder`]: an empty value and a scratch disk choice +/// do not) that the request may not read as written, or trimmed as the app uses it, also as a `file:` +/// URL ([`readable_value`]). +fn shows_folder(key: &str, written: &str) -> bool { + !vectorcraft_engine::cmd::prefscmds::names_a_folder(key, written) || (readable(written) && readable_value(written.trim())) +} + fn key_from(name: &str) -> Option { egui::Key::from_name(name).or(match name.to_ascii_lowercase().as_str() { "enter" | "return" => Some(egui::Key::Enter), diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index ac8fbfa3eb..4d43339a85 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -293,6 +293,24 @@ fn an_absolute_path_outside_the_read_root_is_unreadable() { }); } +/// Under a read root, an absolute path or a `file:` URL outside it is written as a path also with +/// whitespace around it, and a recorded step's text param loses it; a path inside the root stays. +#[test] +fn a_spaced_absolute_path_outside_the_read_root_is_left_out() { + let base = crate::tests_folder::Folder::new("unreadable-spaced"); + std::fs::create_dir_all(base.join("in")).unwrap(); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap(); + let inside = format!(" {} ", base.join("in").join("a.svg").to_string_lossy()); + let mut params = json!({"target": " /outside/root/x", "name": "\tfile:///outside/root/x ", "keep": inside, "list": [" /outside/root/y"]}); + vectorcraft_engine::file_access::confine(roots.as_ref(), || { + assert!(super::unreadable_path_like(" /outside/root/x")); + assert!(super::unreadable_path_like("\tfile:///outside/root/x ")); + assert!(!super::unreadable_path_like(&inside), "{inside}"); + super::leave_out_unreadable_params(params.as_object_mut().unwrap()); + }); + assert_eq!(params, json!({"keep": inside, "list": []})); +} + /// Without a read root, a `file:` URL that names another host may be read, and a recorded step keeps /// it. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4c2785956f..a5e8b6efd0 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5844,6 +5844,7 @@ theme Appearance Външен вид @msg no such color няма такъв цвят @msg {_1} cannot be used on Windows {_1} не може да се използва в Windows @msg more than one action is named “{_1}” повече от едно действие се казва „{_1}“ +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set действието „{_1}“ задава предпочитанието за папка {_2}; изтрийте тази стъпка, за да запазите набора @msg no symbol named `{n}` няма символ с име `{n}` Smudge Stick… Размазваща пръчка… Smudge Stick Размазваща пръчка diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 9ccc94d3a4..4cd869081f 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5847,6 +5847,7 @@ mark-style Japanese Japanski @msg no such color nema takve boje @msg {_1} cannot be used on Windows {_1} se ne može koristiti na Windowsu @msg more than one action is named “{_1}” više od jedne akcije ima naziv „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set akcija „{_1}” postavlja postavku foldera {_2}; izbrišite taj korak da biste sačuvali skup @msg no symbol named `{n}` nema simbola pod nazivom `{n}` Smudge Stick… Štapić za razmazivanje… Smudge Stick Štapić za razmazivanje diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b0159ebc14..522abdab79 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5722,6 +5722,7 @@ digits Malayalam Malaiàlam @msg no such color no existeix aquest color @msg {_1} cannot be used on Windows {_1} no es pot fer servir al Windows @msg more than one action is named “{_1}” més d’una acció es diu “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set l’acció “{_1}” defineix la preferència de carpeta {_2}; suprimiu aquest pas per desar el conjunt @msg name the file poseu un nom al fitxer @msg New Window isn't available yet: a document opens in one window Finestra nova encara no està disponible: un document s’obre en una sola finestra @msg no active document no hi ha cap document actiu diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 27b2d32771..04b292ed8b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6153,6 +6153,7 @@ mark-style Japanese Japonské @msg no such color taková barva neexistuje @msg {_1} cannot be used on Windows Zkratku {_1} nelze ve Windows použít @msg more than one action is named “{_1}” název „{_1}“ má více než jedna akce +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set akce „{_1}“ nastavuje předvolbu složky {_2}; chcete-li sadu uložit, tento krok smažte @msg no symbol named `{n}` žádný symbol s názvem `{n}` Smudge Stick… Rozmazávací tyčinka… Smudge Stick Rozmazávací tyčinka diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 3810bb2ac8..0f3865bb2c 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5726,6 +5726,7 @@ swatch Bright Violet Kraftig violet @msg no such color ingen sådan farve @msg {_1} cannot be used on Windows {_1} kan ikke bruges i Windows @msg more than one action is named “{_1}” mere end én handling hedder »{_1}« +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set handlingen »{_1}« angiver mappeindstillingen {_2}; slet det trin for at gemme sættet @msg no symbol named `{n}` intet symbol med navnet `{n}` Remove Color Profile… Fjern farveprofil… Color wheel Farvehjul diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 7b6cd3e8a6..93cdda7c63 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5739,6 +5739,7 @@ effect Soft Weich @msg no such color keine solche Farbe @msg {_1} cannot be used on Windows {_1} kann unter Windows nicht verwendet werden @msg more than one action is named “{_1}” mehr als eine Aktion heißt „{_1}“ +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set die Aktion „{_1}“ legt die Ordnervoreinstellung {_2} fest; löschen Sie diesen Schritt, um den Satz zu speichern @msg no symbol named `{n}` kein Symbol namens `{n}` Smudge Stick… Wischstift… Smudge Stick Wischstift diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8620169f21..b407274593 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6105,6 +6105,7 @@ effect Points Σημεία @msg no such color δεν υπάρχει τέτοιο χρώμα @msg {_1} cannot be used on Windows Το {_1} δεν μπορεί να χρησιμοποιηθεί στα Windows @msg more than one action is named “{_1}” περισσότερες από μία ενέργειες ονομάζονται «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set η ενέργεια «{_1}» ορίζει την προτίμηση φακέλου {_2}· διαγράψτε αυτό το βήμα για να αποθηκεύσετε το σύνολο @msg no symbol named `{n}` κανένα σύμβολο με όνομα `{n}` Smudge Stick… Μουτζούρα… Smudge Stick Μουτζούρα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a82db20a99..e181c9a86e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6096,6 +6096,7 @@ mark-style Japanese Japonesas @msg no such color no existe ese color @msg {_1} cannot be used on Windows {_1} no se puede usar en Windows @msg more than one action is named “{_1}” más de una acción se llama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set la acción “{_1}” establece la preferencia de carpeta {_2}; elimine ese paso para guardar el conjunto @msg no symbol named `{n}` no hay ningún símbolo llamado `{n}` Smudge Stick… Difuminar con barra… Smudge Stick Difuminar con barra diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index db90a36316..741f782599 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6116,6 +6116,7 @@ mark-style Japanese Japanilainen @msg no such color väriä ei ole @msg {_1} cannot be used on Windows {_1} ei ole käytettävissä Windowsissa @msg more than one action is named “{_1}” useamman kuin yhden toiminnon nimi on ”{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set toiminto ”{_1}” asettaa kansioasetuksen {_2}; poista tämä vaihe, jotta voit tallentaa sarjan @msg no symbol named `{n}` symbolia nimeltä `{n}` ei ole Smudge Stick… Sumennuskynä… Smudge Stick Sumennuskynä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 938e43dd48..23bf37a539 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6148,6 +6148,7 @@ theme Appearance Apparence @msg no such color couleur inexistante @msg {_1} cannot be used on Windows {_1} ne peut pas être utilisé sous Windows @msg more than one action is named “{_1}” plusieurs scripts s’appellent « {_1} » +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set le script « {_1} » définit la préférence de dossier {_2} ; supprimez cette étape pour enregistrer le jeu @msg no symbol named `{n}` aucun symbole nommé `{n}` Smudge Stick… Estompe… Smudge Stick Estompe diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 0eacf9629b..5522cc31bd 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -7170,6 +7170,7 @@ antiAlias Sharp शार्प @msg more than 10000 linked files: package dependencies in smaller parts 10000 से अधिक लिंक्ड फ़ाइलें: पैकेज निर्भरता छोटे भागों में @msg more than 256 commands 256 से अधिक कमांड @msg more than one action is named “{_1}” एक से अधिक कार्रवाई का नाम "{_1}" रखा गया है। +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set कार्रवाई "{_1}" फ़ोल्डर प्राथमिकता {_2} सेट करती है; सेट सेव करने के लिए वह चरण हटाएँ @msg Move Panel to New Window is not available: floating panels stay in the main window नई विंडो में पैनल को स्थानांतरित करना उपलब्ध नहीं है: अस्थायी पैनल मुख्य विंडो में रहते हैं @msg move requires a destination panel move: एक गंतव्य पैनल की आवश्यकता होती है @msg move requires an existing destination panel move: चाल एक मौजूदा गंतव्य पैनल की आवश्यकता है diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3dadc4d3b6..8361c405f3 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6136,6 +6136,7 @@ imageTrace Outline Körvonal @msg no such color nincs ilyen szín @msg {_1} cannot be used on Windows A(z) {_1} nem használható Windows rendszeren @msg more than one action is named “{_1}” több művelet neve is „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set a(z) „{_1}” művelet beállítja a(z) {_2} mappabeállítást; a készlet mentéséhez törölje ezt a lépést @msg no symbol named `{n}` nincs `{n}` nevű szimbólum Smudge Stick… Elmosó rúd… Smudge Stick Elmosó rúd diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e670840b2c..bd7a906986 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5726,6 +5726,7 @@ swatch Bright Violet Violet cerah @msg no such color tidak ada warna seperti itu @msg {_1} cannot be used on Windows {_1} tidak dapat digunakan di Windows @msg more than one action is named “{_1}” lebih dari satu tindakan bernama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set tindakan “{_1}” mengatur preferensi folder {_2}; hapus langkah itu untuk menyimpan set @msg no symbol named `{n}` tidak ada simbol bernama `{n}` Remove Color Profile… Hapus profil warna… Color wheel Roda warna diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 91d1e24bf7..765a2aa6aa 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5774,6 +5774,7 @@ lighting Light Luce @msg no such color colore inesistente @msg {_1} cannot be used on Windows {_1} non può essere usato su Windows @msg more than one action is named “{_1}” più di un’azione si chiama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set l’azione “{_1}” imposta la preferenza della cartella {_2}; elimina quel passaggio per salvare il set @msg no symbol named `{n}` nessun simbolo denominato `{n}` Smudge Stick… Sfumino… Smudge Stick Sfumino diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 628d1ef3de..7d725b5352 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5818,6 +5818,7 @@ lighting Light ライト @msg no such color 指定されたカラーが見つかりません @msg {_1} cannot be used on Windows {_1} は Windows では使用できません @msg more than one action is named “{_1}” 「{_1}」という名前のアクションが複数あります +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set アクション「{_1}」がフォルダーの環境設定 {_2} を設定しています。セットを保存するにはそのステップを削除してください @msg no symbol named `{n}` `{n}` という名前のシンボルはありません Smudge Stick… スマッジスティック… Smudge Stick スマッジスティック diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index aa2c27c296..ea3cf5a668 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5880,6 +5880,7 @@ imageTrace Outline კონტური @msg no such color ასეთი ფერი არ არის @msg {_1} cannot be used on Windows {_1} ვერ გამოიყენება Windows-ში @msg more than one action is named “{_1}” „{_1}“ ერთზე მეტი მოქმედების სახელია +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set მოქმედება „{_1}“ აყენებს საქაღალდის პარამეტრს {_2}; ნაკრების შესანახად წაშალეთ ეს ნაბიჯი @msg no symbol named `{n}` სიმბოლო სახელით `{n}` არ არის @msg no saved selection recalled შენახული მონიშვნა გამოძახებული არ არის @msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set VectorCraft-ის ბოლო გაშვება ფანჯრის გამოჩენამდე შეწყდა (გრაფიკული პროცესორი: {name}), ამიტომ VectorCraft გაეშვა: {summary}. ამ გრაფიკული პროცესორის ხელახლა საცდელად შეცვალეთ პარამეტრები › წარმადობა › გრაფიკული პროცესორი ან გაუშვით WGPU_BACKEND ცვლადის მითითებით diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ca5c2ac8d1..98be548ced 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5735,6 +5735,7 @@ lighting Light 광원 @msg no such color 해당 색상이 없습니다 @msg {_1} cannot be used on Windows {_1}은(는) Windows에서 사용할 수 없습니다 @msg more than one action is named “{_1}” “{_1}”(이)라는 이름의 액션이 둘 이상 있습니다 +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 액션 “{_1}”이(가) 폴더 환경 설정 {_2}을(를) 설정합니다. 세트를 저장하려면 해당 단계를 삭제하세요 @msg no symbol named `{n}` 이름이 `{n}`인 심볼이 없습니다 Smudge Stick… 문지르기 막대… Smudge Stick 문지르기 막대 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d46d7e8e87..441dca9c77 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6300,6 +6300,7 @@ swatch Bright Violet Felviolet @msg no such color kleur bestaat niet @msg {_1} cannot be used on Windows {_1} kan niet worden gebruikt in Windows @msg more than one action is named “{_1}” meer dan één actie heet „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set de actie „{_1}” stelt de mapvoorkeur {_2} in; verwijder die stap om de set op te slaan @msg no symbol named `{n}` geen symbool met de naam `{n}` Remove Color Profile… Kleurprofiel verwijderen… Color wheel Kleurenwiel diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 38d5e2f497..9a689ba5e2 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6008,6 +6008,7 @@ mark-style Japanese Japońskie @msg no such color brak takiego koloru @msg {_1} cannot be used on Windows Skrótu {_1} nie można używać w systemie Windows @msg more than one action is named “{_1}” więcej niż jedna operacja nazywa się „{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set operacja „{_1}” ustawia preferencję folderu {_2}; usuń ten krok, aby zapisać zestaw @msg no symbol named `{n}` brak symbolu o nazwie `{n}` Smudge Stick… Rozmazywacz… Smudge Stick Rozmazywacz diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 2a45bc8e68..ce2a694be7 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6068,6 +6068,7 @@ mark-style Japanese Japonesas @msg no such color cor inexistente @msg {_1} cannot be used on Windows {_1} não pode ser usado no Windows @msg more than one action is named “{_1}” mais de uma ação se chama “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set a ação “{_1}” define a preferência de pasta {_2}; exclua essa etapa para salvar o conjunto @msg no symbol named `{n}` nenhum símbolo chamado `{n}` Smudge Stick… Esfuminho… Smudge Stick Esfuminho diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index af93b86974..f2646a0bb2 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5759,6 +5759,7 @@ lighting Light Свет @msg no such color такого цвета нет @msg {_1} cannot be used on Windows Сочетание {_1} нельзя использовать в Windows @msg more than one action is named “{_1}” несколько действий называются «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set действие «{_1}» задаёт настройку папки {_2}; удалите этот шаг, чтобы сохранить набор @msg no symbol named `{n}` нет символа с именем `{n}` Smudge Stick… Растушёвка… Smudge Stick Растушёвка diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index f26438a201..08c988a3d4 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6107,6 +6107,7 @@ theme Appearance Utseende @msg no such color färgen finns inte @msg {_1} cannot be used on Windows {_1} kan inte användas i Windows @msg more than one action is named “{_1}” fler än en åtgärd heter ”{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set åtgärden ”{_1}” anger mappinställningen {_2}; ta bort det steget för att spara uppsättningen @msg no symbol named `{n}` ingen symbol med namnet `{n}` Angular Vinkelmått Away from the Line Bort från linjen diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a91aab6f0e..58d8cf1546 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5804,6 +5804,7 @@ theme Appearance Görünüm @msg no such color öyle bir renk yok @msg {_1} cannot be used on Windows {_1} Windows'ta kullanılamaz @msg more than one action is named “{_1}” birden fazla eylemin adı “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set “{_1}” eylemi {_2} klasör tercihini ayarlıyor; seti kaydetmek için o adımı silin @msg no symbol named `{n}` `{n}` adında bir sembol yok Artboard Options: edit with the Artboard tool Çalışma Yüzeyi Seçenekleri: Çalışma Yüzeyi aracıyla düzenleme @msg this is a PostScript file (an .ai saved in an older format or without PDF compatibility, or an EPS file), which can't be opened yet: save it as PDF, PDF-compatible .ai or SVG bu henüz açılamayan bir PostScript dosyasıdır (eski bir formatta kaydedilmiş veya PDF uyumluluğu olmayan bir .ai veya bir EPS dosyası): PDF, PDF uyumlu .ai veya SVG olarak kaydedin diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 17ddeb2086..37c56e1881 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5754,6 +5754,7 @@ lighting Light Світло @msg no such color такого кольору немає @msg {_1} cannot be used on Windows Сполучення {_1} не можна використовувати у Windows @msg more than one action is named “{_1}” кілька дій мають назву «{_1}» +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set дія «{_1}» задає параметр папки {_2}; видаліть цей крок, щоб зберегти набір @msg no symbol named `{n}` немає символу з назвою `{n}` Smudge Stick… Розтушовування… Smudge Stick Розтушовування diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7d3d89a82d..4c3fff5f31 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5755,6 +5755,7 @@ theme Appearance Giao diện @msg no such color không có màu như vậy @msg {_1} cannot be used on Windows không thể dùng {_1} trên Windows @msg more than one action is named “{_1}” có nhiều hành động tên là “{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set hành động “{_1}” đặt tùy chọn thư mục {_2}; hãy xóa bước đó để lưu bộ @msg no symbol named `{n}` không có biểu tượng nào tên `{n}` Smudge Stick… Bút chì xóa mờ… Smudge Stick Bút chì xóa mờ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 34ffeb5d6b..cf912fb369 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5987,6 +5987,7 @@ lighting Light 光源 @msg no such color 没有此颜色 @msg {_1} cannot be used on Windows {_1} 在 Windows 上不能使用 @msg more than one action is named “{_1}” 有多个动作名为“{_1}” +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 动作“{_1}”设置了文件夹首选项 {_2};请删除该步骤后再存储动作组 @msg no symbol named `{n}` 没有名为 `{n}` 的符号 Smudge Stick… 涂抹棒… Smudge Stick 涂抹棒 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 309e00346c..2fc05b8c7c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5818,6 +5818,7 @@ lighting Light 光源 @msg no such color 沒有這個顏色 @msg {_1} cannot be used on Windows {_1} 無法在 Windows 上使用 @msg more than one action is named “{_1}” 有多個動作名為「{_1}」 +@msg the action “{_1}” sets the folder preference {_2}; delete that step to save the set 動作「{_1}」設定了資料夾偏好設定 {_2};請刪除該步驟後再儲存動作組 @msg no symbol named `{n}` 沒有名為 `{n}` 的符號 Smudge Stick… 塗抹棒… Smudge Stick 塗抹棒 diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 37cdcb2d56..b29918c878 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1538,7 +1538,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ crate::panels::actions::SAVE_SET, "Save Actions…", "", - "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written; the panel menu's Save Actions… → {path}", + "{set?: index, id?, path?} write the Actions panel's set at set (0 is the top set; default: the current set) with its actions to an action set file (.vcactions) at path, else a picked file named after the set (the web downloads it). id holds the panel's own set id, which its menu passes: the set with this id is saved wherever it is now, and set is not read. A set over 8 MiB as a file, with more than 1,000 actions or with an action of more than 10,000 steps is not written, nor is a set with a prefs.set step (also in a batch step) that sets a folder preference to a folder (an error names the action and the preference; an empty folder and the scratch disk choices Startup and None save); the panel menu's Save Actions… → {path}", ), ( crate::panels::actions::LOAD_SET, diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 1971834a07..630d81e95f 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -3512,6 +3512,48 @@ mod tests { } } + /// `ui.actionSet.save` refuses a set with a step that sets a folder preference to a folder, as + /// `{key, value}`, in `values` or in a batch step's commands at any depth, with an error that names + /// the action and the preference, and writes no file. An empty folder, a scratch disk choice and + /// other preferences save. + #[test] + fn save_actions_refuses_a_step_that_sets_a_folder_preference() { + let mut app = app_with_sets(); + let files = with_files(&mut app, "/sets/x.vcactions"); + let step = |command: &str, params: Value| (command.to_string(), params); + let nested = json!({"commands": [{"command": "command.batch", "params": {"commands": [ + {"command": "prefs.set", "params": {"values": {"fontsFolder": "fonts"}}}, + ]}}]}); + for (steps, key) in [ + (vec![step("prefs.set", json!({"key": "scratchPrimary", "value": "scratch"}))], "scratchPrimary"), + (vec![step("prefs.set", json!({"values": {"uiAccent": "green", "templatesFolder": "/Users/me/Templates"}}))], "templatesFolder"), + ( + vec![ + step("object.move", json!({"dx": 1, "dy": 0})), + step( + "command.batch", + json!({"commands": [{"command": "prefs.set", "params": {"key": "scratchSecondary", "value": " /Volumes/Fast "}}]}), + ), + ], + "scratchSecondary", + ), + (vec![step("command.batch", nested)], "fontsFolder"), + ] { + app.ui.action_sets[1] = ActionSet::new("Other", vec![Action::new("Scratch", steps)]); + let e = app.run(SAVE_SET, json!({"set": 1, "path": "/sets/x.vcactions"})).unwrap_err(); + assert_eq!(e, format!("the action “Scratch” sets the folder preference {key}; delete that step to save the set")); + assert!(files.borrow().is_empty(), "no file is written for {key}"); + } + let kept = vec![ + step("prefs.set", json!({"values": {"scratchPrimary": "None", "scratchSecondary": " Startup ", "fontsFolder": "", "uiAccent": "green"}})), + step("prefs.set", json!({"key": "pluginsFolder", "value": ""})), + step("x.y", json!({"key": "fontsFolder", "value": "fonts"})), + ]; + app.ui.action_sets[1] = ActionSet::new("Other", vec![Action::new("Scratch", kept)]); + assert_eq!(app.run(SAVE_SET, json!({"set": 1, "path": "/sets/x.vcactions"})).unwrap(), json!({"path": "/sets/x.vcactions"})); + assert!(files.borrow().contains_key("/sets/x.vcactions")); + } + /// A file that is not an action set file, or is too large, is refused with a message, and no /// set is added. #[test] diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index a8ae79e713..b5805f9b2a 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -169,6 +169,30 @@ fn ui_steps_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { .collect() } +/// A folder preference whose value in a step running `command` with `params` names a folder +/// ([`vectorcraft_engine::cmd::prefscmds::names_a_folder`]): in a `prefs.set` step, given as +/// `{key, value}` or in `values`, and in a `command.batch` step, in any of its commands at any depth. +/// Every such value is found, also one under `values` that `{key, value}` replaces when the step runs, +/// because [`write_set`] writes the params as they are. +fn folder_pref_in(command: &str, params: &Value) -> Option { + use vectorcraft_engine::cmd::prefscmds::names_a_folder; + match command { + "prefs.set" => { + let names = |key: &str, value: &Value| value.as_str().is_some_and(|v| names_a_folder(key, v)); + let pair = params.get("key").and_then(Value::as_str).filter(|key| params.get("value").is_some_and(|v| names(key, v))); + let values = params.get("values").and_then(Value::as_object).into_iter().flatten(); + pair.map(str::to_string).or_else(|| values.filter(|(key, v)| names(key, v)).map(|(key, _)| key.clone()).next()) + } + "command.batch" => { + let mut steps = params.get("commands").and_then(Value::as_array).into_iter().flatten(); + steps.find_map(|step| { + folder_pref_in(step.get("command").and_then(Value::as_str).unwrap_or_default(), step.get("params").unwrap_or(&Value::Null)) + }) + } + _ => None, + } +} + /// `set` as the text of an action set file. pub fn write_set(set: &ActionSet) -> String { let actions = set @@ -281,7 +305,9 @@ pub(super) fn set_file_stem(name: &str) -> String { /// the set (the web downloads it) → `{path}`. `id` is the panel's own set id ([`ActionSet::id`]), /// which the panel menu passes: the set with this id is saved wherever it is when the file dialog /// answers, and `set` is not read. A set that [`read_set`] would refuse (larger than 8 MiB as a -/// file, more than 1,000 actions, an action with more than 10,000 steps) is not written. +/// file, more than 1,000 actions, an action with more than 10,000 steps) is not written, nor is a set +/// with a step that sets a folder preference to a folder ([`folder_pref_in`]), which names a folder +/// on this computer: the error names the action and the preference. pub fn save_set(app: &mut VectorcraftApp, p: &Value) -> Result { let len = app.ui.action_sets.len(); let set = match (p.get("id"), p.get("set")) { @@ -292,6 +318,11 @@ pub fn save_set(app: &mut VectorcraftApp, p: &Value) -> Result { (_, Some(v)) => v.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < len).ok_or("no such action set")?, }; let Some(s) = app.ui.action_sets.get(set) else { return Err("no such action set".into()) }; + let folder = + s.actions.iter().find_map(|a| a.steps.iter().find_map(|(command, params)| folder_pref_in(command, params)).map(|key| (&a.name, key))); + if let Some((action, key)) = folder { + return Err(format!("the action “{}” sets the folder preference {}; delete that step to save the set", action, key)); + } let text = write_set(s); // What Save Actions… writes, Load Actions… reads back. read_set(&text, "")?; diff --git a/crates/ui-egui/src/tests/automation.rs b/crates/ui-egui/src/tests/automation.rs index 6d7c7075c5..2364a00a56 100644 --- a/crates/ui-egui/src/tests/automation.rs +++ b/crates/ui-egui/src/tests/automation.rs @@ -891,6 +891,62 @@ fn ui_inspect_leaves_out_relative_paths_in_recorded_steps() { let _ = std::fs::remove_dir_all(base); } +/// `ui.inspect` leaves out of a recorded `prefs.set` step the value of a folder preference outside the +/// read root, given as `{key, value}` or in `values`, also in a batch step's commands at any depth: a +/// bare name or a subfolder (in the working folder, outside the root), a path with spaces around it and +/// a `file:` URL. A folder inside the root, an empty value, a scratch disk choice, other preferences and +/// another command's params stay, and the UI state is not changed. +#[test] +fn ui_inspect_leaves_out_folder_preferences_in_recorded_prefs_steps() { + let (mut app, tx, base) = confined("inspect-prefs-steps"); + let (inside, outside) = (s(base.join("in")), s(base.join("outside"))); + let url = if outside.starts_with('/') { format!("file://{outside}") } else { format!("file:///{}", outside.replace('\\', "/")) }; + let folders = ["fontsFolder", "pluginsFolder", "scratchPrimary", "scratchSecondary", "recoveryFolder", "templatesFolder"]; + let mut steps: Vec = folders.iter().map(|k| json!(["prefs.set", {"key": k, "value": "scratch"}])).collect(); + steps.push(json!(["prefs.set", {"values": folders.iter().map(|k| (k.to_string(), json!("tmp/scratch"))).collect::>()}])); + steps.push(json!(["prefs.set", {"values": { + "scratchPrimary": format!(" {outside} "), "fontsFolder": url, "pluginsFolder": format!(" {url}"), "recoveryFolder": "Startup", + "templatesFolder": inside, "scratchSecondary": " None ", "uiAccent": "green", + }}])); + steps.push(json!(["prefs.set", {"key": "scratchPrimary", "value": "Startup"}])); + steps.push(json!(["prefs.set", {"key": "recoveryFolder", "value": ""}])); + steps.push(json!(["prefs.set", {"key": "fontsFolder", "value": inside}])); + let batch = |commands: Value| json!({"commands": commands}); + let inner = batch(json!([{"command": "prefs.set", "params": {"values": {"scratchSecondary": "tmp/scratch", "uiAccent": "red"}}}])); + steps.push(json!([ + "command.batch", + batch(json!([ + {"command": "prefs.set", "params": {"key": "scratchPrimary", "value": "scratch"}}, + {"command": "command.batch", "params": inner}, + ])) + ])); + steps.push(json!(["x.y", {"key": "scratchPrimary", "value": "scratch"}])); + let set: crate::panels::actions::ActionSet = + serde_json::from_value(json!({"name": "set", "actions": [{"name": "prefs", "steps": steps}]})).unwrap(); + app.ui.action_sets = vec![set]; + let r = request(&mut app, &tx, "ui.inspect", json!({})); + let shown = &r["result"]["ui"]["action_sets"][0]["actions"][0]["steps"]; + let mut want: Vec = folders.iter().map(|k| json!(["prefs.set", {"key": k}])).collect(); + want.push(json!(["prefs.set", {"values": {}}])); + want.push(json!(["prefs.set", {"values": {"templatesFolder": inside, "scratchSecondary": " None ", "uiAccent": "green"}}])); + want.push(json!(["prefs.set", {"key": "scratchPrimary", "value": "Startup"}])); + want.push(json!(["prefs.set", {"key": "recoveryFolder", "value": ""}])); + want.push(json!(["prefs.set", {"key": "fontsFolder", "value": inside}])); + let inner = batch(json!([{"command": "prefs.set", "params": {"values": {"uiAccent": "red"}}}])); + want.push(json!([ + "command.batch", + batch(json!([ + {"command": "prefs.set", "params": {"key": "scratchPrimary"}}, + {"command": "command.batch", "params": inner}, + ])) + ])); + want.push(json!(["x.y", {"key": "scratchPrimary", "value": "scratch"}])); + assert_eq!(shown, &json!(want), "{r}"); + assert!(!r.to_string().contains("tmp/scratch") && !r.to_string().contains(&outside.replace('\\', "\\\\")), "{r}"); + assert_eq!(app.ui.action_sets[0].actions[0].steps[0].1, json!({"key": "fontsFolder", "value": "scratch"}), "the UI state is not changed"); + let _ = std::fs::remove_dir_all(base); +} + /// Two libraries loaded from outside the read root under the same file name, only the second of them /// in a library panel: `ui.inspect` and `swatch.library.list` show the second under the same id, and /// that id finds it. diff --git a/docs/mcp.md b/docs/mcp.md index 2b20cfc588..752e501e50 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -148,12 +148,14 @@ the suite) keep the agent to the folders you give it: in a recorded action's steps in `ui.action_sets`, also in a batch step's commands, the `path`, `paths`, `file`, `files` and `folder` params, a param whose name ends in `Path`, `Paths`, `File`, `Files` or `Folder` (`diffPath`, `destinationFolder`), the entries of a list under such a param, and under any other param, as a key or in any - list, an absolute path or a `file:` URL outside the roots (any case of `file:`, `%XX` escapes decoded; a URL that + list, an absolute path or a `file:` URL outside the roots, also with whitespace around it (any case of `file:`, `%XX` escapes decoded; a URL that names another host is always left out), and `~`, a path under `~` or a path that starts with `./` or `../` (either separator) that resolves outside the roots from the app's working folder; and the folder - preferences in `ui.engine_prefs` and in an open Preferences dialog's `ui.dialog.fields` - (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, `recoveryFolder`, `templatesFolder`; an - empty folder and the scratch disk choices `Startup` and `None` stay). A library loaded from a file outside the + preferences in `ui.engine_prefs`, in an open Preferences dialog's `ui.dialog.fields` and in a recorded + `prefs.set` step (its `value` for such a `key`, and the entries of `values`), also in a batch step's commands + (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, `recoveryFolder`, `templatesFolder`, checked + as written and trimmed, a bare name or a subfolder from the app's working folder; an empty folder and the scratch + disk choices `Startup` and `None` stay). A library loaded from a file outside the roots shows as `loaded/` wherever `ui_inspect` and `ui.docking` name it: its id, its panel's id, the Color Guide's `ui.color_guide_limit`, a closed panel's key in `ui.docking_hidden` and the `library` and `panel` params of a recorded action's steps in `ui.action_sets`, and the keys of shortcut overrides (`panel:` @@ -3152,7 +3154,11 @@ already walks Image Trace. Short agent entry points: after the set (the web downloads it), and returns `{path}`. `id` holds the panel's own set id, which the panel menu passes: the set with this id is saved wherever it is when the file dialog answers, and `set` is not read. A set that would be larger than 8 MiB as a file, has more than 1,000 actions or has an action of more than 10,000 steps is not - written, and the command returns the same error as a load of such a file. `ui.actionSet.load {path? | data?}` reads + written, and the command returns the same error as a load of such a file. Nor is a set with a `prefs.set` step, also + in a batch step's commands, that sets a folder preference (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, + `scratchSecondary`, `recoveryFolder`, `templatesFolder`) to a folder, which names a folder on this computer: the + error `the action “” sets the folder preference ; delete that step to save the set` names the first such + action and preference. An empty folder and the scratch disk choices `Startup` and `None` save. `ui.actionSet.load {path? | data?}` reads the file at `path`, else a picked one, or the file's text `data`, adds its set after the other sets and returns `{set, name, actions}`. A set named like one already in the panel gets the next free number (`Print 2`), and the other sets and the current set stay as they are. An action set file (`.vcactions`) is JSON: @@ -6118,12 +6124,13 @@ Preferences. `prefs.reset` resets the preferences at once, without the question. `prefs.export {path?}` writes the preferences as a JSON file, `{"format": "vectorcraft-preferences", "version": 1, "prefs": {…}}`, and replies `{path, count}`; without `path` it replies `{data, count}` with the file's text. The file holds every preference `prefs.list` lists and the `eyedropper`, `perspectiveWidget`, `pathfinder`, `glyphSnap` and -`pixelSnap` groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `recoveryFolder`, `templatesFolder`) and -saved presets. An export over 1 MB, the most `prefs.import` reads, is an error and writes nothing. +`pixelSnap` groups. It leaves out the folder preferences (`fontsFolder`, `pluginsFolder`, `scratchPrimary`, `scratchSecondary`, +`recoveryFolder`, `templatesFolder`) and saved presets. An export over 1 MB, the most `prefs.import` reads, is an error and writes nothing. `prefs.import {path | data}` applies such a file and replies `{imported}`, the number of preferences it set. It checks every value first, as `prefs.set` does: a file over 1 MB, malformed JSON, another `format`, a newer `version`, an empty `prefs`, an unknown preference, a value of the wrong type or out of range, or a folder preference -is an error, and nothing changes. Preferences the file leaves out keep their values. An import leaves the open +is an error, and nothing changes. For `scratchPrimary` and `scratchSecondary` only a folder is an error: an empty +value and the choices `Startup` and `None` are read. Preferences the file leaves out keep their values. An import leaves the open document's units as they are, records no undo step, and is saved with the other preferences. An open Preferences dialog shows the imported values. `prefs.import` is refused as a `command.batch` step. The dialog's Export Preferences… and Import Preferences… buttons run the two commands with a save or open dialog; the web build downloads the exported file and has no Import Preferences… button. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f7782bdfcd..2c0b7cab1e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -747,6 +747,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`. - Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`). The folder and file names drop trailing periods and spaces (`a..svg` gives `a`). When two source files would get the same name that way (compared without case), or a later file's folder or file would take the name of an output an earlier file wrote, of a source file in the destination folder or, for data sets, of a file already there, Batch adds `-2`, `-3`… to the later one's name. - Actions panel menu: Save Actions… writes the current action set with all its actions to a file, and Load Actions… adds the set from such a file to the panel ([Manage a set of actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/manage-a-set-of-actions.html); `ui.actionSet.save`, `ui.actionSet.load`). The file is VectorCraft's own action set file (`.vcactions`); a loaded set named like one in the panel gets the next free number. + - Actions panel menu: Save Actions… does not write a set with a step that sets a folder preference (the Fonts, Plug-ins, Templates and Recovery folders and the two scratch disks) to a folder, and names the action and the preference instead; an empty folder and the scratch disk choices Startup and None save. Export Preferences leaves out both scratch disks, and `ui.inspect` under automation read roots leaves such a folder outside them out of recorded steps. These are VectorCraft's own rules (`ui.actionSet.save`, `prefs.export`). - Actions panel menu: Load Actions… also reads action sets saved as `.aia` files ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `actionSet.read`, `ui.actionSet.load`). VectorCraft's own rules: the set comes in with all its actions. A step with a label and no recorded values comes in as the one menu command with the step's label, when that command is directly in a top menu, is what its menu bar item runs with no values and opens no dialog. A step that is off in the file stays excluded from playback. The other steps, recorded menu items included, are left out, and the status bar says how many. - Actions panel: the panel menu's Action Options… renames the selected action and keeps its steps ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html): the menu item opens a dialog that renames the action or gives it a function key and color; `ui.action.options`). It also sets the action's function key and color. - Actions panel: Create New Action (bottom bar) and the panel menu's New Action… ask for the new action's name, set, function key and color, and Record adds the action and starts recording into it ([Create new actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/record-actions.html): the New Action dialog takes the action's name and the set it goes in; `ui.action.new`, `ui.action.play`). The function key (F1 to F15, with Ctrl or Command and Shift) plays the action. An action's function key plays the action even when a command has the same shortcut, and the command keeps it. VectorCraft's own rules, which the page does not cover: when several actions have the same key the first in panel order plays; a copied action has no key; New Action does not open while an action is recording. The color is kept with the action for Button Mode, which VectorCraft does not have yet. From b7709a3c1fa37aa505a8cbcc7b7be12535287539 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 11:46:18 -0800 Subject: [PATCH 09/49] MCP: SVG export by a confined caller refuses script links outside its read roots An SVG or SVGZ writes each linked JavaScript file of SVG Interactivity into a