mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 11:54:03 +00:00
Merge main into fix/plugin-grant-app-approval
This commit is contained in:
Generated
+2
-1
@@ -1059,7 +1059,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e#c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -6511,6 +6511,7 @@ name = "vectorcraft-eps"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"encoding_rs",
|
||||
"fax",
|
||||
"flate2",
|
||||
"hayro-ccitt",
|
||||
|
||||
+4
-1
@@ -22,7 +22,7 @@ craft-window = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63
|
||||
# The `.vectorcraft` envelope: its members, version, embedded images and the data after the JSON.
|
||||
craft-vectorcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c365d75c48b0e47f4b43cdb6592d0cdc39d2d52e", features = ["egui"] }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] }
|
||||
# internal
|
||||
vectorcraft-geom = { path = "crates/geom" }
|
||||
vectorcraft-color = { path = "crates/color" }
|
||||
@@ -99,6 +99,9 @@ ruzstd = { version = "0.9.1", default-features = false, features = ["std"] }
|
||||
hayro-ccitt = "0.3"
|
||||
# Substring search for the EPS reader's PostScript `search`.
|
||||
memchr = "2"
|
||||
# The WHATWG Encoding Standard's legacy encodings: names in legacy .ai files read in the code page
|
||||
# the user picks (Mac OS Roman, Korean, Japanese, Chinese).
|
||||
encoding_rs = "0.8.42"
|
||||
jpeg-encoder = { version = "0.6", default-features = false, features = ["std"] }
|
||||
gif = { version = "0.14", default-features = false, features = ["std", "raii_no_panic"] }
|
||||
# CMYK image samples (vectorcraft-doc's cmyk module): the image crate's own TIFF and JPEG
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]...
|
||||
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] [--ppi 300]
|
||||
//! vectorcraft-cli commands
|
||||
//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
//! vectorcraft-cli info FILE
|
||||
//! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5]
|
||||
//! vectorcraft-cli perf [--paths 50000] [--deep]
|
||||
@@ -107,7 +107,7 @@ USAGE:
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
|
||||
formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects
|
||||
resolution; other formats leave them out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
|
||||
@@ -120,7 +120,10 @@ USAGE:
|
||||
IN left things out (hidden text, art or layers it could not read; the notes say which) unless
|
||||
--replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy
|
||||
even where that draws differently from its PDF part or page (which can be older), as long as
|
||||
it can be read. A PDF OUT omits the embedded native document unless --preserve-editing is
|
||||
it can be read. --legacy-name-encoding reads the names in a .ai's or an EPS's editing data
|
||||
that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles,
|
||||
swatches, brushes, symbols, variables and data sets, and data sets' text values) in
|
||||
windows-1252 (the default), macintosh, windows-949, shift_jis, gbk or big5. A PDF OUT omits the embedded native document unless --preserve-editing is
|
||||
given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the
|
||||
attachment is written the result's warnings say so (it is left out, with its own warning, for
|
||||
--artboard or a --range without every artboard).
|
||||
@@ -321,8 +324,20 @@ fn commands() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn convert(args: &[String]) -> Result<(), String> {
|
||||
let valued =
|
||||
["--scale", "-s", "--ppi", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"];
|
||||
let valued = [
|
||||
"--scale",
|
||||
"-s",
|
||||
"--ppi",
|
||||
"--artboard",
|
||||
"-a",
|
||||
"--range",
|
||||
"-r",
|
||||
"--decimals",
|
||||
"--ai-version",
|
||||
"--hidden-objects",
|
||||
"--transparent-images",
|
||||
"--legacy-name-encoding",
|
||||
];
|
||||
if help_requested(args, &valued) {
|
||||
return help();
|
||||
}
|
||||
@@ -331,6 +346,9 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
(None::<f64>, None::<u64>, None::<String>, false, false, false, false);
|
||||
let mut ppi = None::<f64>;
|
||||
let mut decimals = None::<u64>;
|
||||
// The encoding of a legacy `.ai`'s names that aren't UTF-8, passed on as `document.open`'s
|
||||
// `legacyNameEncoding`; the engine checks it.
|
||||
let mut legacy_names = None::<String>;
|
||||
// Legacy `.ai` options, passed on as the export's own (`aiVersion`, `hiddenObjects`,
|
||||
// `transparentImages`); the engine checks their values.
|
||||
let mut legacy: Vec<(&str, String)> = vec![];
|
||||
@@ -353,6 +371,9 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
"--replace-lossy" => replace_lossy = true,
|
||||
"--editing-data" => editing_data = true,
|
||||
"--preserve-editing" => preserve_editing = true,
|
||||
"--legacy-name-encoding" => {
|
||||
legacy_names = Some(it.next().cloned().ok_or("--legacy-name-encoding needs an encoding such as windows-949")?);
|
||||
}
|
||||
"--ai-version" => legacy.push(("aiVersion", it.next().cloned().ok_or("--ai-version needs 7 or pdf")?)),
|
||||
"--hidden-objects" => legacy.push(("hiddenObjects", it.next().cloned().ok_or("--hidden-objects needs layer, omit or visible")?)),
|
||||
"--transparent-images" => legacy.push(("transparentImages", it.next().cloned().ok_or("--transparent-images needs flatten or omit")?)),
|
||||
@@ -365,8 +386,14 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
return Err("a .ait template holds every artboard: --artboard and --range don't apply".into());
|
||||
}
|
||||
let mut h = Headless::new();
|
||||
let opened = if editing_data {
|
||||
let params = json!({"path": input, "editingData": "always"});
|
||||
let opened = if editing_data || legacy_names.is_some() {
|
||||
let mut params = json!({"path": input});
|
||||
if editing_data {
|
||||
params["editingData"] = json!("always");
|
||||
}
|
||||
if let Some(e) = &legacy_names {
|
||||
params["legacyNameEncoding"] = json!(e);
|
||||
}
|
||||
h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))?
|
||||
} else {
|
||||
h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))?
|
||||
|
||||
+18
-4
@@ -128,11 +128,14 @@ fn live_inputs() -> u64 {
|
||||
}
|
||||
|
||||
/// A live object evaluated by [`live_art`] on this thread: the object, its document's symbols (by
|
||||
/// name and art) and patterns and the [`LiveInputs`] as they were, the art and its [`art_size`].
|
||||
/// name and art), patterns, swatches and color groups and the [`LiveInputs`] as they were, the art
|
||||
/// and its [`art_size`].
|
||||
struct EvaluatedLive {
|
||||
node: Node,
|
||||
symbols: Vec<(String, Arc<Node>)>,
|
||||
patterns: Vec<crate::PatternDef>,
|
||||
swatches: crate::Shared<Vec<vectorcraft_color::Swatch>>,
|
||||
swatch_groups: crate::Shared<Vec<vectorcraft_color::SwatchGroup>>,
|
||||
inputs: u64,
|
||||
art: Rc<Vec<Node>>,
|
||||
size: usize,
|
||||
@@ -146,6 +149,8 @@ impl EvaluatedLive {
|
||||
&& self.symbols.iter().zip(&doc.symbols).all(|((name, art), s)| *name == s.name && Arc::ptr_eq(art, &s.art))
|
||||
&& self.node == *n
|
||||
&& self.patterns == doc.patterns
|
||||
&& self.swatches == doc.swatches
|
||||
&& self.swatch_groups == doc.swatch_groups
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,8 +189,8 @@ thread_local! {
|
||||
|
||||
/// The art live object `n` of `doc` draws ([`set_live_art`]); [`crate::live::expand_live`] while
|
||||
/// nothing is registered. The art is evaluated again when `n`, a symbol of `doc`, a pattern of
|
||||
/// `doc` or the [`LiveInputs`] have changed since, and on every click while it or the document's
|
||||
/// symbols or patterns hold a placed document ([`holds_placed`]).
|
||||
/// `doc`, its swatches or color groups, or the [`LiveInputs`] have changed since, and on every
|
||||
/// click while it or the document's symbols or patterns hold a placed document ([`holds_placed`]).
|
||||
fn live_art(doc: &Document, n: &Node) -> Rc<Vec<Node>> {
|
||||
let inputs = live_inputs();
|
||||
let kept = EVALUATED.with_borrow_mut(|kept| {
|
||||
@@ -206,7 +211,16 @@ fn live_art(doc: &Document, n: &Node) -> Rc<Vec<Node>> {
|
||||
return art;
|
||||
}
|
||||
let symbols = doc.symbols.iter().map(|s| (s.name.clone(), s.art.clone())).collect();
|
||||
let e = EvaluatedLive { node: n.clone(), symbols, patterns: doc.patterns.clone(), inputs, art: art.clone(), size };
|
||||
let e = EvaluatedLive {
|
||||
node: n.clone(),
|
||||
symbols,
|
||||
patterns: doc.patterns.clone(),
|
||||
swatches: doc.swatches.clone(),
|
||||
swatch_groups: doc.swatch_groups.clone(),
|
||||
inputs,
|
||||
art: art.clone(),
|
||||
size,
|
||||
};
|
||||
EVALUATED.with_borrow_mut(|kept| {
|
||||
kept.insert(0, e);
|
||||
kept.truncate(LIVE_KEPT);
|
||||
|
||||
@@ -605,11 +605,41 @@ fn evaluated_art_is_not_kept_while_a_placed_document_may_show_in_it() {
|
||||
fn a_kept_evaluation_is_stale_after_the_live_inputs_change() {
|
||||
let d = Document::new(100.0, 100.0);
|
||||
let n = Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
let e = EvaluatedLive { node: n.clone(), symbols: vec![], patterns: vec![], inputs: 7, art: std::rc::Rc::new(vec![]), size: 0 };
|
||||
let e = kept_evaluation(&d, &n);
|
||||
assert!(e.is_of(&d, &n, 7));
|
||||
assert!(!e.is_of(&d, &n, 8));
|
||||
}
|
||||
|
||||
/// An evaluation of `n` in `d`, with inputs 7, holding no symbols, patterns or art.
|
||||
fn kept_evaluation(d: &Document, n: &Node) -> EvaluatedLive {
|
||||
EvaluatedLive {
|
||||
node: n.clone(),
|
||||
symbols: vec![],
|
||||
patterns: vec![],
|
||||
swatches: d.swatches.clone(),
|
||||
swatch_groups: d.swatch_groups.clone(),
|
||||
inputs: 7,
|
||||
art: std::rc::Rc::new(vec![]),
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A kept evaluation is stale once a swatch changes, in a color group too: a painted live effect's
|
||||
/// pieces name swatches.
|
||||
#[test]
|
||||
fn a_kept_evaluation_is_stale_after_a_swatch_changes() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let n = Node::path(NodeId(3), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
let e = kept_evaluation(&d, &n);
|
||||
assert!(e.is_of(&d, &n, 7));
|
||||
d.swatches.first_mut().unwrap().name = "Renamed".into();
|
||||
assert!(!e.is_of(&d, &n, 7));
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let e = kept_evaluation(&d, &n);
|
||||
d.swatch_groups.push(vectorcraft_color::SwatchGroup { name: "Added".into(), swatches: vec![] });
|
||||
assert!(!e.is_of(&d, &n, 7));
|
||||
}
|
||||
|
||||
/// A rectangle (40, 40)–(140, 140) clipping a filled 200 × 200 square drawn over its edges, in a
|
||||
/// clip group (or, `layer`, as the first layer's clipping mask): the group (or the layer), the
|
||||
/// clipping path and the art.
|
||||
|
||||
@@ -93,16 +93,17 @@ pub use save::{
|
||||
remembered_save_options, save_filters, save_format, save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder,
|
||||
templates_folder,
|
||||
};
|
||||
pub(crate) use screens::{
|
||||
PARAM_KEYS as SCREEN_PARAM_KEYS, ROW_KEYS as SCREEN_ROW_KEYS, UNWRITTEN_FORMATS as SCREEN_UNWRITTEN_FORMATS,
|
||||
check_settings as check_screen_settings, export as export_screens, safe_file_name, store_settings as store_screen_settings,
|
||||
with_export_preset as with_screen_export_preset,
|
||||
};
|
||||
pub use screens::{
|
||||
PRESET_FILE_EXTS as SCREEN_EXPORT_PRESET_EXTS, PRESET_FILE_FORMAT as SCREEN_EXPORT_PRESET_FORMAT, PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS,
|
||||
PRESETS as SCREEN_PRESETS, SHARED_KEYS as SCREEN_SETTINGS_KEYS, ScreenExport, ScreenExportPreset, ScreenSize,
|
||||
builtin_presets as screen_export_builtin_presets, first_file as screen_first_file, preset_list as screen_export_preset_list,
|
||||
preset_rows as screen_preset_rows,
|
||||
};
|
||||
pub(crate) use screens::{
|
||||
check_settings as check_screen_settings, export as export_screens, safe_file_name, store_settings as store_screen_settings,
|
||||
with_export_preset as with_screen_export_preset,
|
||||
};
|
||||
pub use svg::options_map as svg_options;
|
||||
/// Atomic file writes (a temporary file renamed over the target: a failed write never damages the
|
||||
/// file it replaces), for the apps' own writers too.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -184,7 +184,8 @@ fn through_editing_data(
|
||||
) -> (Document, Vec<String>, bool) {
|
||||
let Some(layers) = layers else { return (doc, warnings, false) };
|
||||
let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines;
|
||||
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit);
|
||||
let (doc, mut warnings, read) =
|
||||
vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit, opts.legacy_names);
|
||||
// The layers weren't read (or couldn't be): the notes of reading the PDF part still hold.
|
||||
if !read {
|
||||
return (doc, warnings, false);
|
||||
@@ -209,12 +210,12 @@ fn from_editing_data_alone(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions)
|
||||
return None;
|
||||
}
|
||||
let private = opened.illustrator_data()?;
|
||||
Some(editing_copy_alone(&private))
|
||||
Some(editing_copy_alone(&private, opts))
|
||||
}
|
||||
|
||||
/// [`from_editing_data_alone`] of `private`, the editor's copy already read.
|
||||
fn editing_copy_alone(private: &[u8]) -> Result<(Document, Vec<String>)> {
|
||||
vectorcraft_eps::ai_alone(private)
|
||||
fn editing_copy_alone(private: &[u8], opts: &LoadOptions) -> Result<(Document, Vec<String>)> {
|
||||
vectorcraft_eps::ai_alone_with(private, opts.legacy_names)
|
||||
.map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})")))
|
||||
}
|
||||
|
||||
@@ -247,7 +248,7 @@ fn without_pdf_part(bytes: &[u8], opts: &LoadOptions, e: &EngineError) -> Option
|
||||
return None;
|
||||
}
|
||||
let private = vectorcraft_pdf::illustrator_data_without_pdf_part(bytes, opts.password.as_deref())?;
|
||||
let (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit).ok()?;
|
||||
let (doc, warnings) = vectorcraft_eps::ai_alone_on_page(&private.data, private.trim, private.user_unit, opts.legacy_names).ok()?;
|
||||
let notes = std::iter::once(format!("the PDF part couldn't be read ({e}): the file opened from its editing data")).chain(warnings).collect();
|
||||
Some(outlined_if_asked(doc, notes, opts))
|
||||
}
|
||||
@@ -323,7 +324,7 @@ pub(crate) fn page_or_editing_data(
|
||||
match read {
|
||||
Ok((doc, notes)) => Ok((doc, notes, false)),
|
||||
Err(e) if is_placeholder(&e) => {
|
||||
let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), ..Default::default() })?;
|
||||
let l = load_with(name, bytes, &LoadOptions { password: opts.password.clone(), legacy_names: opts.legacy_names, ..Default::default() })?;
|
||||
Ok((l.doc, l.warnings, true))
|
||||
}
|
||||
Err(e) if page_index == 0 => match load_with(name, bytes, &LoadOptions { pages: None, ..opts.clone() }) {
|
||||
@@ -352,7 +353,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
_ if matches!(format.id, "eps" | "ps") || vectorcraft_pdf::is_postscript(bytes) => {
|
||||
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
|
||||
restore_or_import(editing, || {
|
||||
let r = vectorcraft_eps::import_trusting(bytes, opts.editing_data, opts.trust_editing_data)
|
||||
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
|
||||
.map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
|
||||
Ok((r.document, r.warnings))
|
||||
})?
|
||||
@@ -391,7 +392,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
if let Some((o, private)) = opened.as_ref().ok().filter(|_| reads_layers_alone(opts)).and_then(|o| Some((o, editing_copy(o, opts)?)))
|
||||
{
|
||||
let pages = o.trim_pages();
|
||||
match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit) {
|
||||
match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit, opts.legacy_names) {
|
||||
Ok((mut doc, warnings)) => {
|
||||
doc.setup.bleed = pages.bleed;
|
||||
editing_data = true;
|
||||
@@ -413,8 +414,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
// decoded draws nothing either): its notes are at most the page's own.
|
||||
let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w));
|
||||
let copy_alone = || match &private {
|
||||
Some(private) => Some(editing_copy_alone(private)),
|
||||
None if layers.is_some() => opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p)),
|
||||
Some(private) => Some(editing_copy_alone(private, opts)),
|
||||
None if layers.is_some() => {
|
||||
opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p, opts))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let alone = (r.only_text || empty)
|
||||
|
||||
@@ -39,6 +39,9 @@ pub struct LoadOptions {
|
||||
/// A `.ai` opens from its editing copy even where that draws differently from its PDF part
|
||||
/// (which can be older than it), with a warning, instead of from the PDF part.
|
||||
pub trust_editing_data: bool,
|
||||
/// The encoding names in a `.ai` or EPS's editing data are read in when they aren't UTF-8
|
||||
/// (`legacyNameEncoding`).
|
||||
pub legacy_names: vectorcraft_eps::NameEncoding,
|
||||
/// A PDF or `.ai` saved with Preserve Editing opens from the VectorCraft document it carries;
|
||||
/// off, it opens as another app would open it, from the editing copy of its art or its pages.
|
||||
pub native: bool,
|
||||
@@ -57,6 +60,7 @@ impl Default for LoadOptions {
|
||||
dxf: Default::default(),
|
||||
editing_data: true,
|
||||
trust_editing_data: false,
|
||||
legacy_names: vectorcraft_eps::NameEncoding::default(),
|
||||
native: true,
|
||||
}
|
||||
}
|
||||
@@ -65,8 +69,9 @@ impl Default for LoadOptions {
|
||||
impl LoadOptions {
|
||||
/// From command params: `pages` (`"2-3, 5"`, a page number or `"all"`), `page` (one page, when
|
||||
/// `pages` isn't given), `cropTo` (or `crop`), `password`, `colorMode` (`rgb` | `cmyk`) with `grays`,
|
||||
/// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`) and `dxf`
|
||||
/// (the DXF import options).
|
||||
/// `textAs` (`text` | `outlines`), `layers` (true | false), `editingData` (true | false | `always`),
|
||||
/// `legacyNameEncoding` (one of [`vectorcraft_eps::NameEncoding::LABELS`]) and `dxf` (the DXF
|
||||
/// import options).
|
||||
pub fn from_params(cmd: &str, p: &Value) -> Result<Self> {
|
||||
let pages = match p.get("pages").filter(|v| !v.is_null()).or_else(|| p.get("page").filter(|v| !v.is_null())) {
|
||||
None => None,
|
||||
@@ -95,8 +100,15 @@ impl LoadOptions {
|
||||
Some(Value::String(s)) if s.eq_ignore_ascii_case("always") => (true, true),
|
||||
Some(v) => (v.as_bool().ok_or_else(|| bad(cmd, "editingData must be true, false or \"always\""))?, false),
|
||||
};
|
||||
let legacy_names = match p.get("legacyNameEncoding").filter(|v| !v.is_null()) {
|
||||
None => vectorcraft_eps::NameEncoding::default(),
|
||||
Some(v) => v
|
||||
.as_str()
|
||||
.and_then(vectorcraft_eps::NameEncoding::from_label)
|
||||
.ok_or_else(|| bad(cmd, format!("legacyNameEncoding must be one of {}", vectorcraft_eps::NameEncoding::LABELS.join(", "))))?,
|
||||
};
|
||||
let dxf = super::dxfimport::options(cmd, p.get("dxf"))?;
|
||||
Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, native: true })
|
||||
Ok(Self { pages, crop, password, color_mode, grays, text_as, layers, dxf, editing_data, trust_editing_data, legacy_names, native: true })
|
||||
}
|
||||
|
||||
/// Does the document read only part of the file, or read it differently (a page range, another
|
||||
|
||||
@@ -1102,7 +1102,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Save Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified",
|
||||
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused",
|
||||
has_doc,
|
||||
|s, p| save(s, SaveMode::Save, p)
|
||||
),
|
||||
|
||||
@@ -193,7 +193,10 @@ pub(super) fn name_without_device(name: &str) -> String {
|
||||
}
|
||||
|
||||
/// Keys of a format row that aren't encoder options.
|
||||
const ROW_KEYS: [&str; 7] = ["format", "scale", "ppi", "width", "height", "suffix", "folder"];
|
||||
pub(crate) const ROW_KEYS: [&str; 7] = ["format", "scale", "ppi", "width", "height", "suffix", "folder"];
|
||||
|
||||
/// The writable formats a format row can't name.
|
||||
pub(crate) const UNWRITTEN_FORMATS: [&str; 5] = ["template", "txt", "dxf", "emf", "wmf"];
|
||||
|
||||
/// One format row: what to write, at which size, with which suffix, into which sub-folder.
|
||||
struct Row {
|
||||
@@ -214,7 +217,7 @@ impl Row {
|
||||
return Err(bad(C, "each format is an object {format, scale?, suffix?}"));
|
||||
}
|
||||
let format = writable(C, Some(str_param(row, "format").unwrap_or("png")), None)?;
|
||||
if matches!(format.id, "template" | "txt" | "dxf" | "emf" | "wmf") {
|
||||
if UNWRITTEN_FORMATS.contains(&format.id) {
|
||||
return Err(bad(C, "Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft"));
|
||||
}
|
||||
// Vector formats have no pixel size: it doesn't apply and adds no @2x suffix (not even one
|
||||
@@ -285,6 +288,36 @@ impl Row {
|
||||
/// or where to.
|
||||
pub const SHARED_KEYS: [&str; 6] = ["formats", "preset", "settings", "prefix", "subfolders", "antiAlias"];
|
||||
|
||||
/// The keys `document.exportForScreens` reads: those of [`ScreenParams`], the formats and their
|
||||
/// settings, `antiAlias`, `exportPreset` and `openLocation`, which the app reads.
|
||||
pub(crate) const PARAM_KEYS: [&str; 25] = [
|
||||
"artboard",
|
||||
"artboards",
|
||||
"range",
|
||||
"folder",
|
||||
"zip",
|
||||
"fullDocument",
|
||||
"includeBleed",
|
||||
"includeArtboardFill",
|
||||
"subfolders",
|
||||
"preset",
|
||||
"prefix",
|
||||
"assets",
|
||||
"layers",
|
||||
"selection",
|
||||
"name",
|
||||
"counterStart",
|
||||
"counterPer",
|
||||
"date",
|
||||
"folders",
|
||||
"pdfs",
|
||||
"formats",
|
||||
"settings",
|
||||
"antiAlias",
|
||||
"exportPreset",
|
||||
"openLocation",
|
||||
];
|
||||
|
||||
/// What `document.exportForScreens` reads besides the formats.
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
|
||||
@@ -1022,6 +1022,21 @@ fn an_ai_file_saved_without_its_pdf_part_pastes_from_its_editing_data() {
|
||||
assert!(e.contains("placeholder"), "{e}");
|
||||
}
|
||||
|
||||
/// `file.place` reads a `.ai` saved without its PDF part in the `legacyNameEncoding` it is given.
|
||||
#[test]
|
||||
fn an_ai_file_saved_without_its_pdf_part_places_in_the_encoding_picked() {
|
||||
let mut data = ai::sample_data();
|
||||
let at = data.windows(4).position(|w| w == b"Pair").unwrap();
|
||||
// 빨강 in code page 949.
|
||||
data.splice(at..at + 4, br"\273\241\260\255".iter().copied());
|
||||
let file = vectorcraft_format::base64_encode(&ai::ai(&compressed(&data), PLACEHOLDER));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"name": "art.ai", "dataBase64": file, "link": false, "legacyNameEncoding": "windows-949"})).unwrap();
|
||||
let placed = s.doc().unwrap().doc.layers[0].children().unwrap()[0].clone();
|
||||
assert_eq!(names(&placed.children().unwrap()[1..]), ["빨강", "Ring", "Window"], "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_file_saved_without_its_pdf_part_places_from_its_editing_data() {
|
||||
let [sample, damaged, plain] = placeholder_files();
|
||||
|
||||
@@ -184,6 +184,41 @@ showpage
|
||||
assert!(e.contains("junk.eps"), "{e}");
|
||||
}
|
||||
|
||||
/// `legacyNameEncoding` picks the code page of a legacy `.ai`'s layer names that aren't UTF-8; by
|
||||
/// default they come in as Windows-1252, with a warning naming them. An encoding it doesn't know is
|
||||
/// refused.
|
||||
#[test]
|
||||
fn legacy_ai_layer_names_open_in_the_encoding_picked() {
|
||||
let mut s = Session::new();
|
||||
// A layer named 레이어 1 in Windows code page 949.
|
||||
let ai = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 100 100\n%%DocumentNeededResources: procset Adobe_IllustratorA_AI3 1.0 0\n%AI3_TemplateBox: 50 50 50 50\n%%EndComments\n\
|
||||
%%BeginProlog\n%%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 0\n%%EndProlog\n%%BeginSetup\nAdobe_IllustratorA_AI3 /initialize get exec\n%%EndSetup\n\
|
||||
%AI5_BeginLayer\n1 1 1 1 0 0 2 0 0 0 Lb\n(\\267\\271\\300\\314\\276\\356 1) Ln\n0 0 0 1 K\n0 i 1 w\n10 10 m\n90 90 L\nS\nLB\n%AI5_EndLayer--\n%%PageTrailer\n%%Trailer\n%%EOF\n";
|
||||
let open_as = |s: &mut Session, encoding: Option<&str>| {
|
||||
let mut p = json!({"name": "korean.ai", "dataBase64": vectorcraft_format::base64_encode(ai)});
|
||||
if let Some(e) = encoding {
|
||||
p["legacyNameEncoding"] = json!(e);
|
||||
}
|
||||
s.execute("document.open", &p)
|
||||
};
|
||||
let r = open_as(&mut s, None).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("·¹À̾î 1"));
|
||||
assert_eq!(
|
||||
r["warnings"],
|
||||
json!(["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"])
|
||||
);
|
||||
for encoding in ["windows-949", "EUC-KR"] {
|
||||
let r = open_as(&mut s, Some(encoding)).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].name.as_deref(), Some("레이어 1"), "{encoding}");
|
||||
assert!(r["warnings"][0].as_str().is_some_and(|w| w.contains("`레이어 1`") && w.contains("windows-949")), "{r}");
|
||||
}
|
||||
let e = open_as(&mut s, Some("utf-16")).unwrap_err();
|
||||
assert!(
|
||||
matches!(&e, EngineError::BadParams { msg, .. } if msg == "legacyNameEncoding must be one of windows-1252, macintosh, windows-949, shift_jis, gbk, big5"),
|
||||
"{e}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_files_place_as_one_group_clipped_to_their_bounding_box() {
|
||||
let mut s = rich();
|
||||
|
||||
+178
-34
@@ -6,9 +6,9 @@
|
||||
//! by `|`; a `?` marks an optional one, and a `:`, `=` or `(` starts what the entry says about it. An entry that starts
|
||||
//! with `…` stands for keys listed elsewhere: those of each command it names after `as` or `of`
|
||||
//! (`…settings as file.new`), or, naming none, any key (the options of the format an export picks). Objects inside a value aren't
|
||||
//! described key by key, so only the top level is checked, except in an export's options: those
|
||||
//! are checked against the options of the formats the command can write, in `marks`, `bleed` and
|
||||
//! the other objects too ([`check_export_options`]).
|
||||
//! described key by key, so only the top level is checked, except in the options of an export, a
|
||||
//! save or Export for Screens: those are checked against the options of the formats the command
|
||||
//! can write, in `marks`, `bleed` and the other objects too ([`check_export_options`]).
|
||||
//!
|
||||
//! [`Session::execute`] answers a key the description doesn't name with a parameter error
|
||||
//! ([`check_params`]), for every caller (the app, the control channel, MCP, the CLI) and for the
|
||||
@@ -531,25 +531,183 @@ fn check_option_tree(id: &str, within: &str, given: &serde_json::Map<String, Val
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The options of an export command `id`. `document.export`, `document.exportSelection` and
|
||||
/// `document.serialize` take a key that some format reads (one object can carry several formats'
|
||||
/// options; `document.formats` lists them). `document.exportPdf`, `document.pdfSettings` and
|
||||
/// `pdf.preset.save` take a key that PDF reads, `document.exportEps` one that EPS reads and
|
||||
/// `document.exportDxf` one that DXF reads. Where a command takes `marks`, `bleed`, `flattener` or
|
||||
/// another object option of PDF or EPS, only the object's own keys pass. An SVG export's `svg`
|
||||
/// object is checked when it is read. Saved presets are not checked: one with a key that is no
|
||||
/// longer read keeps loading.
|
||||
/// `given`, the options object at `within` (`""` for the params themselves, `options.`,
|
||||
/// `formats[0].`), has only `keys`, and only their own keys in the PDF and EPS object options
|
||||
/// (`trees`). `why_not` says why a key that is not near any of `keys` is refused, and where the
|
||||
/// keys are listed.
|
||||
fn check_option_keys(id: &str, within: &str, given: &serde_json::Map<String, Value>, keys: &[&str], why_not: &str, trees: &Value) -> Result<()> {
|
||||
for (key, value) in given {
|
||||
if !keys.contains(&key.as_str()) {
|
||||
return Err(bad(
|
||||
id,
|
||||
match nearest(key, keys) {
|
||||
Some(near) => format!("unknown parameter `{within}{key}`; did you mean `{within}{near}`?"),
|
||||
None => format!("unknown parameter `{within}{key}`; {why_not}"),
|
||||
},
|
||||
));
|
||||
}
|
||||
if let (Some(Value::Object(schema)), Value::Object(value)) = (trees.get(key), value) {
|
||||
check_option_tree(id, &format!("{within}{key}."), value, schema)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Why an export option that no format reads is refused.
|
||||
const NO_FORMAT: &str = "no format takes it (see document.formats)";
|
||||
|
||||
/// Why a save option that no save format reads is refused.
|
||||
const NO_SAVE_FORMAT: &str = "no save format takes it (see file.formatOptions)";
|
||||
|
||||
/// The format ids Export for Screens writes: the writable formats, those exporter plug-ins add
|
||||
/// included, but those it doesn't write.
|
||||
pub fn screen_formats() -> Vec<&'static str> {
|
||||
use super::fileio::{FORMATS, SCREEN_UNWRITTEN_FORMATS, plugin_formats};
|
||||
let added = plugin_formats();
|
||||
FORMATS.iter().chain(added.iter().copied()).filter(|f| f.write && !SCREEN_UNWRITTEN_FORMATS.contains(&f.id)).map(|f| f.id).collect()
|
||||
}
|
||||
|
||||
/// The keys a format row of Export for Screens, and the settings of a format, take: the row's own
|
||||
/// (`format`, `scale`…), `includeArtboardFill` and `richBlack`, which the encoders read from a
|
||||
/// row's options (`with_artboard_fills`, the render options), `params`, an exporter plug-in's own,
|
||||
/// and a key some format reads, as `document.export` takes them.
|
||||
fn screen_option_keys() -> Vec<&'static str> {
|
||||
use super::fileio::{FORMATS, SCREEN_ROW_KEYS};
|
||||
let mut keys: Vec<&str> = SCREEN_ROW_KEYS.to_vec();
|
||||
keys.extend(["includeArtboardFill", "richBlack", "params"]);
|
||||
keys.extend(FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).map(|o| o.name));
|
||||
keys
|
||||
}
|
||||
|
||||
/// A format row of Export for Screens, or the settings of a format, without the keys the export
|
||||
/// refuses in it ([`check_screen_rows`]), inside `marks`, `svg` and the other object options too:
|
||||
/// a saved row or preset that holds a key that is no longer read loads without it.
|
||||
pub fn drop_unread_row_options(row: &mut serde_json::Map<String, Value>) {
|
||||
fn retain_tree(o: &mut serde_json::Map<String, Value>, schema: &serde_json::Map<String, Value>) {
|
||||
o.retain(|k, _| schema.contains_key(k));
|
||||
for (k, v) in o.iter_mut() {
|
||||
if let (Some(Value::Object(inner)), Value::Object(v)) = (schema.get(k), v) {
|
||||
retain_tree(v, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
let keys = screen_option_keys();
|
||||
row.retain(|k, _| keys.contains(&k.as_str()));
|
||||
let trees = screen_option_trees();
|
||||
for (k, v) in row.iter_mut() {
|
||||
if let (Some(Value::Object(schema)), Value::Object(v)) = (trees.get(k), v) {
|
||||
retain_tree(v, schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`option_trees`] of Export for Screens: its remembered settings mark PDF security whose
|
||||
/// passwords were left out with `security.passwordRequired`, which the export reads.
|
||||
fn screen_option_trees() -> Value {
|
||||
let mut trees = option_trees();
|
||||
if let Some(security) = trees.get_mut("security").and_then(Value::as_object_mut) {
|
||||
security.insert("passwordRequired".into(), Value::Null);
|
||||
}
|
||||
trees
|
||||
}
|
||||
|
||||
/// The object options an export or save checks key by key: PDF's `marks`, `bleed` and the others,
|
||||
/// the flattener's, and SVG's `svg` with the artboard keys it may carry.
|
||||
fn option_trees() -> Value {
|
||||
let mut trees = pdf_option_tree();
|
||||
if let Some(tree) = trees.as_object_mut() {
|
||||
tree.insert("flattener".into(), serde_json::to_value(super::flatten::FlattenOptions::default()).unwrap_or_default());
|
||||
let mut svg = serde_json::to_value(vectorcraft_svg::ExportOptions::default()).unwrap_or_default();
|
||||
if let Some(svg) = svg.as_object_mut() {
|
||||
svg.extend(super::fileio::ARTBOARD_PARAMS.iter().chain(&["useArtboards"]).map(|k| (k.to_string(), Value::Null)));
|
||||
}
|
||||
tree.insert("svg".into(), svg);
|
||||
}
|
||||
trees
|
||||
}
|
||||
|
||||
/// The format rows (`formats`) and the per-format `settings` of Export for Screens params `p`, at
|
||||
/// `within` (`settings.` in `document.exportPresets.save`). A row, and the settings of a format,
|
||||
/// take [`screen_option_keys`]; `settings` takes the formats Export for Screens writes.
|
||||
fn check_screen_rows(id: &str, within: &str, p: &serde_json::Map<String, Value>) -> Result<()> {
|
||||
let keys = screen_option_keys();
|
||||
let trees = &screen_option_trees();
|
||||
if let Some(Value::Array(rows)) = p.get("formats") {
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
if let Value::Object(row) = row {
|
||||
check_option_keys(id, &format!("{within}formats[{i}]."), row, &keys, NO_FORMAT, trees)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(Value::Object(settings)) = p.get("settings") {
|
||||
let formats = screen_formats();
|
||||
for (format, options) in settings {
|
||||
if !formats.contains(&format.as_str()) {
|
||||
let why = match nearest(format, &formats) {
|
||||
Some(near) => format!("did you mean `{within}settings.{near}`?"),
|
||||
None => format!("`{within}settings` takes: {}", formats.join(", ")),
|
||||
};
|
||||
return Err(bad(id, format!("unknown parameter `{within}settings.{format}`; {why}")));
|
||||
}
|
||||
if let Value::Object(options) = options {
|
||||
check_option_keys(id, &format!("{within}settings.{format}."), options, &keys, NO_FORMAT, trees)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The options of an export or save command `id`. `document.export`, `document.exportSelection`
|
||||
/// and `document.serialize` take a key that some format reads (one object can carry several
|
||||
/// formats' options; `document.formats` lists them). `document.exportPdf`, `document.pdfSettings`
|
||||
/// and `pdf.preset.save` take a key that PDF reads, `document.exportEps` one that EPS reads and
|
||||
/// `document.exportDxf` one that DXF reads. `document.save`, `file.saveAs`, `file.saveCopy` and
|
||||
/// `file.saveAsTemplate` take, beside the path and in `options`, a key that some save format
|
||||
/// reads. `document.exportForScreens` takes the keys it reads, and checks its format rows and
|
||||
/// settings ([`check_screen_rows`]), as `document.exportPresets.save` does in its `settings` and
|
||||
/// `assets.export` and `assets.settings.set` do in theirs.
|
||||
/// Where a command takes `marks`, `bleed`, `flattener`, `svg` or another object option of PDF,
|
||||
/// EPS or SVG, only the object's own keys pass ([`option_trees`]). Saved presets are not checked:
|
||||
/// one with a key that is no longer read keeps loading.
|
||||
fn check_export_options(id: &str, params: &Value) -> Result<()> {
|
||||
use super::fileio::{FORMATS, dxf, eps, pdf};
|
||||
use super::fileio::{FORMATS, SAVE_FORMATS, SCREEN_PARAM_KEYS, TEMPLATE_FORMATS, dxf, eps, pdf};
|
||||
let Value::Object(given) = params else { return Ok(()) };
|
||||
match (id, given.get("settings")) {
|
||||
("document.exportForScreens", _) => {
|
||||
if let Some(key) = given.keys().find(|k| !SCREEN_PARAM_KEYS.contains(&k.as_str())) {
|
||||
return Err(unknown(id, key, &SCREEN_PARAM_KEYS));
|
||||
}
|
||||
return check_screen_rows(id, "", given);
|
||||
}
|
||||
("document.exportPresets.save", Some(Value::Object(settings))) => {
|
||||
let why = format!("`settings` takes: {}", SCREEN_PARAM_KEYS.join(", "));
|
||||
check_option_keys(id, "settings.", settings, &SCREEN_PARAM_KEYS, &why, &Value::Null)?;
|
||||
return check_screen_rows(id, "settings.", settings);
|
||||
}
|
||||
// Export Assets exports with, and stores, Export for Screens' format rows and settings.
|
||||
("assets.export" | "assets.settings.set", _) => return check_screen_rows(id, "", given),
|
||||
_ => {}
|
||||
}
|
||||
let mut keys: Vec<&str> = vec!["path", "format", "acknowledgeLoss"];
|
||||
// A save's `options` and the keys they take.
|
||||
let mut inner: Option<(&serde_json::Map<String, Value>, Vec<&str>)> = None;
|
||||
let why_not = match id {
|
||||
"document.save" | "file.saveAs" | "file.saveCopy" | "file.saveAsTemplate" => {
|
||||
let saved = FORMATS.iter().filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id));
|
||||
let options: Vec<&str> = saved.flat_map(|f| f.options).map(|o| o.name).collect();
|
||||
if let Some(Value::Object(o)) = given.get("options") {
|
||||
inner = Some((o, options.clone()));
|
||||
}
|
||||
keys.extend(["options", "modified"]);
|
||||
keys.extend(options);
|
||||
NO_SAVE_FORMAT
|
||||
}
|
||||
"document.export" | "document.exportSelection" | "document.serialize" => {
|
||||
// `richBlack` is Appearance of Black's setting for a raster export, and `params` an
|
||||
// exporter plug-in's own.
|
||||
keys.extend(["includeArtboardFill", "selectedOnly", "richBlack", "params"]);
|
||||
keys.extend(FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).map(|o| o.name));
|
||||
"no format takes it"
|
||||
NO_FORMAT
|
||||
}
|
||||
"document.exportPdf" | "document.pdfSettings" | "pdf.preset.save" => {
|
||||
keys.push("includeArtboardFill");
|
||||
@@ -559,38 +717,24 @@ fn check_export_options(id: &str, params: &Value) -> Result<()> {
|
||||
"pdf.preset.save" => keys.extend(["name", "newName", "description"]),
|
||||
_ => {}
|
||||
}
|
||||
"PDF doesn't take it"
|
||||
"PDF doesn't take it (see document.formats)"
|
||||
}
|
||||
"document.exportEps" => {
|
||||
keys.push("includeArtboardFill");
|
||||
keys.extend(eps::OPTIONS.iter().map(|o| o.name));
|
||||
"EPS doesn't take it"
|
||||
"EPS doesn't take it (see document.formats)"
|
||||
}
|
||||
"document.exportDxf" => {
|
||||
keys.extend(dxf::OPTIONS.iter().map(|o| o.name));
|
||||
"DXF doesn't take it"
|
||||
"DXF doesn't take it (see document.formats)"
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let mut trees = pdf_option_tree();
|
||||
if let Some(tree) = trees.as_object_mut() {
|
||||
tree.insert("flattener".into(), serde_json::to_value(super::flatten::FlattenOptions::default()).unwrap_or_default());
|
||||
let trees = option_trees();
|
||||
if let Some((o, options)) = inner {
|
||||
check_option_keys(id, "options.", o, &options, NO_SAVE_FORMAT, &trees)?;
|
||||
}
|
||||
for (key, value) in given {
|
||||
if !keys.contains(&key.as_str()) {
|
||||
return Err(bad(
|
||||
id,
|
||||
match nearest(key, &keys) {
|
||||
Some(near) => format!("unknown parameter `{key}`; did you mean `{near}`?"),
|
||||
None => format!("unknown parameter `{key}`; {why_not} (see document.formats)"),
|
||||
},
|
||||
));
|
||||
}
|
||||
if let (Some(Value::Object(schema)), Value::Object(value)) = (trees.get(key), value) {
|
||||
check_option_tree(id, &format!("{key}."), value, schema)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
check_option_keys(id, "", given, &keys, why_not, &trees)
|
||||
}
|
||||
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` or an `action.batch` included,
|
||||
|
||||
@@ -177,6 +177,208 @@ fn export_options_pass_every_listed_key() {
|
||||
}
|
||||
}
|
||||
|
||||
const SAVES: [&str; 4] = ["document.save", "file.saveAs", "file.saveCopy", "file.saveAsTemplate"];
|
||||
|
||||
/// A save refuses a key no save format reads, beside the path, in `options` and in the PDF and
|
||||
/// SVG objects of either, under each save command.
|
||||
#[test]
|
||||
fn save_options_refuse_a_key_no_save_format_reads() {
|
||||
let why = "no save format takes it (see file.formatOptions)";
|
||||
for id in SAVES {
|
||||
assert_eq!(refusal(id, json!({"path": "a.pdf", "noSuchOption": true})), format!("unknown parameter `noSuchOption`; {why}"));
|
||||
assert_eq!(
|
||||
refusal(id, json!({"path": "a.pdf", "options": {"noSuchOption": true}})),
|
||||
format!("unknown parameter `options.noSuchOption`; {why}")
|
||||
);
|
||||
assert_eq!(refusal(id, json!({"compres": true})), "unknown parameter `compres`; did you mean `compress`?");
|
||||
assert_eq!(refusal(id, json!({"options": {"compres": true}})), "unknown parameter `options.compres`; did you mean `options.compress`?");
|
||||
// An export's own key is not a save's.
|
||||
assert_eq!(refusal(id, json!({"path": "a.png", "quality": 80})), format!("unknown parameter `quality`; {why}"));
|
||||
for (given, message) in [
|
||||
(json!({"path": "a.pdf", "marks": {"useDocumentBleed": true}}), "unknown parameter `marks.useDocumentBleed`; `marks` takes:"),
|
||||
(json!({"path": "a.pdf", "options": {"marks": {"x": 1}}}), "unknown parameter `options.marks.x`; `options.marks` takes:"),
|
||||
(json!({"path": "a.ai", "bleed": {"useDocumen": true}}), "unknown parameter `bleed.useDocumen`; did you mean `bleed.useDocument`?"),
|
||||
// The `svg` object is checked when the save writes no SVG too.
|
||||
(json!({"path": "a.pdf", "svg": {"noSuchOption": true}}), "unknown parameter `svg.noSuchOption`; `svg` takes:"),
|
||||
(
|
||||
json!({"path": "a.pdf", "options": {"svg": {"minfy": true}}}),
|
||||
"unknown parameter `options.svg.minfy`; did you mean `options.svg.minify`?",
|
||||
),
|
||||
] {
|
||||
let got = refusal(id, given.clone());
|
||||
assert!(got.starts_with(message), "{id} {given}: {got}");
|
||||
}
|
||||
// A key that another format a save writes reads still passes: the save leaves it out.
|
||||
for given in [
|
||||
json!({"path": "a.svg", "compress": true}),
|
||||
json!({"path": "a.pdf", "options": {"preview": true, "styling": "style"}}),
|
||||
json!({"path": "a.vectorcraft", "standard": "pdfX4", "marks": {"trim": true}}),
|
||||
json!({"path": "a.ai", "acknowledgeLoss": true, "modified": null, "svg": {"minify": true}}),
|
||||
json!({"path": "a.svg", "svg": {"styling": "css", "artboard": 0, "useArtboards": true}}),
|
||||
] {
|
||||
check_params(id, &given).unwrap_or_else(|e| panic!("{id} {given}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A save passes each option of each format it writes, beside the path and in `options`, and
|
||||
/// each key its description names.
|
||||
#[test]
|
||||
fn save_options_pass_every_listed_key() {
|
||||
use crate::cmd::fileio::{FORMATS, SAVE_FORMATS, TEMPLATE_FORMATS};
|
||||
for f in FORMATS.iter().filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id)) {
|
||||
for o in f.options {
|
||||
for params in [json!({"format": f.id, o.name: null}), json!({"format": f.id, "options": {o.name: null}})] {
|
||||
for id in SAVES {
|
||||
check_params(id, ¶ms).unwrap_or_else(|e| panic!("{id} {params}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in SAVES {
|
||||
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
|
||||
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
|
||||
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export for Screens refuses a key it doesn't take, and in its rows and settings a key no format
|
||||
/// reads, by the row's path; `document.exportPresets.save` checks the rows of its settings.
|
||||
#[test]
|
||||
fn export_for_screens_refuses_a_key_no_format_reads() {
|
||||
let id = "document.exportForScreens";
|
||||
assert!(
|
||||
refusal(id, json!({"folder": "out", "noSuchOption": true})).starts_with("unknown parameter `noSuchOption`; the command takes: artboard, ")
|
||||
);
|
||||
assert_eq!(refusal(id, json!({"subfolder": true})), "unknown parameter `subfolder`; did you mean `subfolders`?");
|
||||
let no_format = "no format takes it (see document.formats)";
|
||||
for (given, message) in [
|
||||
(json!({"formats": [{"format": "png", "noSuchOption": 1}]}), format!("unknown parameter `formats[0].noSuchOption`; {no_format}")),
|
||||
(
|
||||
json!({"formats": [{"format": "png"}, {"format": "pdf", "marks": {"useDocumentBleed": true}}]}),
|
||||
"unknown parameter `formats[1].marks.useDocumentBleed`; `formats[1].marks` takes:".into(),
|
||||
),
|
||||
(
|
||||
json!({"formats": [{"format": "pdf", "security": {"openPasswrd": "x"}}]}),
|
||||
"unknown parameter `formats[0].security.openPasswrd`; did you mean `formats[0].security.openPassword`?".into(),
|
||||
),
|
||||
(
|
||||
json!({"formats": [{"format": "eps", "flattener": {"balanse": 50}}]}),
|
||||
"unknown parameter `formats[0].flattener.balanse`; did you mean `formats[0].flattener.balance`?".into(),
|
||||
),
|
||||
(json!({"formats": [{"format": "jpg", "sufix": "@2x"}]}), "unknown parameter `formats[0].sufix`; did you mean `formats[0].suffix`?".into()),
|
||||
(json!({"settings": {"pnj": {}}}), "unknown parameter `settings.pnj`; did you mean `settings.png`?".into()),
|
||||
(json!({"settings": {"dxf": {}}}), "unknown parameter `settings.dxf`; `settings` takes: ".into()),
|
||||
(
|
||||
json!({"settings": {"png": {"bckground": "white"}}}),
|
||||
"unknown parameter `settings.png.bckground`; did you mean `settings.png.background`?".into(),
|
||||
),
|
||||
(json!({"settings": {"pdf": {"bleed": {"top": 9, "inner": 1}}}}), "unknown parameter `settings.pdf.bleed.inner`;".into()),
|
||||
] {
|
||||
let got = refusal(id, given.clone());
|
||||
assert!(got.starts_with(&message), "{given}: {got}");
|
||||
}
|
||||
let save = "document.exportPresets.save";
|
||||
assert_eq!(
|
||||
refusal(save, json!({"name": "a", "settings": {"formats": [{"format": "png", "noSuchOption": 1}]}})),
|
||||
format!("unknown parameter `settings.formats[0].noSuchOption`; {no_format}")
|
||||
);
|
||||
assert!(
|
||||
refusal(save, json!({"name": "a", "settings": {"settings": {"pdf": {"marks": {"x": 1}}}}}))
|
||||
.starts_with("unknown parameter `settings.settings.pdf.marks.x`;")
|
||||
);
|
||||
assert_eq!(
|
||||
refusal(save, json!({"name": "a", "settings": {"formtas": [{"format": "png"}]}})),
|
||||
"unknown parameter `settings.formtas`; did you mean `settings.formats`?"
|
||||
);
|
||||
assert!(
|
||||
refusal(save, json!({"name": "a", "settings": {"noSuchOption": true}}))
|
||||
.starts_with("unknown parameter `settings.noSuchOption`; `settings` takes: artboard, ")
|
||||
);
|
||||
// Export Assets takes the same rows and settings, and stores them.
|
||||
for asset in ["assets.export", "assets.settings.set"] {
|
||||
assert_eq!(
|
||||
refusal(asset, json!({"formats": [{"format": "png", "noSuchOption": 1}]})),
|
||||
format!("unknown parameter `formats[0].noSuchOption`; {no_format}")
|
||||
);
|
||||
assert!(refusal(asset, json!({"settings": {"pdf": {"marks": {"x": 1}}}})).starts_with("unknown parameter `settings.pdf.marks.x`;"));
|
||||
check_params(asset, &json!({"formats": [{"format": "jpg", "scale": "2x", "quality": 80}]})).unwrap();
|
||||
}
|
||||
// An exporter plug-in's `params` in a row (its format is a plug-in's: tests/plugin_export.rs).
|
||||
check_params(id, &json!({"formats": [{"format": "png", "params": {}}]})).unwrap();
|
||||
// Keys the preset leaves out are still Export for Screens params.
|
||||
check_params(save, &json!({"name": "a", "settings": {"zip": true, "formats": [{"format": "jpg", "quality": 80}]}})).unwrap();
|
||||
// The encoders read Include Artboard Fill and Appearance of Black from a row or a format's
|
||||
// settings.
|
||||
for given in [
|
||||
json!({"formats": [{"format": "png", "includeArtboardFill": false, "richBlack": true}]}),
|
||||
json!({"settings": {"png": {"includeArtboardFill": false, "richBlack": true}}}),
|
||||
] {
|
||||
check_params(id, &given).unwrap_or_else(|e| panic!("{given}: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Export for Screens passes each key its description names, each option of a format in a row or
|
||||
/// in settings, and the built-in presets; `settings` takes the formats a row can write.
|
||||
#[test]
|
||||
fn export_for_screens_passes_every_listed_key() {
|
||||
use crate::cmd::fileio::{FORMATS, check_screen_settings, screen_export_builtin_presets};
|
||||
let id = "document.exportForScreens";
|
||||
let keys = doc_keys(find_command(id).unwrap().params).unwrap().keys;
|
||||
assert!(keys.len() > 20, "{keys:?}");
|
||||
// The keys the check takes are the ones the description names.
|
||||
let (mut named, mut taken) = (keys.clone(), crate::cmd::fileio::SCREEN_PARAM_KEYS.to_vec());
|
||||
named.sort_unstable();
|
||||
taken.sort_unstable();
|
||||
assert_eq!(named, taken);
|
||||
let refused: Vec<&str> = keys.into_iter().filter(|key| check_params(id, &json!({*key: null})).is_err()).collect();
|
||||
assert_eq!(refused, Vec::<&str>::new(), "{id} names and refuses these");
|
||||
check_params(id, &json!({"artboard": 0, "openLocation": true})).unwrap();
|
||||
let s = Session::new();
|
||||
for f in FORMATS.iter().filter(|f| f.write) {
|
||||
for o in f.options {
|
||||
let row = json!({"formats": [{"format": "png", "scale": 2, "suffix": "", "folder": "x", o.name: null}]});
|
||||
check_params(id, &row).unwrap_or_else(|e| panic!("{row}: {e}"));
|
||||
}
|
||||
let written = check_screen_settings(&s, &json!({"formats": [{"format": f.id}]})).is_ok();
|
||||
let settings = json!({"settings": {f.id: {}}});
|
||||
assert_eq!(check_params(id, &settings).is_ok(), written, "{settings}");
|
||||
}
|
||||
for preset in screen_export_builtin_presets() {
|
||||
let settings = serde_json::Value::Object(preset.settings);
|
||||
check_params(id, &settings).unwrap_or_else(|e| panic!("{}: {e}", preset.name));
|
||||
check_params("document.exportPresets.save", &json!({"name": "a", "settings": settings})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved row loses the keys Export for Screens refuses in it, inside its object options too, and
|
||||
/// keeps the others.
|
||||
#[test]
|
||||
fn a_saved_row_drops_the_keys_no_format_reads() {
|
||||
use crate::cmd::params::drop_unread_row_options;
|
||||
let serde_json::Value::Object(mut row) = json!({
|
||||
"format": "pdf", "scale": "1x", "noSuchOption": 1, "params": {"x": 1},
|
||||
"marks": {"useDocumentBleed": true, "trim": true}, "preset": "High Quality Print",
|
||||
"security": {"passwordRequired": true, "noSuchLock": 1}
|
||||
}) else {
|
||||
panic!("an object")
|
||||
};
|
||||
drop_unread_row_options(&mut row);
|
||||
assert_eq!(
|
||||
serde_json::Value::Object(row.clone()),
|
||||
json!({
|
||||
"format": "pdf", "scale": "1x", "params": {"x": 1}, "marks": {"trim": true}, "preset": "High Quality Print",
|
||||
"security": {"passwordRequired": true}
|
||||
})
|
||||
);
|
||||
check_params("document.exportForScreens", &json!({"formats": [row]})).unwrap();
|
||||
// Remembered security whose passwords were left out, with one given again.
|
||||
let security = json!({"passwordRequired": true, "openPassword": "x"});
|
||||
for id in ["document.exportForScreens", "assets.export", "assets.settings.set"] {
|
||||
check_params(id, &json!({"formats": [{"format": "pdf", "security": security}], "settings": {"pdf": {"security": security}}})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// `stroke.set` names every key it reads (its own, `item`, and `ids`/`id`/`target`), so a
|
||||
/// misnamed one is refused instead of answering `objects: 1` with nothing changed.
|
||||
#[test]
|
||||
|
||||
@@ -447,6 +447,42 @@ fn a_painted_effect_on_a_fill_and_refused_results() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A result refused for a swatch the document doesn't have stays cached: once the document has the
|
||||
/// swatch, the next check of that result clears the error.
|
||||
#[test]
|
||||
fn a_refused_swatch_s_error_clears_once_the_document_has_the_swatch() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
let mut later = solid("#00ff00");
|
||||
later["swatch"] = json!("Later");
|
||||
let output = json!({"objects": [{"path": square_at(40.0), "fills": [later]}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.laterswatch", true), &output));
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.laterswatch"}));
|
||||
drawn(&s);
|
||||
assert!(run(&mut s, "plugin.info", json!({"id": "test.engine.laterswatch"}))["lastError"].is_string());
|
||||
run(&mut s, "swatch.new", json!({"name": "Later", "color": "#00ff00", "global": true}));
|
||||
drawn(&s);
|
||||
assert_eq!(run(&mut s, "plugin.info", json!({"id": "test.engine.laterswatch"}))["lastError"], Value::Null);
|
||||
}
|
||||
|
||||
/// An effect that paints nothing has no references to check, so replacing a refused painted effect
|
||||
/// with one clears the refusal's error once it runs.
|
||||
#[test]
|
||||
fn a_replacement_that_paints_nothing_clears_a_refused_swatch_s_error() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
let mut gone = solid("#00ff00");
|
||||
gone["swatch"] = json!("Absent");
|
||||
let output = json!({"objects": [{"path": square_at(40.0), "fills": [gone]}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.replaced", true), &output));
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.replaced"}));
|
||||
drawn(&s);
|
||||
assert!(run(&mut s, "plugin.info", json!({"id": "test.engine.replaced"}))["lastError"].is_string());
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.replaced", false), &output));
|
||||
drawn(&s);
|
||||
assert_eq!(run(&mut s, "plugin.info", json!({"id": "test.engine.replaced"}))["lastError"], Value::Null);
|
||||
}
|
||||
|
||||
/// A painted effect on a group may link its pieces to the document's swatches.
|
||||
#[test]
|
||||
fn a_painted_group_effect_links_to_the_document_s_swatches() {
|
||||
|
||||
@@ -72,7 +72,7 @@ fn save_as_takes_on_path_format_and_options() {
|
||||
let d = dir("saveas");
|
||||
let mut s = session();
|
||||
let pdf = path(&d, "doc.pdf");
|
||||
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "bogus": 1}})).unwrap();
|
||||
s.execute("file.saveAs", &json!({"path": pdf, "options": {"range": "1", "preview": true}})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((st.format, st.title()), ("pdf", "doc.pdf".to_string()));
|
||||
assert_eq!(Value::Object(st.save_options.clone()), json!({"range": "1"}), "only the format's own options are kept");
|
||||
|
||||
@@ -3399,6 +3399,23 @@ fn the_shown_stories_fixture_opens_with_its_layers() {
|
||||
assert!(texts.iter().all(|t| t.contains('\u{2028}') && t.contains('\n')), "{texts:?}");
|
||||
}
|
||||
|
||||
/// The fuzzed editing data with its layer, pattern and global colour named in code page 949 opens
|
||||
/// with those names, read in that encoding, from its layers.
|
||||
#[test]
|
||||
fn the_editing_fixture_reads_names_in_a_legacy_encoding() {
|
||||
// 레이어 1, then 빨강.
|
||||
let text = editing_text()
|
||||
.replace("(One) Ln", r"(\267\271\300\314\276\356 1) Ln")
|
||||
.replace("(Dots)", r"(\273\241\260\255)")
|
||||
.replace("(Black global)", r"(\273\241\260\255 global)");
|
||||
let bytes = editing_eps(&text);
|
||||
let r = vectorcraft_eps::import_names(&bytes, true, true, vectorcraft_eps::NameEncoding::Korean).unwrap();
|
||||
let names: Vec<_> = r.document.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
|
||||
assert_eq!(names, ["레이어 1", "Two"], "{:?}", r.warnings);
|
||||
assert!(r.document.pattern("빨강").is_some(), "{:?}", r.warnings);
|
||||
assert!(r.document.swatch("빨강 global").is_some_and(|s| s.global), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
@@ -3475,6 +3492,26 @@ proptest! {
|
||||
survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// Names of any bytes (a layer's, a pattern's, a global colour's) read in any legacy name
|
||||
/// encoding: each byte written as an octal escape, so every code page sees bytes it doesn't
|
||||
/// define, cut sequences and UTF-8 alike.
|
||||
#[test]
|
||||
fn eps_editing_data_names_in_any_encoding_never_panic(
|
||||
name in prop::collection::vec(any::<u8>(), 0..24),
|
||||
encoding in prop::sample::select(vectorcraft_eps::NameEncoding::LABELS.to_vec()),
|
||||
) {
|
||||
let escaped: String = name.iter().map(|b| format!("\\{b:03o}")).collect();
|
||||
let text = editing_text()
|
||||
.replace("(One) Ln", &format!("({escaped}) Ln"))
|
||||
.replace("(Dots)", &format!("({escaped})"))
|
||||
.replace("(Black global)", &format!("({escaped} global)"));
|
||||
let bytes = editing_eps(&text);
|
||||
let encoding = vectorcraft_eps::NameEncoding::from_label(encoding).unwrap();
|
||||
survive("EPS editing data names in a legacy encoding", || {
|
||||
vectorcraft_eps::import_names(&bytes, true, false, encoding).ok().map(|r| r.document)
|
||||
})?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose envelopes are damaged or hostile: any kind, warp style, options word,
|
||||
/// Fidelity and grid size, corners out of range or repeated, the contents block cut short.
|
||||
#[test]
|
||||
|
||||
@@ -125,6 +125,27 @@ fn an_export_step_or_preset_with_a_key_no_longer_read_still_runs() {
|
||||
s.execute("document.export", &json!({"format": "pdf", "preset": "Old"})).unwrap();
|
||||
}
|
||||
|
||||
/// So does a save, or an Export for Screens, with a key that is no longer read, and an Export for
|
||||
/// Screens preset file that holds one in a row still loads and exports. `Session::execute`
|
||||
/// refuses the same options.
|
||||
#[test]
|
||||
fn a_save_or_screens_step_with_a_key_no_longer_read_still_runs() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let save = json!({"format": "pdf", "noSuchOption": true, "options": {"retired": 1, "marks": {"useDocumentBleed": true}}});
|
||||
assert!(check_params("file.saveCopy", &save).is_err());
|
||||
assert!(s.execute("file.saveCopy", &save).is_err());
|
||||
s.replay("file.saveCopy", &save).unwrap();
|
||||
let screens = json!({"noSuchOption": true, "formats": [{"format": "png", "retired": 1}], "settings": {"pdf": {"marks": {"retired": true}}}});
|
||||
assert!(check_params("document.exportForScreens", &screens).is_err());
|
||||
assert!(s.execute("document.exportForScreens", &screens).is_err());
|
||||
s.replay("document.exportForScreens", &screens).unwrap();
|
||||
let file = json!({"format": "vcexportpresets", "presets": [{"name": "Old", "settings": {"formats": [{"format": "png", "noLongerRead": 1}]}}]});
|
||||
s.execute("document.exportPresets.import", &json!({"data": file.to_string()})).unwrap();
|
||||
check_params("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
|
||||
s.execute("document.exportForScreens", &json!({"exportPreset": "Old"})).unwrap();
|
||||
}
|
||||
|
||||
/// So do an effect's own parameters (#1592): a journal or a file with a key the effect doesn't
|
||||
/// take, or a value of another type, replays and opens; the effect reads what it knows, clamped.
|
||||
#[test]
|
||||
|
||||
@@ -157,6 +157,22 @@ fn the_exporter_is_given_its_parameters_and_the_file_it_writes() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Export for Screens writes an exporter's format from a row, with the row's `params`, and takes
|
||||
/// settings for that format.
|
||||
#[test]
|
||||
fn export_for_screens_takes_an_exporter_s_format_and_params() {
|
||||
let mut s = session();
|
||||
let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#;
|
||||
install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.screens", r#"["tss"]"#, params), Echo::Params));
|
||||
let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "plugin.test.screens", "params": {"density": 7}}]})).unwrap();
|
||||
let file = &r["files"][0];
|
||||
assert!(file["name"].as_str().unwrap().ends_with(".tss"), "{r}");
|
||||
let got: Value = serde_json::from_slice(&vectorcraft_format::base64_decode(file["dataBase64"].as_str().unwrap()).unwrap()).unwrap();
|
||||
assert_eq!(got["density"], json!(7.0));
|
||||
let settings = json!({"formats": [{"format": "plugin.test.screens"}], "settings": {"plugin.test.screens": {"params": {"density": 2}}}});
|
||||
s.execute("document.exportForScreens", &settings).unwrap();
|
||||
}
|
||||
|
||||
/// Only the selected objects, with `selectedOnly`, as for the built-in formats.
|
||||
#[test]
|
||||
fn selected_only_hands_over_the_selection() {
|
||||
|
||||
@@ -37,6 +37,8 @@ hayro-ccitt = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
# Substring search for PostScript `search`.
|
||||
memchr = { workspace = true }
|
||||
# Names in legacy .ai files that aren't UTF-8, read in the encoding the user picks.
|
||||
encoding_rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
vectorcraft-testkit = { workspace = true }
|
||||
|
||||
@@ -462,7 +462,7 @@ fn an_artboard_larger_than_the_canvas_is_written_on_a_large_canvas() {
|
||||
let bounds = read.layers.iter().filter_map(|l| l.visual_bounds()).reduce(|a, b| a.union(b)).unwrap();
|
||||
assert!((bounds.x0 - 1000.0).abs() < 1e-6 && (bounds.y1 - 2000.0).abs() < 1e-6 && (bounds.width() - 2000.0).abs() < 1e-6, "{bounds:?}");
|
||||
// The same without a readable PDF part, on its first page's size.
|
||||
let (alone, notes) = crate::ai_alone_on_page(&p.data, (20_000.0, 20_000.0), 1.389).unwrap();
|
||||
let (alone, notes) = crate::ai_alone_on_page(&p.data, (20_000.0, 20_000.0), 1.389, crate::NameEncoding::default()).unwrap();
|
||||
assert_eq!(alone.artboards.iter().map(|a| a.rect).collect::<Vec<_>>(), [Rect::new(0.0, 0.0, 20_000.0, 20_000.0)], "{notes:?}");
|
||||
// And from the editing data alone, as a file saved without its PDF part opens.
|
||||
let alone = read_back(&doc);
|
||||
|
||||
@@ -73,6 +73,7 @@ use graphics::{GState, Out};
|
||||
use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
pub use ai::NameEncoding;
|
||||
#[cfg(test)]
|
||||
pub(crate) use ai::objects_read;
|
||||
pub(crate) use ai::{
|
||||
@@ -80,7 +81,7 @@ pub(crate) use ai::{
|
||||
symbol_art_member, warp_number,
|
||||
};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from};
|
||||
pub(crate) use native::{MAX_TEXT, scale_document};
|
||||
pub use text::family_style;
|
||||
|
||||
@@ -251,6 +252,13 @@ pub fn import_with(bytes: &[u8], editing_data: bool) -> Result<Imported, String>
|
||||
/// [`import_with`]; `trust`: an Illustrator EPS's layers are used even where they draw differently
|
||||
/// from its page, with a note (`editingData: "always"`, as for a `.ai`).
|
||||
pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<Imported, String> {
|
||||
import_names(bytes, editing_data, trust, NameEncoding::default())
|
||||
}
|
||||
|
||||
/// [`import_trusting`], with the names in the editing copy that aren't UTF-8 (layers, objects,
|
||||
/// artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables
|
||||
/// and data sets) read in `names`.
|
||||
pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
|
||||
let ps = program(bytes)?;
|
||||
let dsc = Dsc::read(ps);
|
||||
// A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no
|
||||
@@ -258,7 +266,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
|
||||
// printed).
|
||||
if editing_data
|
||||
&& dsc.own_legacy
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator)
|
||||
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, names)
|
||||
{
|
||||
return Ok(Imported { document, warnings, preview: false });
|
||||
}
|
||||
@@ -278,7 +286,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
|
||||
}
|
||||
let drew = !out.drawn.is_empty();
|
||||
let why = match (&result, drew) {
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator)),
|
||||
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, names)),
|
||||
(Ok(()), true) => return Ok(finish(out)),
|
||||
(Ok(()), false) => "it draws nothing VectorCraft's PostScript reader can show".to_string(),
|
||||
(Err(e), _) => reason(e, fault.as_ref()),
|
||||
@@ -286,7 +294,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
|
||||
// A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the
|
||||
// prolog defining its operators without including it): its layers, which the program has, or
|
||||
// its art on one layer when it has none (Illustrator 3).
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator)).flatten() {
|
||||
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, names)).flatten() {
|
||||
Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }),
|
||||
Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"),
|
||||
None => why,
|
||||
|
||||
@@ -39,14 +39,17 @@ pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN;
|
||||
pub(crate) use envelope::MAX_CHECKED;
|
||||
pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType};
|
||||
pub(crate) use frames::text_frames;
|
||||
pub use obj::{NameEncoding, Names};
|
||||
pub(crate) use paint::MAX_STOPS as MAX_GRADIENT_STOPS;
|
||||
#[cfg(test)]
|
||||
pub(crate) use read::objects_read;
|
||||
#[cfg(test)]
|
||||
pub use read::read;
|
||||
#[cfg(test)]
|
||||
pub(crate) use read::with_image_limits;
|
||||
pub use read::{
|
||||
BindingRef, Brushes, PaletteSwatches, PlacedPage, Structure, Symbols, active_artboard, artboards, bindings_note, brushes, graphic_styles,
|
||||
palette, read, read_beside, symbols, variables,
|
||||
palette, read_beside, read_in, symbols, variables,
|
||||
};
|
||||
pub(crate) use read::{
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with,
|
||||
|
||||
@@ -57,7 +57,7 @@ use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{AppearanceItem, Node, NodeKind};
|
||||
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj;
|
||||
use super::obj::Names;
|
||||
|
||||
/// The brush manager's order: the plug-in name of its object.
|
||||
pub(super) const MANAGER: &str = "Adobe Brush Manager Order"; // brand-ok: the plug-in name the files use
|
||||
@@ -157,16 +157,17 @@ pub(super) enum BrushError {
|
||||
}
|
||||
|
||||
/// The strings of a plug-in object's section (`section`: what follows `%AI8_BeginPluginObject`):
|
||||
/// its plug-in name, its name and its parameters, joined. The name is `None` with one string, and
|
||||
/// the whole is `None` with none.
|
||||
pub(super) fn plugin_strings(section: &[u8]) -> Option<(String, Option<String>, String)> {
|
||||
/// its plug-in name, the bytes of its name and its parameters, joined, read by `names` without
|
||||
/// notes. The name is `None` with one string, and the whole is `None` with none. A brush's name has
|
||||
/// its note when the brush is read, and the names of its art when their art is read.
|
||||
pub(super) fn plugin_strings(section: &[u8], names: &Names) -> Option<(String, Option<Vec<u8>>, String)> {
|
||||
let mut lex = Lexer::new(section);
|
||||
let mut strings = vec![];
|
||||
let mut params = vec![];
|
||||
while let Some(t) = lex.next_token() {
|
||||
if let Tok::Str(s) | Tok::Hex(s) = t.tok {
|
||||
if strings.len() < 2 {
|
||||
strings.push(obj::text(&s));
|
||||
strings.push(s);
|
||||
} else {
|
||||
// Joined before they are text: a string may end inside a character.
|
||||
let room = MAX_PARAMS.saturating_sub(params.len());
|
||||
@@ -175,8 +176,8 @@ pub(super) fn plugin_strings(section: &[u8]) -> Option<(String, Option<String>,
|
||||
}
|
||||
}
|
||||
let mut strings = strings.into_iter();
|
||||
let kind = strings.next()?;
|
||||
Some((kind, strings.next(), obj::text(¶ms)))
|
||||
let kind = names.quiet(&strings.next()?);
|
||||
Some((kind, strings.next(), names.quiet(¶ms)))
|
||||
}
|
||||
|
||||
/// The brush manager's order (`params`: its parameters): the kind and name of each brush. Pairs
|
||||
|
||||
@@ -47,6 +47,237 @@ pub(super) fn text(bytes: &[u8]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The encoding names in the editing data that aren't UTF-8 are read in (layers, objects,
|
||||
/// artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables
|
||||
/// and data sets, and data sets' text values). Legacy files store them in the code page of the
|
||||
/// system that wrote them and don't say which.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum NameEncoding {
|
||||
/// Windows-1252, Latin-1 for its gaps.
|
||||
#[default]
|
||||
Windows1252,
|
||||
/// Mac OS Roman.
|
||||
Macintosh,
|
||||
/// Korean: Windows code page 949 (EUC-KR and Unified Hangul Code).
|
||||
Korean,
|
||||
/// Japanese: Shift_JIS (Windows code page 932).
|
||||
ShiftJis,
|
||||
/// Simplified Chinese: GBK (Windows code page 936).
|
||||
Gbk,
|
||||
/// Traditional Chinese: Big5 (Windows code page 950).
|
||||
Big5,
|
||||
}
|
||||
|
||||
impl NameEncoding {
|
||||
/// The labels [`NameEncoding::from_label`] reads, one for each encoding.
|
||||
pub const LABELS: [&str; 6] = ["windows-1252", "macintosh", "windows-949", "shift_jis", "gbk", "big5"];
|
||||
|
||||
/// The encoding with label `label` (any case): one of [`Self::LABELS`], or `euc-kr` for
|
||||
/// [`NameEncoding::Korean`].
|
||||
pub fn from_label(label: &str) -> Option<Self> {
|
||||
let label = label.trim().to_ascii_lowercase();
|
||||
Some(match label.as_str() {
|
||||
"windows-1252" => Self::Windows1252,
|
||||
"macintosh" => Self::Macintosh,
|
||||
"windows-949" | "euc-kr" => Self::Korean,
|
||||
"shift_jis" => Self::ShiftJis,
|
||||
"gbk" => Self::Gbk,
|
||||
"big5" => Self::Big5,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The encoding's label, as [`Self::LABELS`] gives it.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Windows1252 => "windows-1252",
|
||||
Self::Macintosh => "macintosh",
|
||||
Self::Korean => "windows-949",
|
||||
Self::ShiftJis => "shift_jis",
|
||||
Self::Gbk => "gbk",
|
||||
Self::Big5 => "big5",
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` as text: UTF-8 as it is, else read in this encoding, with each sequence it doesn't
|
||||
/// define as U+FFFD (Windows-1252 defines every byte).
|
||||
fn decode(self, bytes: &[u8]) -> Decoded {
|
||||
if let Ok(s) = std::str::from_utf8(bytes) {
|
||||
return Decoded { text: s.to_string(), fallback: false, replaced: false };
|
||||
}
|
||||
let other = match self {
|
||||
Self::Windows1252 => return Decoded { text: bytes.iter().map(|b| cp1252(*b)).collect(), fallback: true, replaced: false },
|
||||
Self::Macintosh => encoding_rs::MACINTOSH,
|
||||
Self::Korean => encoding_rs::EUC_KR,
|
||||
Self::ShiftJis => encoding_rs::SHIFT_JIS,
|
||||
Self::Gbk => encoding_rs::GBK,
|
||||
Self::Big5 => encoding_rs::BIG5,
|
||||
};
|
||||
let (text, replaced) = other.decode_without_bom_handling(bytes);
|
||||
Decoded { text: text.into_owned(), fallback: true, replaced }
|
||||
}
|
||||
|
||||
/// [`NameEncoding::decode`] as the text, whether the bytes weren't UTF-8, and whether some
|
||||
/// weren't valid in the encoding either.
|
||||
pub(super) fn decoded(self, bytes: &[u8]) -> (String, bool, bool) {
|
||||
let d = self.decode(bytes);
|
||||
(d.text, d.fallback, d.replaced)
|
||||
}
|
||||
}
|
||||
|
||||
/// A name's hash, for counting the names past the notes.
|
||||
fn hash(name: &str) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
name.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Bytes read as text by [`NameEncoding::decode`].
|
||||
struct Decoded {
|
||||
text: String,
|
||||
/// The bytes weren't UTF-8.
|
||||
fallback: bool,
|
||||
/// Some weren't valid in the encoding either, and became U+FFFD.
|
||||
replaced: bool,
|
||||
}
|
||||
|
||||
/// Most names that weren't UTF-8 each named in a note; the rest are counted in one.
|
||||
const MAX_NAME_NOTES: usize = 3;
|
||||
/// Most other names counted (the count stops there).
|
||||
const MAX_NAMES_COUNTED: usize = 10_000;
|
||||
|
||||
/// The names read in a [`NameEncoding`], and the notes on those that weren't UTF-8: one for each
|
||||
/// name (the first [`MAX_NAME_NOTES`]), then one counting the others (up to
|
||||
/// [`MAX_NAMES_COUNTED`]).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Names {
|
||||
pub encoding: NameEncoding,
|
||||
noted: Vec<(String, bool)>,
|
||||
/// Hashes of the other names.
|
||||
more: std::collections::HashSet<u64>,
|
||||
}
|
||||
|
||||
impl Names {
|
||||
pub fn new(encoding: NameEncoding) -> Self {
|
||||
Self { encoding, ..Self::default() }
|
||||
}
|
||||
|
||||
/// `bytes` read in the encoding, without a note (a list or a parameter string that repeats
|
||||
/// names noted where they are defined).
|
||||
pub fn quiet(&self, bytes: &[u8]) -> String {
|
||||
self.encoding.decode(bytes).text
|
||||
}
|
||||
|
||||
/// A string's or name's text as a name: a string that isn't UTF-8 read in the encoding,
|
||||
/// with a note.
|
||||
pub(super) fn text(&mut self, v: &V) -> Option<String> {
|
||||
match v {
|
||||
V::Str(s) => Some(self.bytes(s)),
|
||||
V::Name(n) => Some(n.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` read as a name (see [`Names::text`]).
|
||||
pub fn bytes(&mut self, bytes: &[u8]) -> String {
|
||||
let d = self.encoding.decode(bytes);
|
||||
if d.fallback {
|
||||
self.note(&d.text, d.replaced);
|
||||
}
|
||||
d.text
|
||||
}
|
||||
|
||||
/// The first text value of `o`'s entry `key`, as a name.
|
||||
pub(super) fn entry(&mut self, o: &Obj, key: &str) -> Option<String> {
|
||||
let v = o.get(key)?.iter().find(|v| matches!(v, V::Str(_)))?;
|
||||
self.text(v)
|
||||
}
|
||||
|
||||
/// The name the XML id in `id` (an `AI10_ArtUID`) gives its object ([`xml_name`]): a string that
|
||||
/// isn't UTF-8 is read in the encoding, with a note on the name.
|
||||
pub(super) fn xml_id_name(&mut self, id: &Obj) -> Option<String> {
|
||||
match id.content.iter().find(|v| matches!(v, V::Str(_) | V::Name(_)))? {
|
||||
V::Str(bytes) => {
|
||||
let d = self.encoding.decode(bytes);
|
||||
let name = xml_name(&d.text);
|
||||
if d.fallback {
|
||||
self.note(&name, d.replaced);
|
||||
}
|
||||
Some(name)
|
||||
}
|
||||
v => v.text().map(|t| xml_name(&t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A note on `name`, which wasn't UTF-8 (`replaced`: parts of it weren't valid in the encoding
|
||||
/// either).
|
||||
pub(super) fn note(&mut self, name: &str, replaced: bool) {
|
||||
if self.noted.iter().any(|(n, _)| n == name) {
|
||||
return;
|
||||
}
|
||||
if self.noted.len() < MAX_NAME_NOTES {
|
||||
self.noted.push((name.to_string(), replaced));
|
||||
} else {
|
||||
self.count(hash(name));
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&mut self, h: u64) {
|
||||
if self.more.len() < MAX_NAMES_COUNTED {
|
||||
self.more.insert(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the notes of `other`, a reader's for part of the same data.
|
||||
pub fn absorb(&mut self, other: Names) {
|
||||
for (name, replaced) in other.noted {
|
||||
self.note(&name, replaced);
|
||||
}
|
||||
for h in other.more {
|
||||
self.count(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the notes of `other` on the names in `used` (names read but then not used, such as the
|
||||
/// layers of data whose layers aren't used, left out).
|
||||
pub fn absorb_among(&mut self, other: Names, used: &[&str]) {
|
||||
for (name, replaced) in other.noted {
|
||||
if used.contains(&name.as_str()) {
|
||||
self.note(&name, replaced);
|
||||
}
|
||||
}
|
||||
for n in used {
|
||||
let h = hash(n);
|
||||
if other.more.contains(&h) {
|
||||
self.count(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The notes on the names that weren't UTF-8.
|
||||
pub fn notes(&self) -> Vec<String> {
|
||||
let label = self.encoding.label();
|
||||
let mut out: Vec<String> = self
|
||||
.noted
|
||||
.iter()
|
||||
.map(|(name, replaced)| {
|
||||
if *replaced {
|
||||
format!(
|
||||
"the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding"
|
||||
)
|
||||
} else {
|
||||
format!("the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !self.more.is_empty() {
|
||||
out.push(format!("more names that aren't UTF-8 were read as {label} ({count})", count = self.more.len()));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn cp1252(b: u8) -> char {
|
||||
const HIGH: [char; 32] = [
|
||||
'€', '\u{81}', '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', '\u{8d}', 'Ž', '\u{8f}', '\u{90}', '‘', '’', '“', '”', '•', '–', '—',
|
||||
@@ -172,4 +403,107 @@ mod tests {
|
||||
assert_eq!(text(b"caf\xe9 \x80"), "café €");
|
||||
assert_eq!(text("ü".as_bytes()), "ü");
|
||||
}
|
||||
|
||||
/// The default reads every byte as Windows-1252 does, the five bytes it leaves undefined as the
|
||||
/// C1 controls of Latin-1, as `text` always has.
|
||||
#[test]
|
||||
fn names_default_to_windows_1252_with_latin_1_in_its_gaps() {
|
||||
let all: Vec<u8> = (0x80..=0xff).collect();
|
||||
let mut names = Names::default();
|
||||
assert_eq!(names.bytes(&all), text(&all));
|
||||
let gaps: String = [0x81u8, 0x8d, 0x8f, 0x90, 0x9d].iter().map(|b| names.bytes(&[*b])).collect();
|
||||
assert_eq!(gaps, "\u{81}\u{8d}\u{8f}\u{90}\u{9d}");
|
||||
assert_eq!(names.bytes(b"\x80\x9f"), "€Ÿ");
|
||||
}
|
||||
|
||||
/// A name that is UTF-8 is read as it is in every encoding, without a note.
|
||||
#[test]
|
||||
fn utf8_names_need_no_encoding() {
|
||||
for label in NameEncoding::LABELS {
|
||||
let mut names = Names::new(NameEncoding::from_label(label).unwrap());
|
||||
assert_eq!(names.bytes("레이어 1".as_bytes()), "레이어 1");
|
||||
assert_eq!(names.text(&V::Str(b"Layer 1".to_vec())).as_deref(), Some("Layer 1"));
|
||||
assert!(names.notes().is_empty(), "{label}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Each encoding reads the bytes of a name written in it.
|
||||
#[test]
|
||||
fn names_in_each_encoding() {
|
||||
for (label, bytes, want) in [
|
||||
("windows-1252", &b"Caf\xe9"[..], "Café"),
|
||||
("macintosh", b"Caf\x8e", "Café"),
|
||||
("windows-949", b"\xb7\xb9\xc0\xcc\xbe\xee 1", "레이어 1"),
|
||||
("euc-kr", b"\xb7\xb9\xc0\xcc\xbe\xee 1", "레이어 1"),
|
||||
("shift_jis", b"\x83\x8c\x83\x43\x83\x84\x81\x5b 1", "レイヤー 1"),
|
||||
("gbk", b"\xcd\xbc\xb2\xe3 1", "图层 1"),
|
||||
("big5", b"\xb9\xcf\xbch 1", "圖層 1"),
|
||||
] {
|
||||
let mut names = Names::new(NameEncoding::from_label(label).unwrap());
|
||||
assert_eq!(names.bytes(bytes), want, "{label}");
|
||||
assert_eq!(
|
||||
names.notes(),
|
||||
vec![format!(
|
||||
"the name `{want}` isn't UTF-8 and was read as {}: if it looks wrong, open the file with another legacyNameEncoding",
|
||||
names.encoding.label()
|
||||
)]
|
||||
);
|
||||
}
|
||||
assert_eq!(NameEncoding::from_label(" Windows-949 "), Some(NameEncoding::Korean));
|
||||
assert_eq!(NameEncoding::from_label("utf-8"), None);
|
||||
for label in NameEncoding::LABELS {
|
||||
assert_eq!(NameEncoding::from_label(label).map(NameEncoding::label), Some(label));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Korean bytes read by default come in as Windows-1252, with a note naming what they
|
||||
/// became.
|
||||
#[test]
|
||||
fn korean_bytes_by_default_come_in_as_windows_1252_with_a_note() {
|
||||
let mut names = Names::default();
|
||||
let name = names.bytes(b"\xb7\xb9\xc0\xcc\xbe\xee 1");
|
||||
assert_eq!(name, "·¹À̾î 1");
|
||||
assert_eq!(
|
||||
names.notes(),
|
||||
vec!["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"]
|
||||
);
|
||||
}
|
||||
|
||||
/// Bytes the encoding doesn't define come in as U+FFFD, and the note says so.
|
||||
#[test]
|
||||
fn malformed_names_keep_a_replacement_character() {
|
||||
let mut names = Names::new(NameEncoding::Korean);
|
||||
let name = names.bytes(b"\xb7\xb9\xc0 1");
|
||||
assert!(name.contains('\u{fffd}'), "{name:?}");
|
||||
assert!(name.starts_with('레'), "{name:?}");
|
||||
let notes = names.notes();
|
||||
assert_eq!(notes.len(), 1);
|
||||
assert!(notes[0].contains("isn't UTF-8, and parts of it aren't valid windows-949 either:"), "{notes:?}");
|
||||
}
|
||||
|
||||
/// The notes name the first names once each, and count the others.
|
||||
#[test]
|
||||
fn name_notes_are_capped() {
|
||||
let mut names = Names::new(NameEncoding::Macintosh);
|
||||
for i in 0..10u8 {
|
||||
let bytes = [b'a' + i, 0x8e];
|
||||
names.bytes(&bytes);
|
||||
names.bytes(&bytes);
|
||||
}
|
||||
let notes = names.notes();
|
||||
assert_eq!(notes.len(), MAX_NAME_NOTES + 1);
|
||||
assert!(notes[0].contains("`aé`"), "{notes:?}");
|
||||
assert_eq!(notes.last().map(String::as_str), Some("more names that aren't UTF-8 were read as macintosh (7)"));
|
||||
let mut all = Names::new(NameEncoding::Macintosh);
|
||||
all.absorb(names);
|
||||
assert_eq!(all.notes(), notes);
|
||||
// The count stops at its limit, whatever the file holds.
|
||||
let mut many = Names::new(NameEncoding::Macintosh);
|
||||
for i in 0..MAX_NAME_NOTES + MAX_NAMES_COUNTED + 50 {
|
||||
let mut bytes = i.to_string().into_bytes();
|
||||
bytes.push(0x8e);
|
||||
many.bytes(&bytes);
|
||||
}
|
||||
assert_eq!(many.notes().last().map(String::as_str), Some("more names that aren't UTF-8 were read as macintosh (10000)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use vectorcraft_color::gradient::{Gradient, GradientGeom, GradientKind, Gradient
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::V;
|
||||
use super::obj::{NameEncoding, Names, V};
|
||||
|
||||
/// Largest number of stops read in one gradient.
|
||||
pub(crate) const MAX_STOPS: usize = 256;
|
||||
@@ -49,8 +49,8 @@ pub(super) struct Named {
|
||||
}
|
||||
|
||||
/// The paint of a colour operator's operands (`op`: its name, lower case or not), with the named
|
||||
/// colour it uses. `None` when the operands don't fit.
|
||||
pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option<Named>)> {
|
||||
/// colour it uses, its name read by `names`. `None` when the operands don't fit.
|
||||
pub(super) fn color_op_in(op: &str, vals: &[V], names: &mut Names) -> Option<(Paint, Option<Named>)> {
|
||||
let nums: Vec<f32> = vals.iter().filter_map(V::num).map(|v| v as f32).collect();
|
||||
let unit = |v: f32| v.clamp(0.0, 1.0);
|
||||
let last = |n: usize| nums.get(nums.len().checked_sub(n)?..);
|
||||
@@ -64,9 +64,9 @@ pub(super) fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option<Named>)> {
|
||||
let [r, g, b] = last(3)? else { return None };
|
||||
Some((Paint::solid(Color::rgb(unit(*r), unit(*g), unit(*b))), None))
|
||||
}
|
||||
"x" => named(vals, Some(false)),
|
||||
"xx" => named(vals, None),
|
||||
"xk" => named(vals, None).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))),
|
||||
"x" => named(vals, Some(false), names),
|
||||
"xx" => named(vals, None, names),
|
||||
"xk" => named(vals, None, names).map(|(p, n)| (p, n.map(|n| Named { spot: false, ..n }))),
|
||||
"xs" | "xz" => registration(op, vals),
|
||||
_ => None,
|
||||
}
|
||||
@@ -78,14 +78,14 @@ fn is_registration(name: &str) -> bool {
|
||||
|
||||
/// `c m y k (name) tint x` or `c m y k [r g b | L a b] (name) tint type Xx` (type 1: RGB, 2: Lab);
|
||||
/// `rgb` forces RGB or CMYK.
|
||||
fn named(vals: &[V], rgb: Option<bool>) -> Option<(Paint, Option<Named>)> {
|
||||
fn named(vals: &[V], rgb: Option<bool>, names: &mut Names) -> Option<(Paint, Option<Named>)> {
|
||||
let at = vals.iter().rposition(|v| matches!(v, V::Str(_) | V::Name(_)))?;
|
||||
let name = vals.get(at)?.text()?;
|
||||
let name = vals.get(at)?;
|
||||
let before: Vec<f32> = vals.get(..at)?.iter().filter_map(V::num).map(|v| v as f32).collect();
|
||||
let after: Vec<f32> = vals.get(at + 1..)?.iter().filter_map(V::num).map(|v| v as f32).collect();
|
||||
let unit = |v: f32| v.clamp(0.0, 1.0);
|
||||
let tint = 1.0 - unit(*after.first()?);
|
||||
if is_registration(&name) {
|
||||
if is_registration(&name.text()?) {
|
||||
let full = Color::cmyk(1.0, 1.0, 1.0, 1.0);
|
||||
let paint = Paint::Solid { color: full.tinted(tint), swatch: Some(vectorcraft_color::swatch::REGISTRATION.into()), tint };
|
||||
return Some((paint, None));
|
||||
@@ -105,6 +105,7 @@ fn named(vals: &[V], rgb: Option<bool>) -> Option<(Paint, Option<Named>)> {
|
||||
let [c, m, y, k] = before.get(from..from + 4)? else { return None };
|
||||
Color::cmyk(unit(*c), unit(*m), unit(*y), unit(*k))
|
||||
};
|
||||
let name = names.text(name)?;
|
||||
let paint = Paint::Solid { color: color.tinted(tint), swatch: Some(name.clone()), tint };
|
||||
Some((paint, Some(Named { name, color, tint, spot: true })))
|
||||
}
|
||||
@@ -130,7 +131,7 @@ fn registration(op: &str, vals: &[V]) -> Option<(Paint, Option<Named>)> {
|
||||
}
|
||||
|
||||
/// The colour of a gradient stop's colour values and style.
|
||||
fn stop_color(style: f64, comps: &[V]) -> Option<(Color, Option<Named>)> {
|
||||
fn stop_color(style: f64, comps: &[V], names: &mut Names) -> Option<(Color, Option<Named>)> {
|
||||
let nums: Vec<f32> = comps.iter().filter_map(V::num).map(|v| v as f32).collect();
|
||||
let unit = |v: f32| v.clamp(0.0, 1.0);
|
||||
let tail = |n: usize| nums.get(nums.len().checked_sub(n)?..);
|
||||
@@ -145,7 +146,7 @@ fn stop_color(style: f64, comps: &[V]) -> Option<(Color, Option<Named>)> {
|
||||
(Color::rgb(unit(*r), unit(*g), unit(*b)), None)
|
||||
}
|
||||
_ => {
|
||||
let (paint, named) = named(comps, Some(style as i64 == 4))?;
|
||||
let (paint, named) = named(comps, Some(style as i64 == 4), names)?;
|
||||
(paint.color()?, named)
|
||||
}
|
||||
})
|
||||
@@ -159,16 +160,16 @@ pub(super) struct GradientDef {
|
||||
}
|
||||
|
||||
impl GradientDef {
|
||||
/// `(name) type stops Bd`.
|
||||
pub fn begin(vals: &[V]) -> Option<Self> {
|
||||
let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None })?;
|
||||
/// `(name) type stops Bd`, the name read by `names`.
|
||||
pub fn begin(vals: &[V], names: &mut Names) -> Option<Self> {
|
||||
let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v))?;
|
||||
let nums: Vec<f64> = vals.iter().filter_map(V::num).collect();
|
||||
let kind = if nums.first().copied() == Some(1.0) { GradientKind::Radial } else { GradientKind::Linear };
|
||||
Some(Self { name, gradient: Gradient::new(kind, vec![]) })
|
||||
}
|
||||
|
||||
/// A stop (`Bs`).
|
||||
pub fn stop(&mut self, vals: &[V], spots: &mut Vec<Named>) {
|
||||
/// A stop (`Bs`), the name of its colour read by `names`.
|
||||
pub fn stop(&mut self, vals: &[V], spots: &mut Vec<Named>, names: &mut Names) {
|
||||
if self.gradient.stops.len() >= MAX_STOPS {
|
||||
return;
|
||||
}
|
||||
@@ -179,7 +180,7 @@ impl GradientDef {
|
||||
let newer = n >= 5 && n.checked_sub(3).and_then(num) == Some(6.0);
|
||||
let (style_at, opacity) = if newer { (n.checked_sub(5), n.checked_sub(4).and_then(num).unwrap_or(1.0)) } else { (n.checked_sub(3), 1.0) };
|
||||
let (Some(style_at), Some(style)) = (style_at, style_at.and_then(num)) else { return };
|
||||
let Some((color, named)) = stop_color(style, vals.get(..style_at).unwrap_or_default()) else { return };
|
||||
let Some((color, named)) = stop_color(style, vals.get(..style_at).unwrap_or_default(), names) else { return };
|
||||
let mut stop = GradientStop::new((ramp / 100.0).clamp(0.0, 1.0) as f32, color);
|
||||
stop.opacity = opacity.clamp(0.0, 1.0) as f32;
|
||||
stop.midpoint = (mid / 100.0).clamp(0.13, 0.87) as f32;
|
||||
@@ -225,9 +226,16 @@ pub(super) struct ColorNames {
|
||||
vals: Vec<V>,
|
||||
def: Option<GradientDef>,
|
||||
spots: Vec<Named>,
|
||||
/// Reads the names found (the reader reads them again, and keeps the notes on them).
|
||||
decoder: Names,
|
||||
}
|
||||
|
||||
impl ColorNames {
|
||||
/// The names found to be read in `encoding`.
|
||||
pub fn new(encoding: NameEncoding) -> Self {
|
||||
Self { decoder: Names::new(encoding), ..Self::default() }
|
||||
}
|
||||
|
||||
pub fn token(&mut self, tok: Tok<'_>) {
|
||||
self.take(tok, true);
|
||||
}
|
||||
@@ -252,7 +260,7 @@ impl ColorNames {
|
||||
};
|
||||
let vals = std::mem::take(&mut self.vals);
|
||||
match w {
|
||||
b"Bd" => self.def = GradientDef::begin(&vals),
|
||||
b"Bd" => self.def = GradientDef::begin(&vals, &mut self.decoder),
|
||||
b"BD" => {
|
||||
if let Some((name, _)) = self.def.take().and_then(GradientDef::finish) {
|
||||
self.gradients.push(name);
|
||||
@@ -260,18 +268,18 @@ impl ColorNames {
|
||||
}
|
||||
b"Bs" => {
|
||||
if let Some(d) = self.def.as_mut() {
|
||||
d.stop(&vals, &mut self.spots);
|
||||
d.stop(&vals, &mut self.spots, &mut self.decoder);
|
||||
}
|
||||
}
|
||||
b"x" | b"X" | b"Xx" | b"XX" | b"Xk" | b"XK" => {
|
||||
if let Some((_, Some(n))) = std::str::from_utf8(w).ok().and_then(|op| color_op(op, &vals)) {
|
||||
if let Some((_, Some(n))) = std::str::from_utf8(w).ok().and_then(|op| color_op_in(op, &vals, &mut self.decoder)) {
|
||||
self.spots.push(n);
|
||||
}
|
||||
}
|
||||
b"@" | b"&" if outer => {
|
||||
for v in &vals {
|
||||
if let V::Str(s) = v {
|
||||
let mut inner = ColorNames::default();
|
||||
let mut inner = ColorNames::new(self.decoder.encoding);
|
||||
let mut lex = Lexer::new(s);
|
||||
while let Some(t) = lex.next_token() {
|
||||
inner.take(t.tok, false);
|
||||
@@ -396,10 +404,10 @@ impl Instance {
|
||||
Self { stroke, length: 1.0, ..Self::default() }
|
||||
}
|
||||
|
||||
/// `flag (name) x y angle length a b c d tx ty Bg`.
|
||||
pub fn bg(&mut self, vals: &[V]) {
|
||||
/// `flag (name) x y angle length a b c d tx ty Bg`, the name read by `names`.
|
||||
pub fn bg(&mut self, vals: &[V], names: &mut Names) {
|
||||
let Some(at) = vals.iter().rposition(|v| matches!(v, V::Str(_))) else { return };
|
||||
self.name = vals.get(at).and_then(V::text);
|
||||
self.name = vals.get(at).and_then(|v| names.text(v));
|
||||
let n: Vec<f64> = vals.get(at + 1..).unwrap_or_default().iter().filter_map(V::num).collect();
|
||||
if let [x, y, angle, length, m @ ..] = n.as_slice() {
|
||||
self.origin = Point::new(*x, *y);
|
||||
@@ -520,6 +528,35 @@ mod tests {
|
||||
v.iter().map(|n| V::Num(*n)).collect()
|
||||
}
|
||||
|
||||
fn color_op(op: &str, vals: &[V]) -> Option<(Paint, Option<Named>)> {
|
||||
color_op_in(op, vals, &mut Names::default())
|
||||
}
|
||||
|
||||
/// A named colour, a gradient and its stop's colour, and the gradient an object names, read in
|
||||
/// the encoding picked, so that names written alike match.
|
||||
#[test]
|
||||
fn colour_and_gradient_names_read_in_the_encoding() {
|
||||
let mut names = Names::new(super::super::obj::NameEncoding::Korean);
|
||||
let mut v = nums(&[0.0, 0.5, 1.0, 0.0]);
|
||||
v.extend([V::Str(b"\xbb\xa1\xb0\xad".to_vec()), V::Num(0.0)]);
|
||||
let (_, named) = color_op_in("x", &v, &mut names).unwrap();
|
||||
assert_eq!(named.unwrap().name, "빨강");
|
||||
let mut d = GradientDef::begin(&[V::Str(b"\xb1\xd7\xb6\xf3".to_vec()), V::Num(0.0)], &mut names).unwrap();
|
||||
let mut stop = nums(&[0.0, 0.5, 1.0, 0.0]);
|
||||
stop.extend([V::Str(b"\xbb\xa1\xb0\xad".to_vec()), V::Num(0.0), V::Num(3.0), V::Num(50.0), V::Num(0.0)]);
|
||||
let mut spots = vec![];
|
||||
d.stop(&stop, &mut spots, &mut names);
|
||||
assert_eq!(spots.first().map(|s| s.name.as_str()), Some("빨강"));
|
||||
let (name, g) = d.finish().unwrap();
|
||||
assert_eq!(name, "그라");
|
||||
let mut i = Instance::begin(&[]);
|
||||
let mut bg = vec![V::Num(1.0), V::Str(b"\xb1\xd7\xb6\xf3".to_vec())];
|
||||
bg.extend(nums(&[0.0, 0.0, 0.0, 10.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
i.bg(&bg, &mut names);
|
||||
assert!(i.paint(&HashMap::from([(name, g)]), &ShownNames::default(), Affine::IDENTITY).is_some());
|
||||
assert_eq!(names.notes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_paints_with_the_registration_swatch_at_its_tint() {
|
||||
let mut v = nums(&[0.75, 0.68, 0.67, 0.9, 0.0, 0.0, 0.0]);
|
||||
@@ -608,10 +645,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gradient_definition_and_instance() {
|
||||
let mut d = GradientDef::begin(&[V::Str(b"G".to_vec()), V::Num(1.0), V::Num(2.0)]).unwrap();
|
||||
let names = &mut Names::default();
|
||||
let mut d = GradientDef::begin(&[V::Str(b"G".to_vec()), V::Num(1.0), V::Num(2.0)], names).unwrap();
|
||||
let mut spots = vec![];
|
||||
d.stop(&nums(&[0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.5, 6.0, 30.0, 100.0]), &mut spots);
|
||||
d.stop(&nums(&[0.81, 0.0, 1.0, 6.0, 50.0, 0.0]), &mut spots);
|
||||
d.stop(&nums(&[0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.5, 6.0, 30.0, 100.0]), &mut spots, names);
|
||||
d.stop(&nums(&[0.81, 0.0, 1.0, 6.0, 50.0, 0.0]), &mut spots, names);
|
||||
let (name, g) = d.finish().unwrap();
|
||||
assert_eq!(name, "G");
|
||||
assert_eq!(g.kind, GradientKind::Radial);
|
||||
@@ -622,7 +660,7 @@ mod tests {
|
||||
let mut i = Instance::begin(&[]);
|
||||
let mut bg = vec![V::Num(1.0), V::Str(b"G".to_vec())];
|
||||
bg.extend(nums(&[100.0, 100.0, 0.0, 50.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
i.bg(&bg);
|
||||
i.bg(&bg, names);
|
||||
i.bm(&nums(&[50.0, 0.0, 0.0, -50.0, 100.0, 100.0]));
|
||||
let shown = ShownNames::default();
|
||||
let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() };
|
||||
@@ -634,7 +672,7 @@ mod tests {
|
||||
assert!(i.paint(&HashMap::new(), &shown, Affine::IDENTITY).is_none());
|
||||
// Newer files give the matrix with `Xm`; the `Bm`s after it are bands of a cached ramp.
|
||||
let mut i = Instance::begin(&[]);
|
||||
i.bg(&bg);
|
||||
i.bg(&bg, names);
|
||||
i.xm(&nums(&[40.0, 0.0, 0.0, -40.0, 10.0, 10.0]));
|
||||
i.bm(&nums(&[5.0, 0.0, 0.0, -40.0, 30.0, 10.0]));
|
||||
let Some(Paint::Gradient(gp)) = i.paint(&defs, &shown, Affine::IDENTITY) else { panic!() };
|
||||
@@ -679,11 +717,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gradient_stops_beyond_the_cap_are_ignored() {
|
||||
let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)]).unwrap();
|
||||
let names = &mut Names::default();
|
||||
let mut d = GradientDef::begin(&[V::Str(b"H".to_vec()), V::Num(0.0)], names).unwrap();
|
||||
let mut spots = vec![];
|
||||
for i in 0..257 {
|
||||
let ramp = i as f64 / 256.0 * 100.0;
|
||||
d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut spots);
|
||||
d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut spots, names);
|
||||
}
|
||||
let (_, g) = d.finish().unwrap();
|
||||
assert_eq!(g.stops.len(), MAX_STOPS);
|
||||
@@ -694,9 +733,10 @@ mod tests {
|
||||
#[test]
|
||||
fn stops_at_one_ramp_point_keep_their_sides() {
|
||||
for (kind, listed) in [(0.0, [0.75, 0.25]), (1.0, [0.25, 0.75])] {
|
||||
let mut d = GradientDef::begin(&[V::Str(b"E".to_vec()), V::Num(kind), V::Num(2.0)]).unwrap();
|
||||
let names = &mut Names::default();
|
||||
let mut d = GradientDef::begin(&[V::Str(b"E".to_vec()), V::Num(kind), V::Num(2.0)], names).unwrap();
|
||||
for (gray, mid) in listed.into_iter().zip([40.0, 70.0]) {
|
||||
d.stop(&nums(&[gray, 0.0, 1.0, 6.0, mid, 30.0]), &mut vec![]);
|
||||
d.stop(&nums(&[gray, 0.0, 1.0, 6.0, mid, 30.0]), &mut vec![], names);
|
||||
}
|
||||
let (_, g) = d.finish().unwrap();
|
||||
let lightness: Vec<f32> = g.stops.iter().map(|s| (s.color.to_rgb()[0] * 100.0).round() / 100.0).collect();
|
||||
@@ -711,16 +751,17 @@ mod tests {
|
||||
/// art space, `Xm` for the whole ramp and a `Bm` for each pair of stops.
|
||||
#[test]
|
||||
fn a_linear_gradient_runs_its_whole_ramp_not_its_last_part() {
|
||||
let mut d = GradientDef::begin(&[V::Str(b"L".to_vec()), V::Num(0.0), V::Num(3.0)]).unwrap();
|
||||
let names = &mut Names::default();
|
||||
let mut d = GradientDef::begin(&[V::Str(b"L".to_vec()), V::Num(0.0), V::Num(3.0)], names).unwrap();
|
||||
for ramp in [100.0, 60.0, 20.0] {
|
||||
d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut vec![]);
|
||||
d.stop(&nums(&[0.2, 0.4, 0.6, 0.0, 1.0, 1.0, 6.0, 50.0, ramp]), &mut vec![], names);
|
||||
}
|
||||
let defs = HashMap::from([d.finish().unwrap()]);
|
||||
let instance = |xm: bool| {
|
||||
let mut i = Instance::begin(&[]);
|
||||
let mut bg = vec![V::Num(1.0), V::Str(b"L".to_vec())];
|
||||
bg.extend(nums(&[0.3, -0.2, -90.0, 0.5, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0]));
|
||||
i.bg(&bg);
|
||||
i.bg(&bg, &mut Names::default());
|
||||
if xm {
|
||||
i.xm(&nums(&[0.0, -200.0, -30.0, 0.0, 10.0, 300.0]));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use vectorcraft_color::Color;
|
||||
use vectorcraft_doc::swatches::color_name;
|
||||
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::V;
|
||||
use super::obj::{Names, V};
|
||||
use super::paint::{self, Named};
|
||||
|
||||
/// Largest number of swatches read from one palette.
|
||||
@@ -73,8 +73,9 @@ impl Palette {
|
||||
}
|
||||
}
|
||||
|
||||
/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end.
|
||||
pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
/// The palette of `data`: what follows `%AI5_BeginPalette`, up to its end, its names read by
|
||||
/// `names`.
|
||||
pub(super) fn palette(data: &[u8], names: &mut Names) -> Palette {
|
||||
let mut lex = Lexer::new(data);
|
||||
let mut out = Palette::default();
|
||||
let mut vals: Vec<V> = Vec::new();
|
||||
@@ -125,15 +126,15 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
cell = None;
|
||||
}
|
||||
"Bg" if in_gradient => {
|
||||
if let Some(name) = last_text(&vals) {
|
||||
if let Some(name) = last_text(&vals, names) {
|
||||
cell = Some(Entry { cell: Cell::Gradient(name.clone()), name });
|
||||
}
|
||||
}
|
||||
"BB" => in_gradient = false,
|
||||
"p" => cell = first_text(&vals).map(|name| Entry { cell: Cell::Pattern(name.clone()), name }),
|
||||
"p" => cell = first_text(&vals, names).map(|name| Entry { cell: Cell::Pattern(name.clone()), name }),
|
||||
"Xs" | "Xz" | "XS" | "XZ" => cell = None,
|
||||
"g" | "k" | "x" | "Xa" | "Xx" | "Xk" => {
|
||||
cell = paint::color_op(w, &vals).and_then(|(p, named)| match named {
|
||||
cell = paint::color_op_in(w, &vals, names).and_then(|(p, named)| match named {
|
||||
Some(Named { name, color, spot, .. }) => Some(Entry { name, cell: Cell::Color { color, global: true, spot } }),
|
||||
None => p.color().map(|color| Entry { name: color_name(color), cell: Cell::Color { color, global: false, spot: false } }),
|
||||
});
|
||||
@@ -141,7 +142,7 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
"Pg" => {
|
||||
cell = None;
|
||||
// Capped as swatches are: a file of many `Pg` would make as many empty groups.
|
||||
if let Some(name) = first_text(&vals)
|
||||
if let Some(name) = first_text(&vals, names)
|
||||
&& out.groups.len() < MAX_SWATCHES
|
||||
{
|
||||
out.groups.push((name, Vec::new()));
|
||||
@@ -150,7 +151,7 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
"Pc" => {
|
||||
// The name before `Pc`, or the one the paint gives; `[Registration]` and the other
|
||||
// built-in swatches every document has are left out.
|
||||
let name = last_text(&vals);
|
||||
let name = last_text(&vals, names);
|
||||
if let Some(mut e) = cell.take()
|
||||
&& !name.as_ref().is_some_and(|n| n.starts_with('['))
|
||||
&& count < MAX_SWATCHES
|
||||
@@ -181,12 +182,12 @@ fn push(vals: &mut Vec<V>, v: V) {
|
||||
}
|
||||
}
|
||||
|
||||
fn first_text(vals: &[V]) -> Option<String> {
|
||||
vals.iter().find(|v| matches!(v, V::Str(_))).and_then(V::text)
|
||||
fn first_text(vals: &[V], names: &mut Names) -> Option<String> {
|
||||
vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v))
|
||||
}
|
||||
|
||||
fn last_text(vals: &[V]) -> Option<String> {
|
||||
vals.iter().rev().find(|v| matches!(v, V::Str(_))).and_then(V::text)
|
||||
fn last_text(vals: &[V], names: &mut Names) -> Option<String> {
|
||||
vals.iter().rev().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -197,6 +198,21 @@ mod tests {
|
||||
Entry { name: name.into(), cell: Cell::Color { color, global, spot } }
|
||||
}
|
||||
|
||||
fn palette(data: &[u8]) -> Palette {
|
||||
super::palette(data, &mut Names::default())
|
||||
}
|
||||
|
||||
/// Swatch, group and spot colour names that aren't UTF-8 are read in the encoding picked.
|
||||
#[test]
|
||||
fn palette_names_read_in_the_encoding() {
|
||||
let mut names = Names::new(super::super::obj::NameEncoding::ShiftJis);
|
||||
// 赤 (red) and 色 (color) in Shift_JIS.
|
||||
let p = super::palette(b"0 0 Pb\n0 (\x90\x46) 1 Pg\n0 1 1 0 (\x90\xd4) 0 x\n(\x90\xd4)\nPc\nPB\n", &mut names);
|
||||
assert_eq!(p.groups.first().map(|(n, s)| (n.as_str(), s.len())), Some(("色", 1)));
|
||||
assert_eq!(p.groups.first().and_then(|(_, s)| s.first()), Some(&color("赤", Color::cmyk(0.0, 1.0, 1.0, 0.0), true, true)));
|
||||
assert_eq!(names.notes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_palette_gives_its_swatches_in_order() {
|
||||
let p = palette(
|
||||
|
||||
@@ -125,7 +125,7 @@ use vectorcraft_geom::{AnchorKind, FillRule, PathData};
|
||||
use super::super::ate::CANVAS_CENTRE;
|
||||
use super::super::graphics::HAIRLINE;
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::{self, Obj, V};
|
||||
use super::obj::{self, NameEncoding, Obj, V};
|
||||
use super::paint::{self, Global, GradientDef, Instance, Named, ShownNames};
|
||||
use crate::ps;
|
||||
|
||||
@@ -238,22 +238,30 @@ pub(super) fn remember_xml_id(xml_ids: &mut HashMap<String, Option<NodeId>>, byt
|
||||
}
|
||||
}
|
||||
|
||||
/// [`read_in`], names that aren't UTF-8 read as Windows-1252.
|
||||
#[cfg(test)]
|
||||
pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
|
||||
read_in(data, large_canvas, NameEncoding::default())
|
||||
}
|
||||
|
||||
/// Read the structure of the editing data `data`, applying the file's `%AI24_LargeCanvasScale` only
|
||||
/// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may
|
||||
/// keep the reduced one, which the PDF import reads as it is (#2031).
|
||||
pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
|
||||
read_with(data, large_canvas, false)
|
||||
/// keep the reduced one, which the PDF import reads as it is (#2031). Names that aren't UTF-8
|
||||
/// (layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes,
|
||||
/// symbols, variables and data sets) are read in `names`, each with a warning.
|
||||
pub fn read_in(data: &[u8], large_canvas: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
read_with(data, large_canvas, false, names)
|
||||
}
|
||||
|
||||
/// [`read`] for a file whose page art replaces the objects the reader doesn't read (see
|
||||
/// [`read_in`] for a file whose page art replaces the objects the reader doesn't read (see
|
||||
/// [`Reader::unread_box`]): an image past the hard limits ([`raster::Limits`]) is then such an
|
||||
/// object, where [`read`] leaves it out with a note.
|
||||
pub(crate) fn read_over_page(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
|
||||
read_with(data, large_canvas, true)
|
||||
/// object, where [`read_in`] leaves it out with a note.
|
||||
pub(crate) fn read_over_page(data: &[u8], large_canvas: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
read_with(data, large_canvas, true, names)
|
||||
}
|
||||
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool) -> Result<Structure, String> {
|
||||
let mut r = Reader::with_names(data);
|
||||
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
let mut r = Reader::with_names(data, names);
|
||||
r.large_canvas = large_canvas.then_some(1.0);
|
||||
r.page_art = page_art;
|
||||
r.run()?;
|
||||
@@ -263,17 +271,18 @@ fn read_with(data: &[u8], large_canvas: bool, page_art: bool) -> Result<Structur
|
||||
/// How many objects [`read`] counts against [`MAX_NODES`] in the editing data `data`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn objects_read(data: &[u8]) -> Result<usize, String> {
|
||||
let mut r = Reader::with_names(data);
|
||||
let mut r = Reader::with_names(data, NameEncoding::default());
|
||||
r.run()?;
|
||||
Ok(r.nodes)
|
||||
}
|
||||
|
||||
/// [`read`] for editing data kept apart from the program `ps` it came with (an EPS's private
|
||||
/// data): the program's header gives the author and the custom colors the editing data's header
|
||||
/// doesn't. `page_art`: as [`read_over_page`] reads a file.
|
||||
pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool) -> Result<Structure, String> {
|
||||
let header = Reader::header_of(ps);
|
||||
let mut r = Reader::with_names(data);
|
||||
/// doesn't. `page_art`: as [`read_over_page`] reads a file. `names`: the encoding of names that
|
||||
/// aren't UTF-8.
|
||||
pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool, names: NameEncoding) -> Result<Structure, String> {
|
||||
let header = Reader::header_of(ps, names);
|
||||
let mut r = Reader::with_names(data, names);
|
||||
r.large_canvas = large_canvas.then_some(1.0);
|
||||
r.page_art = page_art;
|
||||
// The program's custom colors have swatches of their names, which no gradient or pattern takes.
|
||||
@@ -292,8 +301,8 @@ pub fn read_beside(data: &[u8], large_canvas: bool, ps: &[u8], page_art: bool) -
|
||||
/// and patterns. Names come only from what the reader reads: not from the alternates of versioned
|
||||
/// content, the prolog, resources, sections it skips (such as symbols and the text document) or
|
||||
/// what follows the trailer. The palette the reader keeps is the last one outside those and the
|
||||
/// sections it reads on their own.
|
||||
fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) {
|
||||
/// sections it reads on their own. Names that aren't UTF-8 are read in `encoding`.
|
||||
fn swatch_names(data: &[u8], encoding: NameEncoding) -> (ShownNames, Option<&[u8]>) {
|
||||
/// The sections the reader reads on their own whose definitions and colors it keeps.
|
||||
const READ_APART: &[&str] = &["BeginPattern", "BeginBrushPattern", "BeginPluginObject", "BeginArtStyles", "BeginArtStyleList"];
|
||||
/// Whether the marker `m` ends what began with `begin` and ends with `end`, `depth` of its kind
|
||||
@@ -311,7 +320,9 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) {
|
||||
}
|
||||
let mut names = ShownNames::default();
|
||||
names.reserve(vectorcraft_color::swatch::REGISTRATION);
|
||||
let mut colors = paint::ColorNames::default();
|
||||
// The reader reads these names again, and keeps the notes on them.
|
||||
let mut decoder = obj::Names::new(encoding);
|
||||
let mut colors = paint::ColorNames::new(encoding);
|
||||
let mut lex = Lexer::new(data);
|
||||
// What the reader skips wherever it is (the marker of one of its kind nested in it, its end
|
||||
// marker, how many are open inside it, and where reading goes on when it doesn't end), the
|
||||
@@ -386,7 +397,7 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) {
|
||||
// A palette's names are those of the palette kept, as the palette reads them.
|
||||
_ if skip.is_some() || palette_at.is_some() || section.is_some_and(|s| !s.3) => continue,
|
||||
Tok::Str(s) | Tok::Hex(s) if std::mem::take(&mut head) => {
|
||||
let text = obj::text(s);
|
||||
let text = decoder.bytes(s);
|
||||
if !text.contains('\n') {
|
||||
names.reserve(&text);
|
||||
}
|
||||
@@ -396,7 +407,7 @@ fn swatch_names(data: &[u8]) -> (ShownNames, Option<&[u8]>) {
|
||||
colors.token(t.tok);
|
||||
}
|
||||
colors.gradients.iter().filter(|n| !n.contains('\n')).for_each(|n| names.reserve(n));
|
||||
let palette = palette.map(super::palette::palette).unwrap_or_default();
|
||||
let palette = palette.map(|p| super::palette::palette(p, &mut decoder)).unwrap_or_default();
|
||||
if palette.is_empty() {
|
||||
for mode in [ColorMode::Rgb, ColorMode::Cmyk] {
|
||||
let (swatches, groups) = vectorcraft_color::default_swatches(mode.model());
|
||||
@@ -631,6 +642,8 @@ struct Reader<'a> {
|
||||
/// The legacy text object being read (`To` … `TO`), and the modal type attributes.
|
||||
text: Option<legacy_text::LegacyText>,
|
||||
type_state: legacy_text::TypeState,
|
||||
/// How names that aren't UTF-8 are read, and the notes on them.
|
||||
names: obj::Names,
|
||||
/// The file has brush objects (the brush manager's order or a brush).
|
||||
brush_objects: bool,
|
||||
/// The brush manager's order.
|
||||
@@ -668,13 +681,14 @@ pub(crate) fn is_look_slot(name: Option<&str>) -> bool {
|
||||
impl<'a> Reader<'a> {
|
||||
/// A reader of the editing data `data` as a whole, with the names its gradients and patterns
|
||||
/// get in the document and its art styles found first ([`swatch_names`]): the symbols defined
|
||||
/// before the art styles section use them too.
|
||||
fn with_names(data: &'a [u8]) -> Self {
|
||||
/// before the art styles section use them too. Names that aren't UTF-8 are read in `encoding`.
|
||||
fn with_names(data: &'a [u8], encoding: NameEncoding) -> Self {
|
||||
let mut r = Self::new(data);
|
||||
let (names, styles) = swatch_names(data);
|
||||
r.names = obj::Names::new(encoding);
|
||||
let (names, styles) = swatch_names(data, encoding);
|
||||
r.global_names = names;
|
||||
if let Some(section) = styles {
|
||||
r.styles = super::style::art_styles(section);
|
||||
r.styles = super::style::art_styles_in(section, &mut r.names);
|
||||
}
|
||||
r
|
||||
}
|
||||
@@ -794,6 +808,7 @@ impl<'a> Reader<'a> {
|
||||
v7_patterns: HashSet::new(),
|
||||
text: None,
|
||||
type_state: legacy_text::TypeState::default(),
|
||||
names: obj::Names::default(),
|
||||
brush_objects: false,
|
||||
brush_order: None,
|
||||
brush_drafts: Vec::new(),
|
||||
|
||||
@@ -640,7 +640,7 @@ impl<'a> Reader<'a> {
|
||||
/// comes in as the page draws it, within the symbol's box placed by the matrix when the box is
|
||||
/// known.
|
||||
fn symbol_instance(&mut self, o: &Obj) -> Result<(), String> {
|
||||
let name = o.text("SymbolRef");
|
||||
let name = self.names.entry(o, "SymbolRef");
|
||||
let affine = |n: &[f64]| match n {
|
||||
[a, b, c, d, e, f] if [a, b, c, d, e, f].iter().all(|v| v.is_finite()) => Some(super::Affine::new([*a, *b, *c, *d, *e, *f])),
|
||||
_ => None,
|
||||
|
||||
@@ -123,7 +123,7 @@ impl<'a> Reader<'a> {
|
||||
// GUARDS: a definition's art takes the file's art styles and hands them back, so a
|
||||
// section inside it would replace the file's own.
|
||||
if self.subreader_depth == 0 {
|
||||
self.styles = crate::import::ai::style::art_styles(section);
|
||||
self.styles = crate::import::ai::style::art_styles_in(section, &mut self.names);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -134,7 +134,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
if m == "BeginPalette" {
|
||||
let section = self.section("EndPalette")?;
|
||||
self.palette = crate::import::ai::palette::palette(section);
|
||||
self.palette = crate::import::ai::palette::palette(section, &mut self.names);
|
||||
return Ok(());
|
||||
}
|
||||
if m == "BeginEncoding" {
|
||||
@@ -309,7 +309,7 @@ impl<'a> Reader<'a> {
|
||||
return;
|
||||
}
|
||||
let Some(open) = v.iter().position(|&b| b == b'(') else { return };
|
||||
let (Some(head), Some(name)) = (v.get(..open), v.get(open..).and_then(ps_string)) else { return };
|
||||
let (Some(head), Some(name)) = (v.get(..open), v.get(open..).and_then(ps_bytes)) else { return };
|
||||
// One more than a color has, so that too many are seen.
|
||||
let parts: Vec<Option<f32>> = head
|
||||
.split(u8::is_ascii_whitespace)
|
||||
@@ -322,6 +322,7 @@ impl<'a> Reader<'a> {
|
||||
[Some(r), Some(g), Some(b)] if !cmyk => vectorcraft_color::Color::rgb(*r, *g, *b),
|
||||
_ => return,
|
||||
};
|
||||
let name = self.names.bytes(&name);
|
||||
if !name.trim().is_empty() && !self.custom_colors.iter().any(|n| n.name == name) {
|
||||
// A gradient or pattern defined after it does not take its swatch's name.
|
||||
self.global_names.reserve(&name);
|
||||
@@ -378,6 +379,7 @@ impl<'a> Reader<'a> {
|
||||
Legacy::Yes(def) => return self.define_legacy_pattern(&def),
|
||||
}
|
||||
let mut sub = Reader::new(section);
|
||||
sub.names = super::obj::Names::new(self.names.encoding);
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
sub.pattern = Some(PatternHead::default());
|
||||
// The gradients and art styles defined so far and the names given, lent (and the gradients
|
||||
@@ -392,6 +394,7 @@ impl<'a> Reader<'a> {
|
||||
self.take_back_budgets(&mut sub);
|
||||
self.nodes = sub.nodes;
|
||||
self.gradients = std::mem::take(&mut sub.gradients);
|
||||
self.names.absorb(std::mem::take(&mut sub.names));
|
||||
self.global_names = std::mem::take(&mut sub.global_names);
|
||||
self.styles = std::mem::take(&mut sub.styles);
|
||||
// Unread art in the definition, hidden or not, leaves the tile incomplete.
|
||||
@@ -409,8 +412,9 @@ impl<'a> Reader<'a> {
|
||||
/// tile's box (`_ &`). Each layer starts from the state the definition starts with.
|
||||
fn define_legacy_pattern(&mut self, def: &LegacyPattern) {
|
||||
let mut sub = Reader::new(b"");
|
||||
sub.names = super::obj::Names::new(self.names.encoding);
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
sub.pattern = Some(PatternHead { taken: true, name: def.name.clone(), tile: Some(def.tile) });
|
||||
sub.pattern = Some(PatternHead { taken: true, name: self.names.bytes(&def.name), tile: Some(def.tile) });
|
||||
sub.gradients = std::mem::take(&mut self.gradients);
|
||||
sub.global_names = std::mem::take(&mut self.global_names);
|
||||
sub.styles = std::mem::take(&mut self.styles);
|
||||
@@ -429,6 +433,7 @@ impl<'a> Reader<'a> {
|
||||
self.gradients = std::mem::take(&mut sub.gradients);
|
||||
self.global_names = std::mem::take(&mut sub.global_names);
|
||||
self.styles = std::mem::take(&mut sub.styles);
|
||||
self.names.absorb(std::mem::take(&mut sub.names));
|
||||
let whole = read.is_ok()
|
||||
&& sub.unsupported.is_empty()
|
||||
&& sub.unknown.is_empty()
|
||||
@@ -569,10 +574,11 @@ impl<'a> Reader<'a> {
|
||||
return;
|
||||
}
|
||||
let name = match Lexer::new(section).next_token().map(|t| t.tok) {
|
||||
Some(Tok::Str(bytes)) => obj::text(&bytes),
|
||||
Some(Tok::Str(bytes)) => self.names.bytes(&bytes),
|
||||
_ => return,
|
||||
};
|
||||
let mut sub = Reader::new(section);
|
||||
sub.names = super::obj::Names::new(self.names.encoding);
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
sub.symbol = Some(SymbolHead::default());
|
||||
sub.template = self.template;
|
||||
@@ -591,6 +597,7 @@ impl<'a> Reader<'a> {
|
||||
self.global_names = std::mem::take(&mut sub.global_names);
|
||||
self.styles = std::mem::take(&mut sub.styles);
|
||||
self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles);
|
||||
self.names.absorb(std::mem::take(&mut sub.names));
|
||||
// Art stopped by a trailer, or with an array, type, image, placed file, gradient mesh or
|
||||
// gradient still open at the end (as [`Reader::run_string`] checks), isn't whole.
|
||||
let open = sub.done
|
||||
@@ -709,7 +716,7 @@ impl<'a> Reader<'a> {
|
||||
if names.len() >= MAX_LISTED {
|
||||
break;
|
||||
}
|
||||
names.push(obj::text(&bytes));
|
||||
names.push(self.names.quiet(&bytes));
|
||||
}
|
||||
}
|
||||
self.symbol_list = Some(names);
|
||||
@@ -719,7 +726,7 @@ impl<'a> Reader<'a> {
|
||||
/// order or a brush ([`brush`]); other objects are left alone. A brush without a name is counted
|
||||
/// as not read.
|
||||
fn plugin_object(&mut self, section: &[u8]) {
|
||||
let Some((kind, name, params)) = brush::plugin_strings(section) else { return };
|
||||
let Some((kind, name, params)) = brush::plugin_strings(section, &self.names) else { return };
|
||||
if kind == brush::MANAGER {
|
||||
if name.is_some() {
|
||||
self.brush_objects = true;
|
||||
@@ -737,8 +744,12 @@ impl<'a> Reader<'a> {
|
||||
self.brushes_unread += 1;
|
||||
return;
|
||||
}
|
||||
match brush::parse_tool_object(tool, &name, ¶ms) {
|
||||
Ok(d) => self.brush_drafts.push(d),
|
||||
match brush::parse_tool_object(tool, &self.names.quiet(&name), ¶ms) {
|
||||
Ok(d) => {
|
||||
// Only the name of a brush that is read gets a note.
|
||||
self.names.bytes(&name);
|
||||
self.brush_drafts.push(d);
|
||||
}
|
||||
Err(_) => self.brushes_unread += 1,
|
||||
}
|
||||
}
|
||||
@@ -756,8 +767,9 @@ impl<'a> Reader<'a> {
|
||||
self.brush_art_sections += 1;
|
||||
let mut lex = Lexer::new(section);
|
||||
let Some(Tok::Str(name) | Tok::Hex(name)) = lex.next_token().map(|t| t.tok) else { return };
|
||||
let name = obj::text(&name);
|
||||
let name = self.names.bytes(&name);
|
||||
let mut sub = Reader::new(lex.slice(lex.offset(), section.len()));
|
||||
sub.names = obj::Names::new(self.names.encoding);
|
||||
// In a space of its own (y down), with no head to take.
|
||||
sub.pattern = Some(PatternHead { taken: true, ..PatternHead::default() });
|
||||
sub.subreader_depth = self.subreader_depth + 1;
|
||||
@@ -779,6 +791,7 @@ impl<'a> Reader<'a> {
|
||||
self.pattern_tiles = std::mem::take(&mut sub.pattern_tiles);
|
||||
self.v7_patterns = std::mem::take(&mut sub.v7_patterns);
|
||||
self.global_names = std::mem::take(&mut sub.global_names);
|
||||
self.names.absorb(std::mem::take(&mut sub.names));
|
||||
// Images aren't kept: the document's images don't count a brush's art among their users. Art
|
||||
// read with a note (something left out of it) isn't kept either.
|
||||
let whole = read.is_ok()
|
||||
@@ -834,7 +847,8 @@ impl<'a> Reader<'a> {
|
||||
head.taken = true;
|
||||
let Some(at) = vals.iter().position(|v| matches!(v, V::Str(_))) else { return vals };
|
||||
let tile = Self::nums(vals.get(at + 1..at + 5).unwrap_or_default());
|
||||
let ([x0, y0, x1, y1], Some(name)) = (tile.as_slice(), vals.get(at).and_then(V::text)) else { return vals };
|
||||
let [x0, y0, x1, y1] = tile.as_slice() else { return vals };
|
||||
let Some(name) = vals.get(at).and_then(|v| self.names.text(v)) else { return vals };
|
||||
head.name = name;
|
||||
head.tile = Some(Rect::new(*x0, *y0, *x1, *y1));
|
||||
vals.split_off(at + 5)
|
||||
@@ -853,7 +867,7 @@ const MAX_LAYERS: usize = 65_536;
|
||||
/// A pattern's definition in the version 7 form: its head, then a list of layers painted in order,
|
||||
/// the first the lowest.
|
||||
struct LegacyPattern {
|
||||
name: String,
|
||||
name: Vec<u8>,
|
||||
/// The tile in the pattern's space (y up).
|
||||
tile: Rect,
|
||||
layers: Vec<LegacyLayer>,
|
||||
@@ -892,7 +906,7 @@ fn legacy_pattern(section: &[u8]) -> Legacy {
|
||||
let mut lex = super::Lexer::new(section);
|
||||
let mut next = || next_operand(&mut lex).ok().flatten().map(|t| (t.tok, t.hidden));
|
||||
let name = match next() {
|
||||
Some((Tok::Str(s), _)) => super::obj::text(&s),
|
||||
Some((Tok::Str(s), _)) => s,
|
||||
_ => return Legacy::No,
|
||||
};
|
||||
let mut tile = [0.0; 4];
|
||||
@@ -961,6 +975,11 @@ const MAX_STRING: usize = 4096;
|
||||
/// The PostScript string `v` starts with (after spaces), its escapes read, when it ends on the line
|
||||
/// and holds at most [`MAX_STRING`] bytes.
|
||||
fn ps_string(v: &[u8]) -> Option<String> {
|
||||
ps_bytes(v).map(|bytes| obj::text(&bytes))
|
||||
}
|
||||
|
||||
/// [`ps_string`] as the string's bytes.
|
||||
fn ps_bytes(v: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut bytes = v.trim_ascii_start().iter().copied().peekable();
|
||||
if bytes.next()? != b'(' {
|
||||
return None;
|
||||
@@ -997,7 +1016,7 @@ fn ps_string(v: &[u8]) -> Option<String> {
|
||||
out.push(b);
|
||||
}
|
||||
b')' => {
|
||||
let Some(d) = depth.checked_sub(1) else { return Some(super::obj::text(&out)) };
|
||||
let Some(d) = depth.checked_sub(1) else { return Some(out) };
|
||||
depth = d;
|
||||
out.push(b);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::{
|
||||
Affine, Arc, Artboard, CANVAS_CENTRE, ColorMode, Document, Global, HashMap, Node, NodeId, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect,
|
||||
Structure, Swatch, Unit, V, obj,
|
||||
};
|
||||
use crate::import::ai::NameEncoding;
|
||||
use vectorcraft_doc::{DataSet, DataValue, GraphicStyle, Guide, ImageBlob, PatternDef, Symbol, TextWrap, Variable, VariableKind, Variables};
|
||||
|
||||
/// `%AI5_RulerUnits` → [`Unit`] (Illustrator 7 spec table 3; centimeters is 4; pixels is 6 in later files).
|
||||
@@ -141,21 +142,31 @@ pub(crate) const LOOK_SLOT: &str = "\u{0}text:look";
|
||||
|
||||
/// The artboards (names, and rects in art space) of the editing data `data` that [`read`] can't
|
||||
/// read as a whole, as far as it gets: where to put the pages of its PDF part instead, and what to
|
||||
/// call them (#1068); and the index among them of the active artboard (`CropAreaActive`).
|
||||
pub fn artboards(data: &[u8]) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
let found = artboards_read(data);
|
||||
/// call them (#1068); and the index among them of the active artboard (`CropAreaActive`). `names`
|
||||
/// reads their names and keeps the notes on them.
|
||||
pub fn artboards(data: &[u8], names: &mut obj::Names) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
let mut from_start = obj::Names::new(names.encoding);
|
||||
let found = artboards_read(data, &mut from_start);
|
||||
if !found.0.is_empty() && found.1.is_some() {
|
||||
names.absorb(from_start);
|
||||
return found;
|
||||
}
|
||||
// Reading can stop before the artboards (art kept in the document's data comes first), or
|
||||
// between them and the active artboard's entry: read their array on its own.
|
||||
let Some((boards, active)) = artboard_array_read(data) else { return found };
|
||||
let mut from_array = obj::Names::new(names.encoding);
|
||||
let Some((boards, active)) = artboard_array_read(data, &mut from_array) else {
|
||||
names.absorb(from_start);
|
||||
return found;
|
||||
};
|
||||
if found.0.is_empty() {
|
||||
names.absorb(from_array);
|
||||
(boards, active)
|
||||
} else if boards == found.0 {
|
||||
// The same array: its names and rects read alike.
|
||||
names.absorb(from_start);
|
||||
(found.0, active)
|
||||
} else {
|
||||
names.absorb(from_start);
|
||||
found
|
||||
}
|
||||
}
|
||||
@@ -163,7 +174,7 @@ pub fn artboards(data: &[u8]) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
/// The index of the active artboard (`CropAreaActive`) in the `ArtboardArray` of the editing data
|
||||
/// `data`, as [`artboards`] reads it, from that array and the entry beside it alone.
|
||||
pub fn active_artboard(data: &[u8]) -> Option<usize> {
|
||||
artboard_array_read(data)?.1
|
||||
artboard_array_read(data, &mut obj::Names::default())?.1
|
||||
}
|
||||
|
||||
/// Artboards as [`artboards`] gives them: names and rects in art space, and the active one's index.
|
||||
@@ -171,7 +182,7 @@ type ArtboardsRead = (Vec<(String, Rect)>, Option<usize>);
|
||||
|
||||
/// The `ArtboardArray` entry of the editing data `data` ([`artboard_array`]), with the
|
||||
/// `CropAreaActive` entry beside it, read on their own as [`artboards_read`] reads them.
|
||||
fn artboard_array_read(data: &[u8]) -> Option<ArtboardsRead> {
|
||||
fn artboard_array_read(data: &[u8], names: &mut obj::Names) -> Option<ArtboardsRead> {
|
||||
let (array, active) = artboard_array(data)?;
|
||||
let mut alone = b"%_/Document :\n%_/Dictionary :\n".to_vec();
|
||||
alone.extend_from_slice(array);
|
||||
@@ -180,7 +191,7 @@ fn artboard_array_read(data: &[u8]) -> Option<ArtboardsRead> {
|
||||
alone.extend_from_slice(active);
|
||||
}
|
||||
alone.extend_from_slice(b"\n%_; /NotRecorded ,\n%_;\n");
|
||||
Some(artboards_read(&alone))
|
||||
Some(artboards_read(&alone, names))
|
||||
}
|
||||
|
||||
/// The key that ends the `ArtboardArray` entry of the editing data.
|
||||
@@ -278,6 +289,26 @@ fn beside(mut defined: Vec<PatternDef>, have: &[PatternDef]) -> (Vec<PatternDef>
|
||||
(defined, renamed)
|
||||
}
|
||||
|
||||
/// The names of the objects in `art` and what they hold, masks included.
|
||||
fn art_names<'a>(art: impl IntoIterator<Item = &'a Node>) -> Vec<&'a str> {
|
||||
let mut stack: Vec<&Node> = art.into_iter().collect();
|
||||
let mut names = vec![];
|
||||
while let Some(n) = stack.pop() {
|
||||
stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| &**c));
|
||||
names.extend(n.name.as_deref());
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// The names read for the patterns `added` that the patterns don't show as their own: those
|
||||
/// `renamed` replaced, and the names of the objects of their art.
|
||||
fn pattern_names_read<'a>(added: &'a [PatternDef], renamed: &'a std::collections::BTreeMap<String, String>) -> Vec<&'a str> {
|
||||
let shown: std::collections::HashSet<&str> = added.iter().map(|p| p.name.as_str()).collect();
|
||||
let mut names: Vec<&str> = renamed.iter().filter(|(_, new)| shown.contains(new.as_str())).map(|(old, _)| old.as_str()).collect();
|
||||
names.extend(art_names(added.iter().flat_map(|p| p.art.iter().map(|a| &**a))));
|
||||
names
|
||||
}
|
||||
|
||||
/// `n` and what it holds painting with the patterns `renamed` renames (old to new) by their new names.
|
||||
fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap<String, String>) {
|
||||
for p in vectorcraft_doc::swatches::node_paints_mut(n) {
|
||||
@@ -301,9 +332,10 @@ fn repoint(n: &mut Node, renamed: &std::collections::BTreeMap<String, String>) {
|
||||
/// layers aren't read: the swatches its PDF part has are the defaults (#4036). They come as
|
||||
/// [`read`] makes them, with pattern swatches only for the patterns in `patterns` and those the
|
||||
/// editing data defines, which are named apart from `patterns` ([`beside`]); `None` when the
|
||||
/// palette is empty or reading stops before it.
|
||||
pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option<PaletteSwatches> {
|
||||
let mut r = Reader::with_names(data);
|
||||
/// palette is empty or reading stops before it. `names` reads their names and keeps the notes on
|
||||
/// those the swatches take.
|
||||
pub fn palette(data: &[u8], patterns: &[PatternDef], names: &mut obj::Names) -> Option<PaletteSwatches> {
|
||||
let mut r = Reader::with_names(data, names.encoding);
|
||||
// The palette, the gradients and the patterns it names come before the art, so an error in the
|
||||
// art leaves them read.
|
||||
let _ = r.run();
|
||||
@@ -331,6 +363,15 @@ pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option<PaletteSwatches>
|
||||
images.insert(im.key.clone(), blob.clone());
|
||||
}
|
||||
});
|
||||
let used: Vec<&str> = swatches
|
||||
.iter()
|
||||
.chain(groups.iter().flat_map(|g| &g.swatches))
|
||||
.map(|s| s.name.as_str())
|
||||
.chain(groups.iter().map(|g| g.name.as_str()))
|
||||
.chain(added.iter().map(|p| p.name.as_str()))
|
||||
.chain(pattern_names_read(&added, &renamed))
|
||||
.collect();
|
||||
names.absorb_among(std::mem::take(&mut r.names), &used);
|
||||
Some(PaletteSwatches { swatches, groups, patterns: added, images })
|
||||
}
|
||||
|
||||
@@ -349,11 +390,12 @@ pub struct Brushes {
|
||||
/// The brushes of the editing data `data`, for a file whose layers aren't read (a brush library
|
||||
/// has none), as [`read`] makes them, with the patterns of the editing data their art paints with,
|
||||
/// named apart from `patterns` ([`beside`]) as [`palette`] names them. `None` when the file has no
|
||||
/// brush objects or reading stops before them.
|
||||
pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option<Brushes> {
|
||||
/// brush objects or reading stops before them. `names` reads their names and keeps the notes on
|
||||
/// those it brings in.
|
||||
pub fn brushes(data: &[u8], patterns: &[PatternDef], names: &mut obj::Names) -> Option<Brushes> {
|
||||
// Brush objects are plug-in objects: data without one isn't read again.
|
||||
memchr::memmem::find(data, b"BeginPluginObject")?;
|
||||
let mut r = Reader::with_names(data);
|
||||
let mut r = Reader::with_names(data, names.encoding);
|
||||
// The brushes come before the art, so an error in the art leaves them read.
|
||||
let _ = r.run();
|
||||
let (mut lib, unread) = r.brush_library()?;
|
||||
@@ -386,6 +428,14 @@ pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option<Brushes> {
|
||||
images.insert(im.key.clone(), blob.clone());
|
||||
}
|
||||
});
|
||||
let used: Vec<&str> = lib
|
||||
.iter()
|
||||
.map(|b| b.name.as_str())
|
||||
.chain(added.iter().map(|p| p.name.as_str()))
|
||||
.chain(pattern_names_read(&added, &renamed))
|
||||
.chain(art_names(lib.iter().flat_map(|b| b.art())))
|
||||
.collect();
|
||||
names.absorb_among(std::mem::take(&mut r.names), &used);
|
||||
Some(Brushes { brushes: lib, patterns: added, images, note: (unread > 0).then(|| brush::left_out_note(unread)) })
|
||||
}
|
||||
|
||||
@@ -393,16 +443,19 @@ pub fn brushes(data: &[u8], patterns: &[PatternDef]) -> Option<Brushes> {
|
||||
/// style library has none), as [`read`] makes them, with ids from 1, and the data's declared large
|
||||
/// canvas scale (1 when it declares none). `large_canvas`: as [`read`] takes it. The styles are
|
||||
/// those the list names before reading stops, and none when the data has no style list or lists no
|
||||
/// style this reads.
|
||||
pub fn graphic_styles(data: &[u8], large_canvas: bool) -> (Vec<GraphicStyle>, f64) {
|
||||
/// style this reads. `names` reads their names and keeps the notes on them.
|
||||
pub fn graphic_styles(data: &[u8], large_canvas: bool, names: &mut obj::Names) -> (Vec<GraphicStyle>, f64) {
|
||||
if crate::import::ai::find(data, b"BeginArtStyleList").is_none() {
|
||||
return (vec![], 1.0);
|
||||
}
|
||||
let mut r = Reader::with_names(data);
|
||||
let mut r = Reader::with_names(data, names.encoding);
|
||||
r.large_canvas = large_canvas.then_some(1.0);
|
||||
// The styles come before the art, so an error in the art leaves them read.
|
||||
let _ = r.run();
|
||||
(r.graphic_styles().0, r.canvas_scale)
|
||||
let styles = r.graphic_styles().0;
|
||||
let used: Vec<&str> = styles.iter().map(|s| s.name.as_str()).collect();
|
||||
names.absorb_among(std::mem::take(&mut r.names), &used);
|
||||
(styles, r.canvas_scale)
|
||||
}
|
||||
|
||||
/// The symbols of a `.ai` file's editing data, as [`symbols`] reads them.
|
||||
@@ -421,10 +474,10 @@ pub struct Symbols {
|
||||
/// instance of a symbol the data doesn't define, and with each object of their art that is linked
|
||||
/// to a listed graphic style linked to it by the id [`graphic_styles`] gives that style. The
|
||||
/// symbols are those defined before reading stops and before the first layer; `None` when there
|
||||
/// are none.
|
||||
pub fn symbols(data: &[u8]) -> Option<Symbols> {
|
||||
/// are none. `names` reads their names and keeps the notes on those it brings in.
|
||||
pub fn symbols(data: &[u8], names: &mut obj::Names) -> Option<Symbols> {
|
||||
crate::import::ai::find(data, b"BeginSymbol")?;
|
||||
let mut r = Reader::with_names(data);
|
||||
let mut r = Reader::with_names(data, names.encoding);
|
||||
// The symbols come before the art, so an error in the art leaves them read.
|
||||
let _ = r.run();
|
||||
r.check_symbols();
|
||||
@@ -468,14 +521,22 @@ pub fn symbols(data: &[u8]) -> Option<Symbols> {
|
||||
let reg = r.doc.symbol_registration(&s.name);
|
||||
(s, reg)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let used: Vec<&str> = symbols
|
||||
.iter()
|
||||
.map(|(s, _)| s.name.as_str())
|
||||
.chain(swatches.iter().map(|s| s.name.as_str()))
|
||||
.chain(art_names(symbols.iter().map(|(s, _)| &*s.art)))
|
||||
.collect();
|
||||
names.absorb_among(std::mem::take(&mut r.names), &used);
|
||||
Some(Symbols { symbols, images, swatches })
|
||||
}
|
||||
|
||||
/// [`artboards`] as far as reading `data` from its start gets: those read before an error, and
|
||||
/// those of a `/Document` still open when it stopped (its art can be what stopped it).
|
||||
fn artboards_read(data: &[u8]) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
fn artboards_read(data: &[u8], names: &mut obj::Names) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
let mut r = Reader::new(data);
|
||||
r.names = obj::Names::new(names.encoding);
|
||||
let _ = r.run();
|
||||
let open: Vec<Obj> = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect();
|
||||
for o in &open {
|
||||
@@ -484,6 +545,8 @@ fn artboards_read(data: &[u8]) -> (Vec<(String, Rect)>, Option<usize>) {
|
||||
}
|
||||
}
|
||||
let active = r.active_board.filter(|i| *i < r.artboards.len());
|
||||
let used: Vec<&str> = r.artboards.iter().map(|b| b.name.as_str()).collect();
|
||||
names.absorb_among(std::mem::take(&mut r.names), &used);
|
||||
(r.artboards.into_iter().map(|b| (b.name, b.rect)).collect(), active)
|
||||
}
|
||||
|
||||
@@ -535,20 +598,27 @@ impl VariableRead {
|
||||
}
|
||||
|
||||
/// The variable library of the editing data `data` ([`VariableRead`]), for a file whose layers
|
||||
/// [`read`] can't read. Data without a `variableSets` element isn't read.
|
||||
pub fn variables(data: &[u8]) -> VariableRead {
|
||||
/// [`read`] can't read. Data without a `variableSets` element isn't read. `names` reads the
|
||||
/// library's names and keeps the notes on them.
|
||||
pub fn variables(data: &[u8], names: &mut obj::Names) -> VariableRead {
|
||||
if crate::import::ai::find(data, b"variableSets").is_none() {
|
||||
return VariableRead::default();
|
||||
}
|
||||
let mut r = Reader::new(data);
|
||||
r.names = obj::Names::new(names.encoding);
|
||||
let _ = r.run();
|
||||
if r.variables.present {
|
||||
absorb_library_names(&r.variables, r.names, names);
|
||||
return r.variables;
|
||||
}
|
||||
// A `/Document` still open when reading stopped.
|
||||
let open: Vec<Obj> = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect();
|
||||
if let Some(found) = open.iter().filter(|o| o.ty == "Document").map(variable_library).find(|v| v.present) {
|
||||
return found;
|
||||
for o in open.iter().filter(|o| o.ty == "Document") {
|
||||
let found = variable_library(o, &mut r.names);
|
||||
if found.present {
|
||||
absorb_library_names(&found, r.names, names);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
// Reading can stop before the library: read its node on its own.
|
||||
variable_sets_node(data).map_or(r.variables, |node| {
|
||||
@@ -556,11 +626,20 @@ pub fn variables(data: &[u8]) -> VariableRead {
|
||||
alone.extend_from_slice(node);
|
||||
alone.extend_from_slice(b"\n%_; (xmlnode-children) ,\n%_; /Recorded ,\n%_;\n");
|
||||
let mut r = Reader::new(&alone);
|
||||
r.names = obj::Names::new(names.encoding);
|
||||
let _ = r.run();
|
||||
absorb_library_names(&r.variables, r.names, names);
|
||||
r.variables
|
||||
})
|
||||
}
|
||||
|
||||
/// Take into `names` the notes of `read` on the names of the variables and data sets of `library`.
|
||||
fn absorb_library_names(library: &VariableRead, read: obj::Names, names: &mut obj::Names) {
|
||||
let used: Vec<&str> =
|
||||
library.variables.variables.iter().map(|v| v.name.as_str()).chain(library.variables.datasets.iter().map(|d| d.name.as_str())).collect();
|
||||
names.absorb_among(read, &used);
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
/// Take the author and custom colors that the header of a program gives (`outer`, from
|
||||
/// [`Self::header_of`]), where the editing data's own header gives none: the program of an EPS
|
||||
@@ -569,6 +648,7 @@ impl<'a> Reader<'a> {
|
||||
if self.author.is_none() {
|
||||
self.author = outer.author;
|
||||
}
|
||||
self.names.absorb(outer.names);
|
||||
for n in outer.custom_colors {
|
||||
if self.custom_colors.len() < super::directives::MAX_CUSTOM_COLORS && !self.custom_colors.iter().any(|c| c.name == n.name) {
|
||||
self.custom_colors.push(n);
|
||||
@@ -577,8 +657,8 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
/// A reader that has read the header of the program `ps` (its comment lines up to
|
||||
/// `%%EndComments`).
|
||||
pub(super) fn header_of(ps: &[u8]) -> Reader<'_> {
|
||||
/// `%%EndComments`), its names that aren't UTF-8 read in `names`.
|
||||
pub(super) fn header_of(ps: &[u8], names: NameEncoding) -> Reader<'_> {
|
||||
let mut end = 0;
|
||||
for line in ps.split_inclusive(|&b| b == b'\n' || b == b'\r') {
|
||||
if !line.starts_with(b"%") && !line.trim_ascii().is_empty() {
|
||||
@@ -590,6 +670,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
let mut outer = Reader::new(ps.get(..end).unwrap_or_default());
|
||||
outer.names = obj::Names::new(names);
|
||||
let _ = outer.run();
|
||||
outer
|
||||
}
|
||||
@@ -732,10 +813,10 @@ impl<'a> Reader<'a> {
|
||||
{
|
||||
super::remember_xml_id(&mut self.xml_ids, &mut self.xml_id_bytes, uid, id);
|
||||
}
|
||||
let name = match o.text("AIArtName") {
|
||||
let name = match self.names.entry(o, "AIArtName") {
|
||||
Some(name) => name,
|
||||
None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) {
|
||||
Some(id) => obj::xml_name(&id),
|
||||
None => match o.obj("AI10_ArtUID").and_then(|id| self.names.xml_id_name(id)) {
|
||||
Some(name) => name,
|
||||
None => return,
|
||||
},
|
||||
};
|
||||
@@ -753,7 +834,7 @@ impl<'a> Reader<'a> {
|
||||
/// Effects Settings, its variable library (the first one read), and its Objects on Path records.
|
||||
pub(super) fn document(&mut self, o: &Obj) {
|
||||
if !self.variables.present {
|
||||
self.variables = variable_library(o);
|
||||
self.variables = variable_library(o, &mut self.names);
|
||||
}
|
||||
self.constraint_data(o);
|
||||
if let Some(r) = find_entry(o, "AI Auto Rasterize", 0) {
|
||||
@@ -769,7 +850,7 @@ impl<'a> Reader<'a> {
|
||||
if !sane(r) || r.area() <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let name = ab.text("Name").unwrap_or_else(|| format!("Artboard {}", i + 1));
|
||||
let name = self.names.entry(ab, "Name").unwrap_or_else(|| format!("Artboard {}", i + 1));
|
||||
let mut board = AiArtboard::new(name, r);
|
||||
display_options(ab, &mut board);
|
||||
if active == Some(i as f64) {
|
||||
@@ -1057,7 +1138,7 @@ impl<'a> Reader<'a> {
|
||||
let mut styles: Vec<GraphicStyle> = vec![];
|
||||
let mut ids = HashMap::new();
|
||||
for raw in &self.style_list {
|
||||
let name = obj::text(raw);
|
||||
let name = self.names.quiet(raw);
|
||||
if name.is_empty() || ids.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
@@ -1242,6 +1323,7 @@ impl<'a> Reader<'a> {
|
||||
if !doc.symbols.is_empty() {
|
||||
doc.limit_depth_noting(&mut warnings);
|
||||
}
|
||||
warnings.extend(self.names.notes());
|
||||
Ok(Structure {
|
||||
doc,
|
||||
to_doc,
|
||||
@@ -1399,9 +1481,57 @@ const MAX_LIBRARY_BYTES: usize = 8 << 20;
|
||||
/// variable holds its value: `p` elements of text, one paragraph each; a `/Bool` text, whether its
|
||||
/// objects show; a linked file's URL (`file://`) or path. Graph values aren't read. What is
|
||||
/// malformed, repeated or past the caps (a declaration, a data set, a value, a setting, a second
|
||||
/// `variableSet`) is left out and counted; a library past [`MAX_LIBRARY_NODES`] isn't read.
|
||||
fn variable_library(o: &Obj) -> VariableRead {
|
||||
find_variable_sets(o, 0).map_or_else(VariableRead::default, read_variable_sets)
|
||||
/// `variableSet`) is left out and counted; a library past [`MAX_LIBRARY_NODES`] isn't read. Text
|
||||
/// that isn't UTF-8 is read in the encoding of `names`, which gets a note on each such name of a
|
||||
/// variable or a data set.
|
||||
fn variable_library(o: &Obj, names: &mut obj::Names) -> VariableRead {
|
||||
let Some(sets) = find_variable_sets(o, 0) else { return VariableRead::default() };
|
||||
let mut legacy = HashMap::new();
|
||||
let read = read_variable_sets(&library_in_utf8(sets, names.encoding, &mut legacy, 0));
|
||||
for name in read.variables.variables.iter().map(|v| &v.name).chain(read.variables.datasets.iter().map(|d| &d.name)) {
|
||||
if let Some(replaced) = legacy.get(name) {
|
||||
names.note(name, *replaced);
|
||||
}
|
||||
}
|
||||
read
|
||||
}
|
||||
|
||||
/// The XML nodes of a variable library with their text (a `/String` or `/UnicodeString` entry, whose
|
||||
/// strings a long one is written across are joined first: a string may end inside a character) in
|
||||
/// UTF-8, so that an element named after a variable and that variable's name are alike whatever
|
||||
/// the encoding. `legacy` gets the text that wasn't UTF-8, trimmed as [`library_name`] trims it, and
|
||||
/// whether parts of it weren't valid in the encoding either.
|
||||
fn library_in_utf8(o: &Obj, encoding: NameEncoding, legacy: &mut HashMap<String, bool>, depth: usize) -> Obj {
|
||||
let mut out = o.clone();
|
||||
if depth > MAX_DEPTH {
|
||||
return out;
|
||||
}
|
||||
for (_, values) in &mut out.entries {
|
||||
match values.split_last_mut() {
|
||||
Some((V::Name(ty), strings)) if matches!(ty.as_str(), "String" | "UnicodeString") && strings.iter().all(|v| matches!(v, V::Str(_))) => {
|
||||
let bytes: Vec<u8> = strings.iter().flat_map(|v| if let V::Str(s) = v { s.as_slice() } else { &[] }).copied().collect();
|
||||
let (text, not_utf8, replaced) = encoding.decoded(&bytes);
|
||||
if not_utf8 {
|
||||
legacy.insert(text.trim().to_string(), replaced);
|
||||
}
|
||||
let ty = V::Name(ty.clone());
|
||||
*values = vec![V::Str(text.into_bytes()), ty];
|
||||
}
|
||||
_ => {
|
||||
for v in values.iter_mut() {
|
||||
if let V::Obj(child) = v {
|
||||
*child = std::rc::Rc::new(library_in_utf8(child, encoding, legacy, depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for v in &mut out.content {
|
||||
if let V::Obj(child) = v {
|
||||
*child = std::rc::Rc::new(library_in_utf8(child, encoding, legacy, depth + 1));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The `variableSets` XML node in `o` or the dictionaries it holds.
|
||||
|
||||
@@ -169,7 +169,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
if let Some(d) = self.gradient_def.as_mut() {
|
||||
match w {
|
||||
"Bs" => d.stop(&vals, &mut self.stop_spots),
|
||||
"Bs" => d.stop(&vals, &mut self.stop_spots, &mut self.names),
|
||||
"BD" => {
|
||||
if let Some((name, g)) = self.gradient_def.take().and_then(GradientDef::finish) {
|
||||
self.global_names.define(Global::Gradient, &name);
|
||||
@@ -227,7 +227,7 @@ impl<'a> Reader<'a> {
|
||||
"Lb" => self.layer_attrs(&vals),
|
||||
"Ln" => {
|
||||
if let Some(Frame { kind: Kind::Layer(a), .. }) = self.frames.last_mut() {
|
||||
a.name = vals.iter().rev().find_map(V::text).unwrap_or_default();
|
||||
a.name = vals.iter().rev().find_map(|v| self.names.text(v)).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
"LB" => self.layer_end()?,
|
||||
@@ -319,12 +319,12 @@ impl<'a> Reader<'a> {
|
||||
"A" => self.gs.locked = Self::nums(&vals).last() == Some(&1.0),
|
||||
"XW" => self.state_marker(&vals, hidden),
|
||||
// Gradients.
|
||||
"Bd" => self.gradient_def = GradientDef::begin(&vals),
|
||||
"Bd" => self.gradient_def = GradientDef::begin(&vals, &mut self.names),
|
||||
"Bs" | "BD" => {}
|
||||
"Bb" => self.instance = Some(Instance::begin(&vals)),
|
||||
"Bg" => {
|
||||
if let Some(i) = self.instance.as_mut() {
|
||||
i.bg(&vals);
|
||||
i.bg(&vals, &mut self.names);
|
||||
}
|
||||
}
|
||||
"Bm" => {
|
||||
@@ -479,7 +479,7 @@ impl<'a> Reader<'a> {
|
||||
if self.frames.is_empty() {
|
||||
return;
|
||||
}
|
||||
let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None });
|
||||
let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| self.names.text(v));
|
||||
let Some(tile) = name.as_deref().and_then(|n| self.pattern_tiles.get(n)).copied() else {
|
||||
self.unread_pattern(w);
|
||||
return;
|
||||
@@ -523,7 +523,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
fn color(&mut self, w: &str, vals: &[V]) {
|
||||
let Some((p, named)) = paint::color_op(w, vals) else { return };
|
||||
let Some((p, named)) = paint::color_op_in(w, vals, &mut self.names) else { return };
|
||||
// Registration (`Xs`, `Xz`) says nothing of the document's colour model.
|
||||
let registration = matches!(w, "Xs" | "XS" | "Xz" | "XZ") || p.is_registration();
|
||||
match p.color().filter(|_| !registration) {
|
||||
@@ -706,7 +706,7 @@ impl<'a> Reader<'a> {
|
||||
/// follows an object.
|
||||
fn style_marker(&mut self, vals: &[V], hidden: bool) {
|
||||
let code = Self::nums(vals).last().copied().unwrap_or(0.0);
|
||||
let style = vals.iter().rev().find_map(V::text).unwrap_or_default();
|
||||
let style = vals.iter().rev().find_map(|v| self.names.text(v)).unwrap_or_default();
|
||||
let after = std::mem::take(&mut self.after_object);
|
||||
// An object's own state block ends `6 () XW` (a layer's `7 () XW`): an `Xy` before it isn't the layer's.
|
||||
if code == 6.0 {
|
||||
|
||||
@@ -60,7 +60,7 @@ use vectorcraft_effects::is_pathfinder;
|
||||
|
||||
use super::brush::PathBrush;
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::{self, V};
|
||||
use super::obj::{self, Names, V};
|
||||
use super::paint;
|
||||
|
||||
const GLOW: &str = "Adobe Outer Glow"; // brand-ok: the filter's name in the files
|
||||
@@ -386,8 +386,14 @@ fn closes(data: &[u8]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The styles of the section `data` (between its markers), by name.
|
||||
/// [`art_styles_in`], names read in the default encoding.
|
||||
pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
art_styles_in(data, &mut Names::default())
|
||||
}
|
||||
|
||||
/// The styles of the section `data` (between its markers), by name: the names of the styles and of
|
||||
/// the colours, gradients and patterns they paint with read by `names`.
|
||||
pub(super) fn art_styles_in(data: &[u8], names: &mut Names) -> HashMap<String, ArtStyle> {
|
||||
let mut styles = HashMap::new();
|
||||
let mut name: Option<String> = None;
|
||||
let mut filters: Vec<Filter> = vec![];
|
||||
@@ -443,7 +449,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
}
|
||||
b"FillStyle" => style_entry = Some(Kind::Fill),
|
||||
b"StrokeStyle" | b"BlendStyle" => style_entry = Some(Kind::Other),
|
||||
b"Name" if name.is_none() && filters.is_empty() => name = operands.iter().rev().find_map(V::text),
|
||||
b"Name" if name.is_none() && filters.is_empty() => name = operands.iter().rev().find_map(|v| names.text(v)),
|
||||
b"All" | b"[Registration]" | b"Registration" => operands.push(V::Name(obj::text(n))),
|
||||
b"CompoundFilter" => pending = Some(true),
|
||||
b"BasicFilter" => pending = Some(false),
|
||||
@@ -470,7 +476,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
}
|
||||
b"Array" => dict_array = Some(vec![]),
|
||||
b"PluginObject" if in_basic => {
|
||||
let mut texts = operands.iter().rev().filter_map(V::text);
|
||||
let mut texts = operands.iter().rev().filter_map(|v| names.text(v));
|
||||
if let (Some(plugin), Some(name), Some(f)) = (texts.next(), texts.next(), filters.last_mut()) {
|
||||
f.plugin_object = Some((name, plugin));
|
||||
}
|
||||
@@ -541,7 +547,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
}
|
||||
}
|
||||
if let Some(f) = filters.last_mut() {
|
||||
paint_op(f, op, &operands);
|
||||
paint_op(f, op, &operands, names);
|
||||
}
|
||||
if op == "," || op == ";" || op.starts_with('X') || op.starts_with('B') || op.len() <= 2 {
|
||||
operands.clear();
|
||||
@@ -614,13 +620,13 @@ fn block_data(lex: &mut Lexer, encoded: &[u8]) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// A paint operator of a fill's or stroke's style.
|
||||
fn paint_op(f: &mut Filter, op: &str, vals: &[V]) {
|
||||
/// A paint operator of a fill's or stroke's style, the names it gives read by `names`.
|
||||
fn paint_op(f: &mut Filter, op: &str, vals: &[V], names: &mut Names) {
|
||||
let last = vals.iter().rev().find_map(V::num);
|
||||
match op {
|
||||
"Xa" | "k" | "g" | "x" | "Xx" | "Xk" | "Xs" | "Xz" | "XA" | "K" | "G" | "X" | "XX" | "XK" | "XS" | "XZ" | "xa" | "xx" | "xk" | "xs"
|
||||
| "xz" => {
|
||||
let Some((p, _)) = paint::color_op(op, vals) else { return };
|
||||
let Some((p, _)) = paint::color_op_in(op, vals, names) else { return };
|
||||
if op.ends_with(|c: char| c.is_ascii_lowercase()) {
|
||||
f.fill = Some(p);
|
||||
} else {
|
||||
@@ -681,13 +687,13 @@ fn paint_op(f: &mut Filter, op: &str, vals: &[V]) {
|
||||
"Bg" => {
|
||||
f.gradient = true;
|
||||
let at = vals.iter().rposition(|v| matches!(v, V::Str(_)));
|
||||
let name = at.and_then(|i| vals.get(i)).and_then(V::text);
|
||||
let name = at.and_then(|i| vals.get(i)).and_then(|v| names.text(v));
|
||||
let n: Vec<f64> = at.map_or(&[][..], |i| vals.get(i + 1..).unwrap_or_default()).iter().filter_map(V::num).collect();
|
||||
let angle = n.get(2).copied().filter(|a| a.is_finite()).unwrap_or(0.0);
|
||||
f.gradient_of = Some((name, angle));
|
||||
}
|
||||
"p" => {
|
||||
let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None });
|
||||
let name = vals.iter().find(|v| matches!(v, V::Str(_))).and_then(|v| names.text(v));
|
||||
let matrix = vals.iter().rev().find_map(|v| match v {
|
||||
V::Arr(a) => {
|
||||
let n: Vec<f64> = a.iter().filter_map(V::num).collect();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use super::*;
|
||||
use vectorcraft_color::Color;
|
||||
|
||||
fn paint_op(f: &mut Filter, op: &str, vals: &[V]) {
|
||||
super::paint_op(f, op, vals, &mut Names::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_variable_width_stroke_keeps_its_profile() {
|
||||
let data = b"/KnownStyle :\n(Tapered) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
|
||||
@@ -783,7 +783,7 @@ fn pattern_names_are_shown_up_to_their_newline() {
|
||||
// Nor the name of a custom color the program of an EPS gives, or None's.
|
||||
let ps = b"%!PS-Adobe-3.0\n%%CMYKCustomColor: 0 0 1 0 (Outer Ink)\n%%EndComments\n";
|
||||
let data = stream(&format!("{}{}{}", def("Outer Ink\\n1", 8), def("[None]\\n1", 8), layer(&filled("Outer Ink\\n1"))));
|
||||
let doc = super::read_beside(data.as_bytes(), true, ps, false).unwrap().doc;
|
||||
let doc = super::read_beside(data.as_bytes(), true, ps, false, NameEncoding::default()).unwrap().doc;
|
||||
assert_eq!(doc.patterns.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Outer Ink 2", "[None] 2"]);
|
||||
assert!(matches!(doc.swatch("Outer Ink").unwrap().paint, Paint::Solid { .. }));
|
||||
assert!(matches!(fill(&art(&doc)[0]), Paint::Pattern { pattern, .. } if pattern == "Outer Ink 2"));
|
||||
@@ -2510,6 +2510,7 @@ fn ai_variable_library_without_the_layers() {
|
||||
let library = variable_library_data(&sample_variable_sets("variableAndTypes", &[]));
|
||||
let unread = stream(&format!("{library}{}", layer("1 Zz\n")));
|
||||
assert!(read(unread.as_bytes()).is_err());
|
||||
let variables = |data: &[u8]| variables(data, &mut Names::default());
|
||||
let found = variables(unread.as_bytes());
|
||||
assert!(found.present && found.variables.locked && found.skipped == 0);
|
||||
assert_eq!((found.variables.variables.len(), found.variables.datasets.len()), (5, 3));
|
||||
@@ -5724,7 +5725,7 @@ fn artboards_after_what_stops_the_reading() {
|
||||
let array = format!("%_/Array :\n{}{}%_; (ArtboardArray) ,\n", board("one", 0), board("two", 200));
|
||||
// Art in the document's own data that the reader can't follow comes before the artboards.
|
||||
let data = format!("%_/Document :\n%_/Dictionary :\n%_/Array :\n%_/Dictionary :\nLB\n{array}%_; /NotRecorded ,\n%_;\n");
|
||||
let boards: Vec<_> = super::artboards(data.as_bytes()).0.into_iter().map(|(n, r)| (n, r.x0)).collect();
|
||||
let boards: Vec<_> = super::artboards(data.as_bytes(), &mut Names::default()).0.into_iter().map(|(n, r)| (n, r.x0)).collect();
|
||||
assert_eq!(boards, [("one".to_string(), 0.0), ("two".to_string(), 200.0)]);
|
||||
}
|
||||
|
||||
@@ -6348,7 +6349,7 @@ fn a_symbol_library_without_layers_gives_its_symbols() {
|
||||
);
|
||||
let list = "%AI24_BeginSymbolList\n(Tile)\n(Pair)\n(Lost)\n(Dot)\n%AI24_EndSymbolList\n";
|
||||
let body = format!("%AI3_TemplateBox: 100.5 50.5 100.5 50.5\n{styles}{defs}{list}");
|
||||
let read = super::symbols(stream(&body).as_bytes()).expect("the library's symbols");
|
||||
let read = super::symbols(stream(&body).as_bytes(), &mut Names::default()).expect("the library's symbols");
|
||||
let names: Vec<&str> = read.symbols.iter().map(|(s, _)| s.name.as_str()).collect();
|
||||
assert_eq!(names, ["Tile", "Pair", "Dot", "Pic"]);
|
||||
let registrations: Vec<u8> = read.symbols.iter().map(|(_, r)| r.0).collect();
|
||||
@@ -6365,7 +6366,7 @@ fn a_symbol_library_without_layers_gives_its_symbols() {
|
||||
assert_eq!(keys.len(), 1);
|
||||
assert_eq!(read.images.keys().cloned().collect::<Vec<_>>(), keys);
|
||||
// Without symbols there are none to give.
|
||||
assert!(super::symbols(stream(&styles).as_bytes()).is_none());
|
||||
assert!(super::symbols(stream(&styles).as_bytes(), &mut Names::default()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -10228,3 +10229,258 @@ fn copies_of_a_multi_layer_mask_count_toward_the_limit() {
|
||||
let e = read(mask_stream7(&filled).as_bytes()).unwrap_err();
|
||||
assert!(e.contains("objects"), "{e}");
|
||||
}
|
||||
|
||||
// ---- names that aren't UTF-8 ----------------------------------------------------------------
|
||||
|
||||
/// `body` read with its names that aren't UTF-8 in `encoding`.
|
||||
fn read_names(body: &str, encoding: &str) -> Structure {
|
||||
let encoding = NameEncoding::from_label(encoding).unwrap();
|
||||
match super::read_in(stream(body).as_bytes(), true, encoding) {
|
||||
Ok(s) => s,
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The notes on names that weren't UTF-8.
|
||||
fn name_notes(s: &Structure) -> Vec<&str> {
|
||||
s.warnings.iter().map(String::as_str).filter(|w| w.contains("isn't UTF-8") || w.contains("aren't UTF-8")).collect()
|
||||
}
|
||||
|
||||
/// A layer's name in a legacy code page (`(name) Ln`, the bytes as octal escapes) is read in the
|
||||
/// encoding picked, with a note that names it and the encoding; one in UTF-8 is read as it is in
|
||||
/// every encoding, without a note.
|
||||
#[test]
|
||||
fn layer_names_are_read_in_the_legacy_encoding_picked() {
|
||||
let layer_named = |name: &str| {
|
||||
format!(
|
||||
"%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{RED}{}f\nLB\n%AI5_EndLayer--\n",
|
||||
rect_path(0.0, 0.0, 10.0, 10.0)
|
||||
)
|
||||
};
|
||||
for (encoding, bytes, want) in [
|
||||
("windows-949", r"\267\271\300\314\276\356 1", "레이어 1"),
|
||||
("shift_jis", r"\203\214\203C\203\204\201[ 1", "レイヤー 1"),
|
||||
("gbk", r"\315\274\262\343 1", "图层 1"),
|
||||
("big5", r"\271\317\274h 1", "圖層 1"),
|
||||
("macintosh", r"Caf\216", "Café"),
|
||||
("windows-1252", r"Caf\351", "Café"),
|
||||
] {
|
||||
let s = read_names(&layer_named(bytes), encoding);
|
||||
assert_eq!(s.doc.layers[0].name.as_deref(), Some(want), "{encoding}");
|
||||
assert_eq!(
|
||||
name_notes(&s),
|
||||
[format!("the name `{want}` isn't UTF-8 and was read as {encoding}: if it looks wrong, open the file with another legacyNameEncoding")],
|
||||
"{encoding}"
|
||||
);
|
||||
let utf8 = read_names(&layer_named(r"\353\240\210\354\235\264\354\226\264 1"), encoding);
|
||||
assert_eq!(utf8.doc.layers[0].name.as_deref(), Some("레이어 1"), "{encoding}");
|
||||
assert!(name_notes(&utf8).is_empty(), "{encoding}: {:?}", utf8.warnings);
|
||||
}
|
||||
// By default the Korean bytes come in as Windows-1252, as before, now with the note. A comment
|
||||
// in UTF-8 doesn't change how the names are read.
|
||||
let s = read_names(&format!("%%Title: 레이어\n{}", layer_named(r"\267\271\300\314\276\356 1")), "windows-1252");
|
||||
assert_eq!(s.doc.layers[0].name.as_deref(), Some("·¹À̾î 1"));
|
||||
assert_eq!(
|
||||
name_notes(&s),
|
||||
["the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding"]
|
||||
);
|
||||
// Bytes the encoding doesn't define come in as U+FFFD, and the note says so.
|
||||
let s = read_names(&layer_named(r"\267\271\300 1"), "windows-949");
|
||||
assert_eq!(s.doc.layers[0].name.as_deref(), Some("레\u{fffd} 1"));
|
||||
assert_eq!(
|
||||
name_notes(&s),
|
||||
["the name `레\u{fffd} 1` isn't UTF-8, and parts of it aren't valid windows-949 either: open the file with another legacyNameEncoding"]
|
||||
);
|
||||
}
|
||||
|
||||
/// An object's name, an artboard's, a spot colour's and a pattern's are read in the encoding
|
||||
/// picked too, and a fill names the pattern as its definition does, whichever encoding is picked.
|
||||
#[test]
|
||||
fn object_artboard_colour_and_pattern_names_are_read_in_the_encoding_picked() {
|
||||
// 그라 (a pattern and a group), 빨강 (an artboard and a spot colour), 레이어 1 (the layer).
|
||||
let (pattern, red, layer_name) = (r"\261\327\266\363", r"\273\241\260\255", r"\267\271\300\314\276\356 1");
|
||||
let boards = format!(
|
||||
"%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_200 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_({red}) /UnicodeString (Name) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n"
|
||||
);
|
||||
let tile = DOTS.replace("(Dots)", &format!("({pattern})"));
|
||||
let art_ = format!(
|
||||
"0 Ae\nu\n0 1 1 0 ({red}) 0 x\n{}f\n({pattern}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\nU\n%_/ArtDictionary :\n%_({pattern}) /String (AIArtName) ,\n%_;\n9 () XW\n",
|
||||
rect_path(0.0, 0.0, 10.0, 10.0),
|
||||
rect_path(20.0, 0.0, 30.0, 10.0),
|
||||
);
|
||||
let body = format!("{boards}{tile}%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({layer_name}) Ln\n{art_}LB\n%AI5_EndLayer--\n");
|
||||
for (encoding, [pattern, red, layer_name]) in [("windows-949", ["그라", "빨강", "레이어 1"]), ("windows-1252", ["±×¶ó", "»¡°\u{ad}", "·¹À̾î 1"])]
|
||||
{
|
||||
let s = read_names(&body, encoding);
|
||||
let doc = &s.doc;
|
||||
assert_eq!(doc.layers[0].name.as_deref(), Some(layer_name), "{encoding}");
|
||||
assert_eq!(doc.artboards.first().map(|a| a.name.as_str()), Some(red), "{encoding}");
|
||||
let group = &art(doc)[0];
|
||||
assert_eq!(group.name.as_deref(), Some(pattern), "{encoding}");
|
||||
let kids = group.children().unwrap();
|
||||
assert!(matches!(fill(&kids[0]), Paint::Solid { swatch: Some(s), .. } if s == red), "{encoding}: {:?}", fill(&kids[0]));
|
||||
assert!(doc.swatch(red).is_some_and(|s| s.spot), "{encoding}");
|
||||
assert!(matches!(fill(&kids[1]), Paint::Pattern { pattern: p, .. } if p == pattern), "{encoding}: {:?}", fill(&kids[1]));
|
||||
assert!(doc.pattern(pattern).is_some(), "{encoding}");
|
||||
assert!(s.unread.is_empty(), "{encoding}: {:?}", s.unread);
|
||||
assert_eq!(name_notes(&s).len(), 3, "{encoding}: {:?}", s.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Past three names, the notes count the others in one.
|
||||
#[test]
|
||||
fn notes_on_names_that_arent_utf8_are_capped() {
|
||||
let layers: String = (0..6)
|
||||
.map(|i| {
|
||||
format!(
|
||||
"%AI5_BeginLayer\n1 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n(L{i} Caf\\216) Ln\n{RED}{}f\nLB\n%AI5_EndLayer--\n",
|
||||
rect_path(0.0, 0.0, 10.0, 10.0)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let s = read_names(&layers, "macintosh");
|
||||
let names: Vec<_> = s.doc.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
|
||||
assert!(names.iter().all(|n| n.ends_with(" Café")), "{names:?}");
|
||||
let notes = name_notes(&s);
|
||||
assert_eq!(notes.len(), 4, "{notes:?}");
|
||||
assert_eq!(notes[3], "more names that aren't UTF-8 were read as macintosh (3)");
|
||||
}
|
||||
|
||||
/// 빨강 and 그라 in code page 949, as octal escapes.
|
||||
const KO_RED: &str = r"\273\241\260\255";
|
||||
const KO_GRA: &str = r"\261\327\266\363";
|
||||
|
||||
/// A brush's name, the art it names and the brush manager's order are read in the encoding picked:
|
||||
/// the brush finds its art and the order finds the brush, with a note on the brush and on its art.
|
||||
#[test]
|
||||
fn brush_names_are_read_in_the_encoding_picked() {
|
||||
let params = SPRAY.replace("Dot art", KO_GRA);
|
||||
let scatter = format!("%AI8_BeginPluginObject\n({SCATTER})\n({KO_RED})\n({params}) .\n%AI8_EndPluginObject\n");
|
||||
let manager = brush::MANAGER;
|
||||
let order = format!("%AI8_BeginPluginObject\n({manager})\n({manager})\n( {SCATTER}/ {KO_RED}/) .\n%AI8_EndPluginObject\n");
|
||||
let body = format!("{}{order}{scatter}{}", brush_art(KO_GRA, 4.0), layer(""));
|
||||
for (encoding, name) in [("windows-949", "빨강"), ("windows-1252", "»¡°\u{ad}")] {
|
||||
let s = read_names(&body, encoding);
|
||||
let lib = vectorcraft_brush::library(&s.doc);
|
||||
assert_eq!(lib.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(), [name], "{encoding}: {:?}", s.warnings);
|
||||
assert!(matches!(brush_named(&lib, name), vectorcraft_brush::BrushKind::Scatter(_)), "{encoding}");
|
||||
assert_eq!(name_notes(&s).len(), 2, "{encoding}: {:?}", s.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// A symbol's name, the instances that name it and the symbol list are read in the encoding
|
||||
/// picked, with a note on each symbol.
|
||||
#[test]
|
||||
fn symbol_names_are_read_in_the_encoding_picked() {
|
||||
let list = format!("%AI24_BeginSymbolList\n({KO_GRA})\n({KO_RED})\n%AI24_EndSymbolList\n");
|
||||
let defs = symbols(&format!("{}{}{list}", symbol(KO_RED, &dot(), 5), symbol(KO_GRA, &dot(), 5)));
|
||||
let s = read_names(&(defs + &layer(&instance(KO_RED, [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))), "windows-949");
|
||||
let names: Vec<&str> = s.doc.symbols.iter().map(|d| d.name.as_str()).collect();
|
||||
assert_eq!(names, ["그라", "빨강"], "{:?}", s.warnings);
|
||||
assert!(matches!(&art(&s.doc)[0].kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "빨강"), "{:?}", art(&s.doc)[0].kind);
|
||||
assert_eq!(name_notes(&s).len(), 2, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A symbol library read without its layers has notes on the names of the objects in its art.
|
||||
#[test]
|
||||
fn a_symbol_librarys_art_has_notes_on_its_object_names() {
|
||||
let inner = format!("{}{}", dot(), vectorcraft_testkit::ai::named(KO_GRA));
|
||||
let data = stream(&symbols(&symbol(KO_RED, &inner, 5)));
|
||||
let mut names = obj::Names::new(NameEncoding::Korean);
|
||||
assert!(super::symbols(data.as_bytes(), &mut names).is_some());
|
||||
let notes = names.notes();
|
||||
assert_eq!(notes.len(), 2, "{notes:?}");
|
||||
assert!(notes.iter().any(|n| n.contains("`그라`")), "{notes:?}");
|
||||
}
|
||||
|
||||
/// An object without a name of its own takes the one its XML id gives it, read in the encoding
|
||||
/// picked, with a note on that name.
|
||||
#[test]
|
||||
fn an_object_named_by_its_xml_id_is_read_in_the_encoding_picked() {
|
||||
let square = format!("{RED}{}f\n{}", rect_path(0.0, 0.0, 10.0, 10.0), vectorcraft_testkit::ai::named(&format!("{KO_RED}_1")));
|
||||
let s = read_names(&layer(&square), "windows-949");
|
||||
assert_eq!(art(&s.doc)[0].name.as_deref(), Some("빨강 1"), "{:?}", s.warnings);
|
||||
let notes = name_notes(&s);
|
||||
assert_eq!(notes.len(), 1, "{notes:?}");
|
||||
assert!(notes[0].contains("`빨강 1`"), "{notes:?}");
|
||||
}
|
||||
|
||||
/// The strings of a plug-in object that isn't a brush, and the name of a brush that isn't read,
|
||||
/// have no notes.
|
||||
#[test]
|
||||
fn plug_in_objects_that_are_not_read_have_no_name_notes() {
|
||||
let other = format!("%AI8_BeginPluginObject\n(Other Tool)\n({KO_RED})\n(x) .\n%AI8_EndPluginObject\n");
|
||||
let broken = format!("%AI8_BeginPluginObject\n({SCATTER})\n({KO_GRA})\n(not a brush) .\n%AI8_EndPluginObject\n");
|
||||
let s = read_names(&format!("{other}{broken}{}", layer("")), "windows-949");
|
||||
assert!(name_notes(&s).is_empty(), "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A graphic style's name, in the art styles and in the style list, and an object's link to it are
|
||||
/// read in the encoding picked, with a note on the style.
|
||||
#[test]
|
||||
fn graphic_style_names_are_read_in_the_encoding_picked() {
|
||||
let defs = [fill_style(KO_RED, "0 0 0 0 1 0 0 Xa\n")];
|
||||
let s = read_names(&format!("{}{}", styles_and_list(&defs, &[KO_RED]), layer(&styled_square(KO_RED))), "windows-949");
|
||||
assert_eq!(style_names(&s.doc), [("빨강".to_string(), 1)], "{:?}", s.warnings);
|
||||
assert_eq!(art(&s.doc)[0].extras().graphic_style, Some(1), "{:?}", s.warnings);
|
||||
assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A variable's and a data set's names are read in the encoding picked, each with a note, a name
|
||||
/// written across two strings that end inside a character included. The element named after the
|
||||
/// variable still names it, and the data set's text is read in the encoding too, without a note.
|
||||
#[test]
|
||||
fn variable_and_data_set_names_are_read_in_the_encoding_picked() {
|
||||
let split_red = "\\273)\n%_(\\241\\260\\255";
|
||||
let library = vectorcraft_testkit::ai::variable_library(&["QVARQ"], &["QSETQ"]).replace("QVARQ", split_red).replace("QSETQ", KO_GRA);
|
||||
let s = read_names(&format!("{library}{}", layer(&format!("{RED}{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)))), "windows-949");
|
||||
let vars = &s.doc.variables;
|
||||
assert_eq!(vars.variables.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(), ["빨강"], "{:?}", s.warnings);
|
||||
assert_eq!(vars.datasets.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(), ["그라"], "{:?}", s.warnings);
|
||||
assert_eq!(vars.dataset("그라").and_then(|d| d.values.get("빨강")), Some(&vectorcraft_doc::DataValue::Text("그라 빨강".into())));
|
||||
assert_eq!(name_notes(&s).len(), 2, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A binding names its variable by the name read in the encoding picked.
|
||||
#[test]
|
||||
fn a_bound_variables_name_is_read_in_the_encoding_picked() {
|
||||
use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node};
|
||||
let attributes =
|
||||
[("trait", XmlValue::Str("textcontent")), ("varName", XmlValue::Unicode("QVARQ")), ("docRef", XmlValue::Raw("/XMLUIDREF : (one) ;"))];
|
||||
let variable = xml_node(1, "variable", XmlValue::Str(""), &attributes, &[]);
|
||||
let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &[variable])]);
|
||||
let library = variable_library_data(&xml_element("variableSets", &[], &[set])).replace("QVARQ", KO_RED);
|
||||
let square = layer(&format!("{RED}{}f\n{}", rect_path(0.0, 0.0, 10.0, 10.0), named("one")));
|
||||
let s = read_names(&format!("{library}{square}"), "windows-949");
|
||||
assert_eq!(s.bindings, [BindingRef { variable: "빨강".into(), target: Some("one".into()) }], "{:?}", s.warnings);
|
||||
assert_eq!(s.doc.variables.variables.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(), ["빨강"]);
|
||||
assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A custom color of the header is read in the encoding picked, so it is the color an object
|
||||
/// names: one swatch comes in, with a note.
|
||||
#[test]
|
||||
fn a_custom_color_of_the_header_is_read_in_the_encoding_picked() {
|
||||
let body = format!(
|
||||
"%!PS-Adobe-3.0\n%%CMYKCustomColor: 0 1 1 0 ({KO_RED})\n%%EndComments\n{}",
|
||||
layer(&format!("0 1 1 0 ({KO_RED}) 0 x\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0)))
|
||||
);
|
||||
let s = super::read_in(body.as_bytes(), true, NameEncoding::Korean).unwrap();
|
||||
assert_eq!(s.doc.swatches.iter().filter(|w| w.spot).map(|w| w.name.as_str()).collect::<Vec<_>>(), ["빨강"], "{:?}", s.warnings);
|
||||
assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A pattern's name in the version 7 form is read in the encoding picked, with a note, and a fill
|
||||
/// that names it finds it.
|
||||
#[test]
|
||||
fn a_version_7_pattern_name_is_read_in_the_encoding_picked() {
|
||||
let layers = "(0 g) @ (2 2 m 9 2 L 9 9 L f) &\n";
|
||||
let fill_it = format!("({KO_RED}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0));
|
||||
let stream = pattern_stream7(&pattern7(KO_RED, layers), &pattern_layer7("Art", &fill_it));
|
||||
let s = super::read_in(stream.as_bytes(), true, NameEncoding::Korean).unwrap();
|
||||
assert!(s.doc.pattern("빨강").is_some(), "{:?}", s.doc.patterns.iter().map(|p| &p.name).collect::<Vec<_>>());
|
||||
assert!(matches!(fill(&art(&s.doc)[0]), Paint::Pattern { pattern, .. } if pattern == "빨강"), "{:?}", s.warnings);
|
||||
assert_eq!(name_notes(&s).len(), 1, "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ use build::{Frame, MADE, prune_unseen};
|
||||
use compare::{
|
||||
MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, Shares, compare, compared, comparison_render, comparison_samples, differ, differ_in_colour,
|
||||
};
|
||||
pub use layers::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, layered_ai, layered_ai_from};
|
||||
pub use layers::{AiLayers, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard, layered_ai, layered_ai_from};
|
||||
pub(super) use layers::{layered, program_alone};
|
||||
pub(crate) use slots::MAX_TEXT;
|
||||
pub use slots::is_loss;
|
||||
|
||||
@@ -49,7 +49,7 @@ pub(super) struct Frame<'a> {
|
||||
/// (a `.ai` saved without its PDF part), its type is made from the file's text document alone and
|
||||
/// nothing is compared. `outlined`: the page's type was read as outlines, so it has none to put in
|
||||
/// the text objects. `unit`: the size in points of a unit of the page's user space (see
|
||||
/// [`layered_ai`]).
|
||||
/// [`layered_ai`]). `names`: the encoding of names that aren't UTF-8.
|
||||
pub(super) fn build(
|
||||
data: &[u8],
|
||||
visible: Option<&Document>,
|
||||
@@ -57,11 +57,12 @@ pub(super) fn build(
|
||||
outlined: bool,
|
||||
trust: bool,
|
||||
unit: f64,
|
||||
names: ai::NameEncoding,
|
||||
) -> Result<(Document, Vec<String>), String> {
|
||||
// An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art
|
||||
// space, as its PDF part may (#2031).
|
||||
let large_canvas = matches!(page, Page::Art);
|
||||
let structure = if visible.is_some() { ai::read_over_page(data, large_canvas)? } else { ai::read(data, large_canvas)? };
|
||||
let structure = if visible.is_some() { ai::read_over_page(data, large_canvas, names)? } else { ai::read_in(data, large_canvas, names)? };
|
||||
build_from(structure, data, visible, page, outlined, trust, unit)
|
||||
}
|
||||
|
||||
@@ -69,9 +70,10 @@ pub(super) fn build(
|
||||
/// (width and height, either way round) points large in units of `unit` points: the document is
|
||||
/// drawn `unit` times as large when its first artboard, or its art's box, has the page's size in
|
||||
/// those units and not in points, as [`build_from`] decides with a page; else as many times as
|
||||
/// large as its declared large canvas scale when it has the page's size at that scale.
|
||||
pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64) -> Result<(Document, Vec<String>), String> {
|
||||
let structure = ai::read(data, false)?;
|
||||
/// large as its declared large canvas scale when it has the page's size at that scale. `names`: the
|
||||
/// encoding of names that aren't UTF-8.
|
||||
pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec<String>), String> {
|
||||
let structure = ai::read_in(data, false, names)?;
|
||||
let board = structure.artboard.map(|a| structure.to_doc.transform_rect_bbox(a));
|
||||
let art = structure.bbox.map(|[a, b, c, d]| structure.to_doc.transform_rect_bbox(Rect::new(a, b, c, d)));
|
||||
let (w, h) = page;
|
||||
|
||||
@@ -11,10 +11,12 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect
|
||||
/// ([`use_symbols`]) and its variable library ([`use_variables`]) when it could be read, and with
|
||||
/// its layer tree, empty, below the page's layer when that could be read (#2033). A program in the
|
||||
/// legacy format without layers (`illustrator`, see [`program_alone`]) is read with its art on one
|
||||
/// layer; when that can't be used, the file is `visible` as it was.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool) -> Imported {
|
||||
/// layer; when that can't be used, the file is `visible` as it was. `names`: the encoding of names
|
||||
/// that aren't UTF-8.
|
||||
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, names: ai::NameEncoding) -> Imported {
|
||||
let Some(data) = editing_copy(ps) else {
|
||||
let read = illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0).ok());
|
||||
let read =
|
||||
illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0, names).ok());
|
||||
return match read {
|
||||
Some((document, warnings)) => with_page_notes(document, warnings, &visible),
|
||||
None => visible,
|
||||
@@ -24,7 +26,8 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus
|
||||
// can't be used.
|
||||
// Private data kept apart from the program: the program's own header gives the author and
|
||||
// custom colors the private data's header may lack.
|
||||
let read = |d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true) } else { ai::read_over_page(d, true) };
|
||||
let read =
|
||||
|d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true, names) } else { ai::read_over_page(d, true, names) };
|
||||
let (tree, result) = match data.as_deref() {
|
||||
Ok(d) => match read(d) {
|
||||
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)),
|
||||
@@ -37,12 +40,14 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus
|
||||
Err(why) => {
|
||||
let mut v = visible;
|
||||
if let Ok(data) = &data {
|
||||
let mut read = ai::Names::new(names);
|
||||
let have = v.document.patterns.len();
|
||||
let own_palette = use_palette(data, &mut v.document);
|
||||
use_brushes(data, &mut v.document, have, &mut v.warnings);
|
||||
use_styles(data, &mut v.document, true, 1.0, None);
|
||||
use_symbols(data, &mut v.document, own_palette, &mut v.warnings);
|
||||
use_variables(data, &mut v.document, &mut v.warnings);
|
||||
let own_palette = use_palette(data, &mut v.document, &mut read);
|
||||
use_brushes(data, &mut v.document, have, &mut v.warnings, &mut read);
|
||||
use_styles(data, &mut v.document, true, 1.0, None, &mut read);
|
||||
use_symbols(data, &mut v.document, own_palette, &mut v.warnings, &mut read);
|
||||
use_variables(data, &mut v.document, &mut v.warnings, &mut read);
|
||||
add_notes(&mut v.warnings, read.notes());
|
||||
}
|
||||
if tree.is_empty() {
|
||||
v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, in one layer"));
|
||||
@@ -112,12 +117,13 @@ fn layer_tree(nodes: &[Arc<Node>]) -> Vec<Arc<Node>> {
|
||||
/// `unit`: the size in points of a unit of the PDF part's user space, its first page's `UserUnit`,
|
||||
/// by which its art came in scaled. Editing data whose artboard matches the page only in those
|
||||
/// units comes in `unit` times as large, and so does the artboard layout `place_pages` takes from
|
||||
/// it.
|
||||
/// it. Names that aren't UTF-8 are read as Windows-1252 ([`layered_ai_from`]).
|
||||
pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outlined: bool, trust: bool, unit: f64) -> (Document, Vec<String>, bool) {
|
||||
layered_ai_from(AiLayers::decode(private), visible, warnings, outlined, trust, unit)
|
||||
layered_ai_from(AiLayers::decode(private), visible, warnings, outlined, trust, unit, ai::NameEncoding::default())
|
||||
}
|
||||
|
||||
/// [`layered_ai`] of the editing copy `layers`, as far as it was already decoded and read.
|
||||
/// [`layered_ai`] of the editing copy `layers`, as far as it was already decoded and read, with the
|
||||
/// names that aren't UTF-8 read in `names`.
|
||||
pub fn layered_ai_from(
|
||||
layers: AiLayers,
|
||||
visible: Document,
|
||||
@@ -125,6 +131,7 @@ pub fn layered_ai_from(
|
||||
outlined: bool,
|
||||
trust: bool,
|
||||
unit: f64,
|
||||
names: ai::NameEncoding,
|
||||
) -> (Document, Vec<String>, bool) {
|
||||
let unread = |mut all: Vec<String>, why: &str| {
|
||||
all.push(format!("{LAYERS_UNREAD} from its editing data ({why}): they come from its PDF part"));
|
||||
@@ -137,34 +144,41 @@ pub fn layered_ai_from(
|
||||
};
|
||||
if !has_layers(&data) {
|
||||
let (mut visible, mut warnings) = (visible, warnings);
|
||||
let (boards, active) = ai::artboards(&data);
|
||||
let (boards, active) = ai::artboards(&data, &mut ai::Names::default());
|
||||
if board_scale(&boards, &visible, unit).is_some() {
|
||||
visible.opened_artboard = active_page(active, &visible);
|
||||
}
|
||||
let mut read = ai::Names::new(names);
|
||||
let have = visible.patterns.len();
|
||||
let own_palette = use_palette(&data, &mut visible);
|
||||
use_brushes(&data, &mut visible, have, &mut warnings);
|
||||
use_styles(&data, &mut visible, false, unit, Some(&boards));
|
||||
use_symbols(&data, &mut visible, own_palette, &mut warnings);
|
||||
use_variables(&data, &mut visible, &mut warnings);
|
||||
let own_palette = use_palette(&data, &mut visible, &mut read);
|
||||
use_brushes(&data, &mut visible, have, &mut warnings, &mut read);
|
||||
use_styles(&data, &mut visible, false, unit, Some(&boards), &mut read);
|
||||
use_symbols(&data, &mut visible, own_palette, &mut warnings, &mut read);
|
||||
use_variables(&data, &mut visible, &mut warnings, &mut read);
|
||||
add_notes(&mut warnings, read.notes());
|
||||
return (visible, warnings, false);
|
||||
}
|
||||
// The PDF part, its pages placed and named as the editing data's artboards are, when the
|
||||
// layers can't be used (`why`).
|
||||
let from_pdf_part = |mut visible: Document, mut warnings: Vec<String>, why: &str| {
|
||||
let (boards, active) = ai::artboards(&data);
|
||||
place_pages(&boards, active, &mut visible, unit);
|
||||
let mut read = ai::Names::new(names);
|
||||
let mut board_names = ai::Names::new(names);
|
||||
let (boards, active) = ai::artboards(&data, &mut board_names);
|
||||
if place_pages(&boards, active, &mut visible, unit) {
|
||||
read.absorb(board_names);
|
||||
}
|
||||
let have = visible.patterns.len();
|
||||
let own_palette = use_palette(&data, &mut visible);
|
||||
use_brushes(&data, &mut visible, have, &mut warnings);
|
||||
use_styles(&data, &mut visible, false, unit, Some(&boards));
|
||||
let own_palette = use_palette(&data, &mut visible, &mut read);
|
||||
use_brushes(&data, &mut visible, have, &mut warnings, &mut read);
|
||||
use_styles(&data, &mut visible, false, unit, Some(&boards), &mut read);
|
||||
let mut all = unread(warnings, why);
|
||||
use_symbols(&data, &mut visible, own_palette, &mut all);
|
||||
use_variables(&data, &mut visible, &mut all);
|
||||
use_symbols(&data, &mut visible, own_palette, &mut all, &mut read);
|
||||
use_variables(&data, &mut visible, &mut all, &mut read);
|
||||
add_notes(&mut all, read.notes());
|
||||
(visible, all, false)
|
||||
};
|
||||
let built = structure
|
||||
.unwrap_or_else(|| ai::read_over_page(&data, false).map(Box::new))
|
||||
.unwrap_or_else(|| ai::read_over_page(&data, false, names).map(Box::new))
|
||||
.and_then(|structure| build_from(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit));
|
||||
match built {
|
||||
Ok((mut done, notes)) => {
|
||||
@@ -183,8 +197,8 @@ pub fn layered_ai_from(
|
||||
/// reading it, when the file's text document makes all of its type and nothing in its layers needs
|
||||
/// the PDF part (see `build_without_page`). Nothing is compared with the PDF part. Else, as the
|
||||
/// error, what was decoded and read of the editing copy, for [`layered_ai_from`] once the PDF part
|
||||
/// is imported.
|
||||
pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64) -> Result<(Document, Vec<String>), AiLayers> {
|
||||
/// is imported. `names`: the encoding of names that aren't UTF-8.
|
||||
pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec<String>), AiLayers> {
|
||||
let data = match ai::decode_private(private) {
|
||||
Ok(data) => data,
|
||||
Err(why) => return Err(AiLayers { data: Err(why), structure: None }),
|
||||
@@ -193,7 +207,7 @@ pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64) -> Resul
|
||||
return Err(AiLayers { data: Ok(data), structure: None });
|
||||
}
|
||||
// An image past the hard limits is read as an unread box, which the PDF part's art replaces.
|
||||
let structure = match ai::read_over_page(&data, false) {
|
||||
let structure = match ai::read_over_page(&data, false, names) {
|
||||
Ok(structure) => structure,
|
||||
Err(why) => return Err(AiLayers { data: Ok(data), structure: Some(Err(why)) }),
|
||||
};
|
||||
@@ -234,9 +248,10 @@ pub fn ai_opened_artboard(private: &[u8], doc: &Document) -> Option<u32> {
|
||||
/// sets, active data set and lock of the variable library of its editing data `data`, as [`build`]
|
||||
/// gives a document read from the layers, and the note about what of the library wasn't read
|
||||
/// added to `warnings`: its bindings aren't, as the objects they bind aren't the layers' objects.
|
||||
/// Left as it is when the editing data has no library.
|
||||
fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec<String>) {
|
||||
let read = ai::variables(data);
|
||||
/// Left as it is when the editing data has no library. `names` reads the library's names and keeps
|
||||
/// the notes on them.
|
||||
fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec<String>, names: &mut ai::Names) {
|
||||
let read = ai::variables(data, names);
|
||||
warnings.extend(read.note());
|
||||
warnings.extend(ai::bindings_note(read.bindings.len() + read.bindings_skipped));
|
||||
if read.present {
|
||||
@@ -250,9 +265,10 @@ fn use_variables(data: &[u8], doc: &mut Document, warnings: &mut Vec<String>) {
|
||||
/// palette doesn't have follow the palette's swatches. The patterns that the editing data defines come
|
||||
/// in for the palette's pattern swatches, with the images they show; one whose name a pattern of
|
||||
/// the PDF part already has takes a free variant of it ([`ai::palette`]). Left as
|
||||
/// it is when the palette is empty. Whether the palette's swatches came in.
|
||||
fn use_palette(data: &[u8], doc: &mut Document) -> bool {
|
||||
let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns) else { return false };
|
||||
/// it is when the palette is empty. Whether the palette's swatches came in. `names` reads the
|
||||
/// palette's names and keeps the notes on those the swatches take.
|
||||
fn use_palette(data: &[u8], doc: &mut Document, names: &mut ai::Names) -> bool {
|
||||
let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns, names) else { return false };
|
||||
for (key, blob) in images {
|
||||
doc.images.entry(key).or_insert(blob);
|
||||
}
|
||||
@@ -269,15 +285,25 @@ fn use_palette(data: &[u8], doc: &mut Document) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// `notes` added to `warnings`, those it already has left out.
|
||||
fn add_notes(warnings: &mut Vec<String>, notes: Vec<String>) {
|
||||
for n in notes {
|
||||
if !warnings.contains(&n) {
|
||||
warnings.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `doc` (a file read as its page or PDF part, in place of its layers) with the brushes of its
|
||||
/// editing data `data` in place of the default ones, as [`build`] gives a document read from the
|
||||
/// layers, the patterns of the editing data their art paints with (named apart from the first
|
||||
/// `have` patterns of `doc`, those of the page or PDF part, as [`use_palette`] names them, which
|
||||
/// may have added some of them already), and the note for the brushes left out added to
|
||||
/// `warnings`. Left as it is when the editing data has no brushes.
|
||||
fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec<String>) {
|
||||
/// `warnings`. Left as it is when the editing data has no brushes. `names` reads the brushes' names
|
||||
/// and keeps the notes on them.
|
||||
fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec<String>, names: &mut ai::Names) {
|
||||
let page = doc.patterns.get(..have).unwrap_or(&doc.patterns);
|
||||
let Some(ai::Brushes { brushes, patterns, images, note }) = ai::brushes(data, page) else { return };
|
||||
let Some(ai::Brushes { brushes, patterns, images, note }) = ai::brushes(data, page, names) else { return };
|
||||
for (key, blob) in images {
|
||||
doc.images.entry(key).or_insert(blob);
|
||||
}
|
||||
@@ -299,9 +325,10 @@ fn use_brushes(data: &[u8], doc: &mut Document, have: usize, warnings: &mut Vec<
|
||||
/// the size of `doc`'s pages in units of `unit` points, else as many times as large as the data's
|
||||
/// declared large canvas scale when they are the pages' size at that scale ([`board_scale`]).
|
||||
/// `boards`: the editing data's artboards when they were already read. Left as it is when the
|
||||
/// editing data has no style this reads.
|
||||
fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, boards: Option<&[(String, Rect)]>) {
|
||||
let (mut styles, canvas) = ai::graphic_styles(data, large_canvas);
|
||||
/// editing data has no style this reads. `names` reads the styles' names and keeps the notes on
|
||||
/// them.
|
||||
fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, boards: Option<&[(String, Rect)]>, names: &mut ai::Names) {
|
||||
let (mut styles, canvas) = ai::graphic_styles(data, large_canvas, names);
|
||||
if styles.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +337,7 @@ fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, bo
|
||||
let boards = match boards {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
read = ai::artboards(data).0;
|
||||
read = ai::artboards(data, &mut ai::Names::default()).0;
|
||||
&read
|
||||
}
|
||||
};
|
||||
@@ -336,12 +363,12 @@ fn use_styles(data: &[u8], doc: &mut Document, large_canvas: bool, unit: f64, bo
|
||||
/// symbols' art keeps the editing data's units, as in a document read from the layers: when
|
||||
/// [`use_styles`] draws the styles' strokes and effects larger (a file in larger units), linked art
|
||||
/// isn't scaled with them. Left as it is when `doc` has symbols already or the editing data has
|
||||
/// none this reads.
|
||||
fn use_symbols(data: &[u8], doc: &mut Document, own_palette: bool, warnings: &mut Vec<String>) {
|
||||
/// none this reads. `names` reads the symbols' names and keeps the notes on them.
|
||||
fn use_symbols(data: &[u8], doc: &mut Document, own_palette: bool, warnings: &mut Vec<String>, names: &mut ai::Names) {
|
||||
if !doc.symbols.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(ai::Symbols { symbols, images, swatches }) = ai::symbols(data) else { return };
|
||||
let Some(ai::Symbols { symbols, images, swatches }) = ai::symbols(data, names) else { return };
|
||||
for (key, blob) in images {
|
||||
doc.images.entry(key).or_insert(blob);
|
||||
}
|
||||
@@ -414,14 +441,14 @@ fn board_scale(boards: &[(String, Rect)], doc: &Document, unit: f64) -> Option<f
|
||||
/// read (#1068); the page of the artboard at `active` (the active one) is the active artboard. Left
|
||||
/// as they are unless there are as many artboards as pages, each the size of its page in points,
|
||||
/// or, when `unit` is above 1, each the size of its page in units of `unit` points: then the layout
|
||||
/// is drawn `unit` times as large.
|
||||
fn place_pages(boards: &[(String, Rect)], active: Option<usize>, doc: &mut Document, unit: f64) {
|
||||
let Some(k) = board_scale(boards, doc, unit) else { return };
|
||||
/// is drawn `unit` times as large. Whether the pages took the artboards' names.
|
||||
fn place_pages(boards: &[(String, Rect)], active: Option<usize>, doc: &mut Document, unit: f64) -> bool {
|
||||
let Some(k) = board_scale(boards, doc, unit) else { return false };
|
||||
for ((name, _), a) in boards.iter().zip(&mut doc.artboards) {
|
||||
a.name.clone_from(name);
|
||||
}
|
||||
doc.opened_artboard = active_page(active, doc);
|
||||
let [(_, first), _, ..] = boards else { return };
|
||||
let [(_, first), _, ..] = boards else { return true };
|
||||
// Art space (y up) → the document, the first artboard's top left at the origin, as `read` has it,
|
||||
// `k` points a unit.
|
||||
let to_doc = Affine::new([k, 0.0, 0.0, -k, -first.x0 * k, first.y1 * k]);
|
||||
@@ -433,6 +460,7 @@ fn place_pages(boards: &[(String, Rect)], active: Option<usize>, doc: &mut Docum
|
||||
for (a, v) in doc.artboards.iter_mut().zip(&moves) {
|
||||
a.rect = a.rect + *v;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The id of the page of `doc` (a `.ai` read as its PDF part: a page per artboard) for the artboard
|
||||
@@ -464,19 +492,25 @@ fn place_art(layer: &mut Node, pages: &[Rect], moves: &[Vec2]) {
|
||||
/// A `.ai` saved without its PDF part (its pages show only a placeholder) from its editing copy
|
||||
/// alone (`private`, the joined `AIPrivateData` streams) → the document and the import's notes.
|
||||
/// Editing data written on a large canvas comes in at its declared scale: no page holds the size.
|
||||
/// Names that aren't UTF-8 are read as Windows-1252 ([`ai_alone_with`]).
|
||||
pub fn ai_alone(private: &[u8]) -> Result<(Document, Vec<String>), String> {
|
||||
ai_alone_with(private, ai::NameEncoding::default())
|
||||
}
|
||||
|
||||
/// [`ai_alone`], with the names that aren't UTF-8 read in `names`.
|
||||
pub fn ai_alone_with(private: &[u8], names: ai::NameEncoding) -> Result<(Document, Vec<String>), String> {
|
||||
let data = ai::decode_private(private)?;
|
||||
build_from(ai::read(&data, true)?, &data, None, Page::Artboard, false, false, 1.0)
|
||||
build_from(ai::read_in(&data, true, names)?, &data, None, Page::Artboard, false, false, 1.0)
|
||||
}
|
||||
|
||||
/// [`ai_alone`] for a `.ai` whose PDF part can't be read, its first page `page` (width and height)
|
||||
/// points large in units of `unit` points (its `UserUnit`): editing data whose first artboard, or
|
||||
/// whose art's box, has the page's size in those units, and not in points, comes in `unit` times as
|
||||
/// large, as [`layered_ai`] reads it. A page turned a quarter (`/Rotate 90`) is measured either
|
||||
/// way round.
|
||||
pub fn ai_alone_on_page(private: &[u8], page: (f64, f64), unit: f64) -> Result<(Document, Vec<String>), String> {
|
||||
/// way round. `names`: the encoding of names that aren't UTF-8.
|
||||
pub fn ai_alone_on_page(private: &[u8], page: (f64, f64), unit: f64, names: ai::NameEncoding) -> Result<(Document, Vec<String>), String> {
|
||||
let data = ai::decode_private(private)?;
|
||||
build_on_page(&data, page, unit)
|
||||
build_on_page(&data, page, unit, names)
|
||||
}
|
||||
|
||||
/// Does the editing data (or a program in the legacy format) have layers?
|
||||
@@ -499,15 +533,16 @@ fn editing_copy(ps: &[u8]) -> Option<Result<Cow<'_, [u8]>, String>> {
|
||||
/// but whose program has no layers (Illustrator 3 had none; image editors still export their
|
||||
/// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program
|
||||
/// has no layers and isn't in the format: other PostScript isn't read as the format's operators.
|
||||
pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
/// `names`: the encoding of names that aren't UTF-8.
|
||||
pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool, names: ai::NameEncoding) -> Option<Result<(Document, Vec<String>), String>> {
|
||||
if ai::has_eps_data(ps) {
|
||||
return None;
|
||||
}
|
||||
if has_layers(ps) {
|
||||
return Some(build(ps, None, Page::Art, false, false, 1.0));
|
||||
return Some(build(ps, None, Page::Art, false, false, 1.0, names));
|
||||
}
|
||||
let program = illustrator.then(|| one_layer(ps)).flatten()?;
|
||||
Some(build(&program, None, Page::Art, false, false, 1.0))
|
||||
Some(build(&program, None, Page::Art, false, false, 1.0, names))
|
||||
}
|
||||
|
||||
/// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its
|
||||
|
||||
@@ -1411,6 +1411,89 @@ fn an_ai_file_without_layers_keeps_the_gradients_of_its_palette_unlinked() {
|
||||
assert_eq!(g.gradient.stops.len(), 2);
|
||||
}
|
||||
|
||||
/// A file that comes in as its PDF part reads the palette's names that aren't UTF-8 in the encoding
|
||||
/// picked, with the notes on them; a layer name that isn't UTF-8, in layers that aren't used, gets
|
||||
/// none.
|
||||
#[test]
|
||||
fn an_ai_file_that_comes_in_as_its_pdf_part_notes_its_palette_names_in_a_legacy_encoding() {
|
||||
let image = "%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceGray XN\n[ 1 0 0 1 0 100 ] 6000 6000 0 Xh\n\
|
||||
[ 1 0 0 1 0 100 ] 0 0 6000 6000 6000 6000 8 1 0 0 1 0 4 4 0 0\n%%BeginData: 1\rXI\n\u{0}%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN";
|
||||
// 빨강 (red) in code page 949, for the group and its ink; 그라 for the layer.
|
||||
let palette = PALETTE.replace("Inks", r"\273\241\260\255").replace("Red ink", r"\273\241\260\255 ink");
|
||||
let art = native(&format!("{palette}{}", layer(r"\261\327\266\363", true, &format!("{}\n{image}", square(10, 10)))));
|
||||
// An image past 2^25 pixels isn't read here, and the page has no art of its own in its box.
|
||||
let (d, warnings, read) = crate::import::ai::with_image_limits(1 << 26, 1 << 25, 512 << 20, || {
|
||||
crate::layered_ai_from(crate::AiLayers::decode(&private(&art)), pdf_part(10.0, 86.0), vec![], false, true, 1.0, crate::NameEncoding::Korean)
|
||||
});
|
||||
assert!(!read, "the PDF part is used: {warnings:?}");
|
||||
let groups: Vec<&str> = d.swatch_groups.iter().map(|g| g.name.as_str()).collect();
|
||||
assert_eq!(groups, ["빨강"]);
|
||||
assert!(d.swatch("빨강 ink").is_some_and(|s| s.spot));
|
||||
let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8")).collect();
|
||||
assert_eq!(notes.len(), 2, "{warnings:?}");
|
||||
assert!(notes.iter().all(|n| n.contains("windows-949") && !n.contains("그라")), "{notes:?}");
|
||||
}
|
||||
|
||||
/// A library without layers reads the names of its brushes, graphic styles, symbols, variables and
|
||||
/// data sets that aren't UTF-8 in the encoding picked, with a note on each name it brings in.
|
||||
#[test]
|
||||
fn an_ai_library_without_layers_reads_its_names_in_a_legacy_encoding() {
|
||||
// 빨강 and 그라 in code page 949.
|
||||
let (red, gra) = (r"\273\241\260\255", r"\261\327\266\363");
|
||||
let art = format!("%AI8_BeginBrushPattern\n({gra})\n0 0 m\n4 0 L\n4 4 L\nf\n%AI8_EndBrushPattern\n");
|
||||
let spray = format!(
|
||||
"%AI8_BeginPluginObject\n(Adobe Scatter Brush Tool)\n({red})\n(2 /{gra}/ 5 1 0 0 / 0 0.5 1.5 1 1.2 2 0 -0.25 0.25 2 -45 30 1 1 0) .\n%AI8_EndPluginObject\n"
|
||||
);
|
||||
let order = format!(
|
||||
"%AI8_BeginPluginObject\n(Adobe Brush Manager Order)\n(Adobe Brush Manager Order)\n( Adobe Scatter Brush Tool/ {red}/) .\n%AI8_EndPluginObject\n"
|
||||
);
|
||||
let styles =
|
||||
format!("%AI9_BeginArtStyles\n{}%AI9_EndArtStyles\n%AI9_BeginArtStyleList\n({red})\n%AI9_EndArtStyleList\n", stroke_art_style(red, 1.0, ""));
|
||||
let symbols = format!("%AI5_Begin_NonPrinting\n{}%AI5_End_NonPrinting--\n", symbol_def(gra, &symbol_square(-2.0, -2.0, 2.0, 2.0), 1));
|
||||
let variables = vectorcraft_testkit::ai::variable_library(&["QVARQ"], &["QSETQ"]).replace("QVARQ", red).replace("QSETQ", gra);
|
||||
let library = format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%BeginSetup\n{art}{order}{spray}{styles}{symbols}{variables}%%EndSetup\n%%PageTrailer\n%%Trailer\n"
|
||||
);
|
||||
let (d, warnings, read) = crate::layered_ai_from(
|
||||
crate::AiLayers::decode(&private(&library)),
|
||||
pdf_part(10.0, 86.0),
|
||||
vec![],
|
||||
false,
|
||||
true,
|
||||
1.0,
|
||||
crate::NameEncoding::Korean,
|
||||
);
|
||||
assert!(!read, "{warnings:?}");
|
||||
assert_eq!(vectorcraft_brush::library(&d).iter().map(|b| b.name.as_str()).collect::<Vec<_>>(), ["빨강"], "{warnings:?}");
|
||||
assert_eq!(d.graphic_styles.iter().map(|g| g.name.as_str()).collect::<Vec<_>>(), ["빨강"], "{warnings:?}");
|
||||
assert_eq!(d.symbols.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["그라"], "{warnings:?}");
|
||||
assert_eq!(d.variables.variables.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(), ["빨강"], "{warnings:?}");
|
||||
assert_eq!(d.variables.datasets.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["그라"], "{warnings:?}");
|
||||
let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8") || w.contains("aren't UTF-8")).collect();
|
||||
assert_eq!(notes.len(), 2, "{warnings:?}");
|
||||
assert!(notes.iter().all(|n| n.contains("windows-949")), "{notes:?}");
|
||||
}
|
||||
|
||||
/// A pattern of a file without layers that takes a free name beside a pattern of the PDF part has
|
||||
/// a note on the name it was read under.
|
||||
#[test]
|
||||
fn a_renamed_pattern_of_a_file_without_layers_has_a_note_on_its_legacy_name() {
|
||||
// 빨강 in code page 949.
|
||||
let red = r"\273\241\260\255";
|
||||
let tile = format!("%AI3_BeginPattern: ({red})\n({red}) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n");
|
||||
let palette = format!("%AI5_BeginPalette\n0 0 Pb\n({red}) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Tile)\nPc\nPB\n%AI5_EndPalette\n");
|
||||
let art = native(&format!("{tile}{palette}"));
|
||||
let mut part = pdf_part(10.0, 86.0);
|
||||
part.patterns.push(vectorcraft_doc::PatternDef::new("빨강", vec![]));
|
||||
let (d, warnings, read) =
|
||||
crate::layered_ai_from(crate::AiLayers::decode(&private(&art)), part, vec![], false, true, 1.0, crate::NameEncoding::Korean);
|
||||
assert!(!read, "{warnings:?}");
|
||||
assert!(d.pattern("빨강 2").is_some(), "{:?}", d.patterns.iter().map(|p| &p.name).collect::<Vec<_>>());
|
||||
let notes: Vec<&String> = warnings.iter().filter(|w| w.contains("isn't UTF-8")).collect();
|
||||
assert_eq!(notes.len(), 1, "{warnings:?}");
|
||||
assert!(notes[0].contains("`빨강`"), "{notes:?}");
|
||||
}
|
||||
|
||||
/// A `.ai` pattern library (a palette of pattern swatches and no layers) whose PDF part defines no
|
||||
/// pattern gets the patterns its editing data defines with the palette's pattern swatches.
|
||||
#[test]
|
||||
|
||||
@@ -44,8 +44,8 @@ mod tiff;
|
||||
|
||||
pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data};
|
||||
pub use import::{
|
||||
AiLayers, Imported, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, family_style, import, import_trusting,
|
||||
import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
AiLayers, Imported, NameEncoding, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard,
|
||||
family_style, import, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
|
||||
};
|
||||
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai, writes_image_mask};
|
||||
pub use print::{PrintJob, PrintPage, print};
|
||||
|
||||
@@ -44,6 +44,38 @@ fn an_option_no_format_takes_is_an_error() {
|
||||
assert!(!r.is_error, "{r:?}");
|
||||
}
|
||||
|
||||
/// `app.save`, `file.save` and `file.exportForScreens` refuse a key no format takes before they
|
||||
/// write anything.
|
||||
#[test]
|
||||
fn a_save_or_screens_option_no_format_takes_is_an_error() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-mcp-save-options-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut h = Headless::new();
|
||||
h.call("engine.execute", json!({"command": "file.new", "params": {"width": 50, "height": 30}})).unwrap();
|
||||
let pdf = dir.join("a.pdf").to_string_lossy().into_owned();
|
||||
for (params, named) in [
|
||||
(json!({"path": pdf, "noSuchOption": true}), "noSuchOption"),
|
||||
(json!({"path": pdf, "options": {"marks": {"useDocumentBleed": true}}}), "options.marks.useDocumentBleed"),
|
||||
] {
|
||||
let e = h.call("app.save", params.clone()).unwrap_err();
|
||||
assert!(e.contains(named), "app.save: {e}");
|
||||
let e = h.call("engine.execute", json!({"command": "file.save", "params": params})).unwrap_err();
|
||||
assert!(e.contains(named), "file.save: {e}");
|
||||
}
|
||||
let folder = dir.join("screens").to_string_lossy().into_owned();
|
||||
for (params, named) in [
|
||||
(json!({"folder": folder, "noSuchOption": true}), "noSuchOption"),
|
||||
(json!({"folder": folder, "formats": [{"format": "pdf", "marks": {"useDocumentBleed": true}}]}), "formats[0].marks.useDocumentBleed"),
|
||||
] {
|
||||
let e = h.call("engine.execute", json!({"command": "file.exportForScreens", "params": params})).unwrap_err();
|
||||
assert!(e.contains(named), "file.exportForScreens: {e}");
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "nothing was written");
|
||||
h.call("app.save", json!({"path": pdf})).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_export_uses_document_resolution_unless_overridden() {
|
||||
let mut h = Headless::new();
|
||||
|
||||
+110
-13
@@ -174,7 +174,25 @@ fn weight(key: &Key, out: Option<&Out>) -> usize {
|
||||
};
|
||||
anchors(&key.path) + out + (key.params.len() + key.paints.len()) / 50 + 1
|
||||
}
|
||||
static ERRORS: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
|
||||
static ERRORS: Mutex<Option<HashMap<String, Failures>>> = Mutex::new(None);
|
||||
|
||||
/// The errors recorded for a live-effect plug-in, one for each kind of outcome that can clear it.
|
||||
#[derive(Default)]
|
||||
struct Failures {
|
||||
/// From a run of the plug-in.
|
||||
run: Option<String>,
|
||||
/// From checking the swatches and patterns a painted result names against the document.
|
||||
references: Option<String>,
|
||||
/// Was the latest failure a refused reference?
|
||||
references_latest: bool,
|
||||
}
|
||||
|
||||
/// Which of a plug-in's [`Failures`] an outcome replaces.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Origin {
|
||||
Run,
|
||||
References,
|
||||
}
|
||||
|
||||
/// Where an installation ([`crate::Plugin::serial`]) stands as a live effect in this process.
|
||||
enum Runs {
|
||||
@@ -227,21 +245,42 @@ fn end_run(serial: u64, cancelled: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The last error of live-effect plug-in `id` (cleared when it next succeeds).
|
||||
/// The last error of live-effect plug-in `id`: the latest failure that still stands. A run that
|
||||
/// succeeds clears a failed run's error, and a reference check that passes clears a refused
|
||||
/// reference's.
|
||||
pub fn last_error(id: &str) -> Option<String> {
|
||||
ERRORS.lock().unwrap_or_else(|e| e.into_inner()).as_ref()?.get(id).cloned()
|
||||
let errors = ERRORS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let failures = errors.as_ref()?.get(id)?;
|
||||
let (latest, other) = if failures.references_latest { (&failures.references, &failures.run) } else { (&failures.run, &failures.references) };
|
||||
latest.clone().or_else(|| other.clone())
|
||||
}
|
||||
|
||||
/// Records how a run of plug-in `id` went: a failure as its last error (a painted effect's named
|
||||
/// after the plug-in, [`painted::named`]), a success clearing it.
|
||||
fn note(id: &str, plugin: &crate::Plugin, r: std::result::Result<(), &Error>) {
|
||||
/// Records how a run of plug-in `id`, or a check of what its result names, went: a failure as that
|
||||
/// origin's error (a painted effect's named after the plug-in, [`painted::named`]), a success
|
||||
/// clearing that origin's error. A run of an effect that paints nothing clears a refused
|
||||
/// reference's error too.
|
||||
fn note(id: &str, plugin: &crate::Plugin, origin: Origin, r: std::result::Result<(), &Error>) {
|
||||
let message = r.err().map(|e| if plugin.manifest().paint_output { painted::named(&plugin.manifest().name, e) } else { e.to_string() });
|
||||
let mut errors = ERRORS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let errors = errors.get_or_insert_with(HashMap::new);
|
||||
match r {
|
||||
Ok(()) => errors.remove(id),
|
||||
Err(e) if plugin.manifest().paint_output => errors.insert(id.to_string(), painted::named(&plugin.manifest().name, e)),
|
||||
Err(e) => errors.insert(id.to_string(), e.to_string()),
|
||||
};
|
||||
let failures = errors.entry(id.to_string()).or_default();
|
||||
if message.is_some() {
|
||||
failures.references_latest = matches!(origin, Origin::References);
|
||||
}
|
||||
match origin {
|
||||
Origin::Run => {
|
||||
failures.run = message;
|
||||
// Nothing checks the references of an effect that paints nothing, such as one that
|
||||
// replaced a painted effect under this id.
|
||||
if !plugin.manifest().paint_output {
|
||||
failures.references = None;
|
||||
}
|
||||
}
|
||||
Origin::References => failures.references = message,
|
||||
}
|
||||
if failures.run.is_none() && failures.references.is_none() {
|
||||
errors.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does live-effect plug-in `id` return painted pieces (its manifest declares `paintOutput`)?
|
||||
@@ -294,7 +333,7 @@ pub fn apply_painted(
|
||||
fn references_found(id: &str, plugin: &crate::Plugin, pieces: &[Piece], doc: Option<&Document>) -> Option<()> {
|
||||
let checked = painted::check_references(pieces, doc);
|
||||
if doc.is_some() {
|
||||
note(id, plugin, checked.as_ref().map(|_| ()));
|
||||
note(id, plugin, Origin::References, checked.as_ref().map(|_| ()));
|
||||
}
|
||||
checked.ok()
|
||||
}
|
||||
@@ -334,7 +373,7 @@ fn evaluate(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: FillR
|
||||
if cancelled {
|
||||
return None;
|
||||
}
|
||||
note(id, &plugin, r.as_ref().map(|_| ()));
|
||||
note(id, &plugin, Origin::Run, r.as_ref().map(|_| ()));
|
||||
let out = r.ok();
|
||||
let size = weight(&key, out.as_ref());
|
||||
if size > CACHE_ANCHORS / 4 {
|
||||
@@ -377,6 +416,7 @@ fn run(plugin: &crate::Plugin, mut params: Value, path: &PathData, bounds: Rect,
|
||||
mod tests {
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{FillLayer, StrokeLayer};
|
||||
use vectorcraft_geom::{Point, SubPath};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -400,6 +440,63 @@ mod tests {
|
||||
assert_eq!((none.fills, none.strokes, none.item), (vec![], vec![], json!("object")));
|
||||
}
|
||||
|
||||
/// A result taken from the cache passes its reference check without clearing the error of a
|
||||
/// failed run, and the next run that succeeds clears that error.
|
||||
#[test]
|
||||
fn a_passed_reference_check_keeps_a_failed_run_s_error() {
|
||||
let _lock = crate::safe_mode::TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::safe_mode::set_active(None);
|
||||
const ID: &str = "test.effect.lasterror";
|
||||
let manifest = format!(r#"{{"id":"{ID}","name":"Test","version":"1","kind":"effect","api":2,"paintOutput":true,"params":{{}}}}"#);
|
||||
// Traps on a long input.
|
||||
let first = "(if (i32.gt_u (local.get $il) (i32.const 4096)) (then unreachable))";
|
||||
let src = crate::wat::with_imports(&manifest, "", first, r#"{"objects":[]}"#);
|
||||
registry::install(crate::Plugin::load(&wat::parse_str(&src).unwrap(), Limits::default()).unwrap());
|
||||
let doc = Document::new(100.0, 100.0);
|
||||
let line = |n: usize| PathData::single(SubPath::polyline(&(0..n).map(|i| Point::new(i as f64, 0.0)).collect::<Vec<_>>(), false));
|
||||
let draw =
|
||||
|path: &PathData| apply_painted(ID, &json!({}), path, Rect::new(0.0, 0.0, 1.0, 1.0), FillRule::NonZero, Paints::default(), Some(&doc));
|
||||
let short = line(2);
|
||||
assert!(draw(&short).is_some());
|
||||
assert_eq!(last_error(ID), None);
|
||||
assert!(draw(&line(1000)).is_none());
|
||||
let failed = last_error(ID).unwrap();
|
||||
assert!(draw(&short).is_some(), "from the cache");
|
||||
assert_eq!(last_error(ID), Some(failed));
|
||||
assert!(draw(&line(3)).is_some(), "a new run");
|
||||
assert_eq!(last_error(ID), None);
|
||||
registry::remove(ID);
|
||||
}
|
||||
|
||||
/// With a refused reference and a failed run both standing, the latest of the two is reported.
|
||||
#[test]
|
||||
fn the_latest_of_a_refused_reference_and_a_failed_run_is_reported() {
|
||||
let _lock = crate::safe_mode::TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::safe_mode::set_active(None);
|
||||
const ID: &str = "test.effect.latesterror";
|
||||
let manifest = format!(r#"{{"id":"{ID}","name":"Test","version":"1","kind":"effect","api":2,"paintOutput":true,"params":{{}}}}"#);
|
||||
let mut fill = serde_json::to_value(Paint::solid(Color::WHITE)).unwrap();
|
||||
fill["swatch"] = json!("Missing");
|
||||
let square = PathData::single(SubPath::polyline(&[(0.0, 0.0), (9.0, 0.0), (9.0, 9.0)].map(|(x, y)| Point::new(x, y)), true));
|
||||
let output = json!({"objects": [{"path": square, "fills": [fill]}]}).to_string();
|
||||
// Traps on a long input.
|
||||
let first = "(if (i32.gt_u (local.get $il) (i32.const 4096)) (then unreachable))";
|
||||
let src = crate::wat::with_imports(&manifest, "", first, &output);
|
||||
registry::install(crate::Plugin::load(&wat::parse_str(&src).unwrap(), Limits::default()).unwrap());
|
||||
let doc = Document::new(100.0, 100.0);
|
||||
let line = |n: usize| PathData::single(SubPath::polyline(&(0..n).map(|i| Point::new(i as f64, 0.0)).collect::<Vec<_>>(), false));
|
||||
let draw =
|
||||
|path: &PathData| apply_painted(ID, &json!({}), path, Rect::new(0.0, 0.0, 1.0, 1.0), FillRule::NonZero, Paints::default(), Some(&doc));
|
||||
let short = line(2);
|
||||
assert!(draw(&short).is_none());
|
||||
assert!(last_error(ID).unwrap().contains("swatch"));
|
||||
assert!(draw(&line(1000)).is_none());
|
||||
assert!(last_error(ID).unwrap().contains("trapped"));
|
||||
assert!(draw(&short).is_none(), "from the cache, refused again");
|
||||
assert!(last_error(ID).unwrap().contains("swatch"));
|
||||
registry::remove(ID);
|
||||
}
|
||||
|
||||
/// A first run that is cancelled leaves the next run to be the first: the launch marker names
|
||||
/// the plug-in during that one.
|
||||
#[test]
|
||||
|
||||
@@ -470,10 +470,9 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
o.remove("wait");
|
||||
}
|
||||
let method = req.method.as_str();
|
||||
// The key check of `document.export` runs before the file is written.
|
||||
if method == "app.export"
|
||||
&& let Err(e) = crate::menus::check_params(&app.session, "document.export", &p)
|
||||
{
|
||||
// The key check of `document.export` or `document.save` runs before the file is written.
|
||||
let checked = if method == "app.export" { "document.export" } else { "document.save" };
|
||||
if let Err(e) = crate::menus::check_params(&app.session, checked, &p) {
|
||||
return err(e);
|
||||
}
|
||||
wrap(if wait { crate::background::in_foreground(app, |app| save_or_export(app, method, &p)) } else { save_or_export(app, method, &p) })
|
||||
|
||||
@@ -144,6 +144,7 @@ pub use document_setup::open as open_document_setup;
|
||||
pub use effect::open as open_effect_dialog;
|
||||
pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES, shown_choices};
|
||||
pub use export_as::open as open_export_as;
|
||||
pub(crate) use export_for_screens::drop_unread_screen_settings;
|
||||
pub use export_for_screens::open as open_export_for_screens;
|
||||
pub(crate) use export_for_screens::{
|
||||
KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, ScreenSize, pdf};
|
||||
use vectorcraft_engine::cmd::params::{drop_unread_row_options, screen_formats};
|
||||
|
||||
use super::{DialogSpec, form, png_options, svg_options};
|
||||
use crate::state::Dialog;
|
||||
@@ -213,12 +214,29 @@ pub(crate) fn saved_options(app: &VectorcraftApp, saved: &Map<String, Value>, id
|
||||
options.insert("antiAlias".into(), aa.clone());
|
||||
}
|
||||
if let Some(o) = saved.get("settings").and_then(|v| v.get(id)).and_then(Value::as_object) {
|
||||
options.extend(o.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
let mut o = o.clone();
|
||||
drop_unread_row_options(&mut o);
|
||||
options.extend(o);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
/// A row with its size as the text the Scale field shows (`2x`, `100w`, `100h`, `72ppi`).
|
||||
/// Export settings `settings` (as `document.exportSettings` or a preset holds them) without the
|
||||
/// keys the export no longer reads in its rows and per-format settings, and without settings for a
|
||||
/// format it doesn't write.
|
||||
pub(crate) fn drop_unread_screen_settings(settings: &mut Map<String, Value>) {
|
||||
if let Some(rows) = settings.get_mut("formats").and_then(Value::as_array_mut) {
|
||||
rows.iter_mut().filter_map(Value::as_object_mut).for_each(drop_unread_row_options);
|
||||
}
|
||||
if let Some(all) = settings.get_mut("settings").and_then(Value::as_object_mut) {
|
||||
let formats = screen_formats();
|
||||
all.retain(|id, _| formats.contains(&id.as_str()));
|
||||
all.values_mut().filter_map(Value::as_object_mut).for_each(drop_unread_row_options);
|
||||
}
|
||||
}
|
||||
|
||||
/// A row with its size as the text the Scale field shows (`2x`, `100w`, `100h`, `72ppi`), without
|
||||
/// the keys the export no longer reads.
|
||||
fn editable_row(row: &Value) -> Value {
|
||||
let num = |k: &str| row.get(k).and_then(Value::as_f64);
|
||||
let text = match (num("width"), num("height"), num("ppi"), row.get("scale")) {
|
||||
@@ -231,6 +249,7 @@ fn editable_row(row: &Value) -> Value {
|
||||
};
|
||||
let mut r = row.clone();
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
drop_unread_row_options(o);
|
||||
for k in ["width", "height", "ppi"] {
|
||||
o.remove(k);
|
||||
}
|
||||
@@ -887,7 +906,11 @@ pub(super) fn apply_preset(d: &mut Dialog, preset: &Value) {
|
||||
d.fields.insert("formats".into(), json!(rows));
|
||||
d.fields.insert("preset".into(), json!(p));
|
||||
}
|
||||
if let (Some(all), Some(given)) = (d.fields.get_mut("settings").and_then(Value::as_object_mut), st["settings"].as_object()) {
|
||||
// Settings for a format the export doesn't write, and keys it no longer reads, are left out.
|
||||
let mut given = Map::from_iter([("settings".to_string(), st["settings"].clone())]);
|
||||
drop_unread_screen_settings(&mut given);
|
||||
if let (Some(all), Some(given)) = (d.fields.get_mut("settings").and_then(Value::as_object_mut), given.get("settings").and_then(Value::as_object))
|
||||
{
|
||||
for (id, o) in given {
|
||||
match (all.get_mut(id).and_then(Value::as_object_mut), o.as_object()) {
|
||||
(Some(mine), Some(o)) => mine.extend(o.iter().map(|(k, v)| (k.clone(), v.clone()))),
|
||||
|
||||
@@ -112,7 +112,8 @@ pub fn open(app: &mut VectorcraftApp, mode: Mode, path: Option<&str>) {
|
||||
/// only when saving).
|
||||
fn options(d: &Dialog) -> Value {
|
||||
let mode = Mode::of(d);
|
||||
let mut o: Map<String, Value> = d.fields.iter().filter(|(k, _)| !UI_KEYS.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
let mut o: Map<String, Value> =
|
||||
d.fields.iter().filter(|(k, _)| !UI_KEYS.contains(&k.as_str()) && !k.starts_with("__")).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
if mode == Mode::Export {
|
||||
o.remove("preserveEditing");
|
||||
if d.bool("useArtboards") {
|
||||
@@ -379,6 +380,19 @@ mod tests {
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("decimals", 0.0), 1.0);
|
||||
}
|
||||
|
||||
/// A dialog a save panel opened carries the dialog layer's own `__` fields; the save is given
|
||||
/// only the SVG options, so it is not refused for an unknown key.
|
||||
#[test]
|
||||
fn save_as_svg_from_a_save_panel_passes_only_the_options() {
|
||||
let (mut app, written) = app("/tmp/panel.svg");
|
||||
app.run("file.saveAs", Value::Null).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert(crate::dialogs::AFTER_PANEL.into(), json!(true));
|
||||
assert!(options(app.ui.dialog.as_ref().unwrap()).get(crate::dialogs::AFTER_PANEL).is_none());
|
||||
let d = app.ui.dialog.take().unwrap();
|
||||
confirm(&mut app, &d).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert_eq!(written.borrow()[0].0, "/tmp/panel.svg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_artboards_export_one_file_each() {
|
||||
let (mut app, written) = app("/tmp/set.svg");
|
||||
|
||||
@@ -957,6 +957,34 @@ fn saving_an_applied_preset_keeps_its_bleed_and_fill() {
|
||||
assert_eq!((s.get("includeBleed"), s.get("includeArtboardFill")), (Some(&json!(true)), Some(&json!(false))));
|
||||
}
|
||||
|
||||
/// Remembered settings and a preset that hold keys the export no longer reads, in a row, in a
|
||||
/// format's settings and inside an object option, and settings for a format it doesn't write, open
|
||||
/// without them, so the dialog's export is not refused.
|
||||
#[test]
|
||||
fn saved_keys_no_longer_read_are_left_out_of_the_dialog_s_export() {
|
||||
let (mut app, _) = app(1, false);
|
||||
let stale = json!({
|
||||
"formats": [{"format": "png", "scale": "2x", "noSuchOption": 1}, {"format": "pdf", "marks": {"trim": true, "noSuchMark": 1}}],
|
||||
"settings": {"png": {"noSuchSetting": true}, "dxf": {}, "pdf": {"bleed": {"top": 1, "inner": 2}}}
|
||||
});
|
||||
let Value::Object(saved) = stale.clone() else { panic!("an object") };
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).export_settings = saved;
|
||||
let folder = temp_folder("stale");
|
||||
let check = |app: &VectorcraftApp| {
|
||||
let p = export_for_screens::params(app, app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
app.session.check_params("document.exportForScreens", &p).unwrap_or_else(|e| panic!("{e}: {p}"));
|
||||
p
|
||||
};
|
||||
open(&mut app);
|
||||
set(&mut app, "folder", json!(folder.to_string_lossy()));
|
||||
let p = check(&app);
|
||||
assert_eq!(p["formats"][0]["scale"], json!("2x"));
|
||||
assert_eq!(p["formats"][1]["marks"]["trim"], json!(true));
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &json!({"name": "Old", "settings": stale}));
|
||||
check(&app);
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
/// A preset that puts each item in its own folder (`item` or `itemFormat`, which the dialog has no
|
||||
/// control for) keeps that folder mode when it is exported or saved again from the dialog, until
|
||||
/// the sub-folder controls change (in the dialog or by `ui.dialog.set`).
|
||||
|
||||
@@ -4619,6 +4619,9 @@ axis Both И двете
|
||||
@msg its section ending with `%{end}` doesn't end разделът му, завършващ на `%{end}`, няма край
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) четките, които VectorCraft все още не може да прочете, бяха пропуснати от панела „Четки“ ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet обектите с множество запълвания или щрихове, ефекти или четки се появиха във вид на изрисувания си изглед ({_1}): тяхната поява все още не подлежи на редактиране
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding името `{name}` не е в UTF-8 и е прочетено като {label}: ако изглежда грешно, отворете файла с друго legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding името `{name}` не е в UTF-8, а части от него не са валидни и в {label}: отворете файла с друго legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) още имена, които не са в UTF-8, са прочетени като {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиентът „{name}“ не е дефиниран във файла: заменен е с обикновен цвят
|
||||
@msg an image VectorCraft can't read was left out изображение, което VectorCraft не може да разчете, беше пропуснато
|
||||
@msg an image whose data is cut short was left out изображение с непълни данни беше пропуснато
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5015,6 +5015,9 @@ imageTrace Outline Obris
|
||||
@msg its section ending with `%{end}` doesn't end njen odjeljak koji završava s `%{end}` ne završava
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) četkice koje VectorCraft još ne može pročitati izostavljene su iz panela Četkice ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekti s više ispuna ili poteza, efekata ili četkica stigli su kao njihov iscrtani izgled ({_1}): njihov izgled se još ne može uređivati
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding naziv `{name}` nije u UTF-8 i pročitan je kao {label}: ako izgleda pogrešno, otvorite datoteku s drugim legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding naziv `{name}` nije u UTF-8, a dijelovi nisu ispravni ni u {label}: otvorite datoteku s drugim legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) još naziva koji nisu u UTF-8 pročitano je kao {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradijent „{name}” nije definisan u datoteci: umjesto njega koristi se obična boja
|
||||
@msg an image VectorCraft can't read was left out izostavljena je slika koju VectorCraft ne može pročitati
|
||||
@msg an image whose data is cut short was left out izostavljena je slika čiji su podaci skraćeni
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5508,6 +5508,9 @@ digits Malayalam Malaiàlam
|
||||
@msg objects inside a blend, envelope, repeat or Intertwine group can't be attached to a path els objectes dins d’una fusió, un embolcall, una repetició o un grup Intertwine no es poden adjuntar a un traçat
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) els pinzells que VectorCraft encara no pot llegir s'han deixat fora del tauler Pinzells ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet els objectes amb diversos farciments o traços, efectes o pinzells han arribat amb el seu aspecte dibuixat ({_1}): la seva aparença encara no és editable
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding el nom `{name}` no és UTF-8 i s’ha llegit com a {label}: si sembla incorrecte, obriu el fitxer amb un altre legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding el nom `{name}` no és UTF-8, i algunes parts tampoc no són {label} vàlid: obriu el fitxer amb un altre legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) s’han llegit com a {label} més noms que no són UTF-8 ({count})
|
||||
@msg Offset Path: select paths Desplaça el traçat: selecciona traçats
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Desplaça el traçat: el desplaçament no deixa res dels traçats seleccionats (una retracció més gran que les formes)
|
||||
@msg only a dock group above or below others collapses, not one beside another només es plega un grup d’acoblament situat a sobre o a sota d’altres, no un al costat d’un altre
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5090,6 +5090,9 @@ imageTrace Outline Obrys
|
||||
@msg its section ending with `%{end}` doesn't end jeho oddíl končící `%{end}` nemá konec
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) štětce, které VectorCraft zatím neumí načíst, byly vynechány z panelu Štětce ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekty s více výplněmi nebo tahy, efekty či štětci byly načteny jako vykreslený vzhled ({_1}): jejich vzhled zatím nelze upravovat
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding název `{name}` není v UTF-8 a byl načten jako {label}: pokud vypadá špatně, otevřete soubor s jiným legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding název `{name}` není v UTF-8 a jeho části nejsou platné ani v {label}: otevřete soubor s jiným legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) další názvy, které nejsou v UTF-8, byly načteny jako {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it přechod „{name}“ není v souboru definován: zastupuje ho plná barva
|
||||
@msg an image VectorCraft can't read was left out obrázek, který VectorCraft neumí přečíst, byl vynechán
|
||||
@msg graphs come in as plain art, without their data or settings grafy se načtou jako obyčejná grafika, bez svých dat a nastavení
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4707,6 +4707,9 @@ imageTrace Outline Kontur
|
||||
@msg its section ending with `%{end}` doesn't end dens sektion, der ender med `%{end}`, ender ikke
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pensler, som VectorCraft endnu ikke kan læse, blev udeladt af panelet Pensler ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekter med flere fyld eller streger, effekter eller pensler kom ind, som de er tegnet ({_1}): deres udseende er endnu ikke redigerbart
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding navnet `{name}` er ikke UTF-8 og blev læst som {label}: hvis det ser forkert ud, så åbn filen med en anden legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding navnet `{name}` er ikke UTF-8, og dele af det er heller ikke gyldig {label}: åbn filen med en anden legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) flere navne, der ikke er UTF-8, blev læst som {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten »{name}« er ikke defineret i filen: der bruges en flad farve i stedet
|
||||
@msg an image VectorCraft can't read was left out et billede, som VectorCraft ikke kan læse, blev udeladt
|
||||
@msg an image whose data is cut short was left out et billede, hvis data er afkortet, blev udeladt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4698,6 +4698,9 @@ theme Appearance Erscheinungsbild
|
||||
@msg its section ending with `%{end}` doesn't end sein Abschnitt, der mit `%{end}` enden soll, endet nicht
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) Pinsel, die VectorCraft noch nicht lesen kann, wurden im Bedienfeld „Pinsel“ ausgelassen ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet Objekte mit mehreren Flächen oder Konturen, Effekten oder Pinseln wurden so übernommen, wie sie gezeichnet aussehen ({_1}): Ihr Aussehen ist noch nicht bearbeitbar
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding Der Name `{name}` ist kein UTF-8 und wurde als {label} gelesen: Wenn er falsch aussieht, öffnen Sie die Datei mit einer anderen legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding Der Name `{name}` ist kein UTF-8, und Teile davon sind auch kein gültiges {label}: Öffnen Sie die Datei mit einer anderen legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) Weitere Namen, die kein UTF-8 sind, wurden als {label} gelesen ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it der Verlauf „{name}“ ist in der Datei nicht definiert: Eine einfache Farbe steht an seiner Stelle
|
||||
@msg an image VectorCraft can't read was left out ein Bild, das VectorCraft nicht lesen kann, wurde ausgelassen
|
||||
@msg graphs come in as plain art, without their data or settings Diagramme werden als einfache Grafik übernommen, ohne ihre Daten und Einstellungen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4730,6 +4730,9 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg its section ending with `%{end}` doesn't end η ενότητά του που τελειώνει σε `%{end}` δεν τερματίζεται
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) τα πινέλα που το VectorCraft δεν μπορεί ακόμη να διαβάσει παραλείφθηκαν από τον πίνακα Πινέλα ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet τα αντικείμενα με πολλαπλά γεμίσματα, περιγράμματα, εφέ ή πινέλα εισήχθησαν ως απεικονίσεις ({_1}): η εμφάνισή τους δεν είναι ακόμη επεξεργάσιμη
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding το όνομα `{name}` δεν είναι UTF-8 και διαβάστηκε ως {label}: αν φαίνεται λάθος, ανοίξτε το αρχείο με άλλο legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding το όνομα `{name}` δεν είναι UTF-8 και τμήματά του δεν είναι έγκυρα ούτε σε {label}: ανοίξτε το αρχείο με άλλο legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) περισσότερα ονόματα που δεν είναι UTF-8 διαβάστηκαν ως {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it το ντεγκραντέ «{name}» δεν ορίζεται στο αρχείο: χρησιμοποιείται στη θέση του ένα συμπαγές χρώμα
|
||||
@msg an image VectorCraft can't read was left out παραλείφθηκε εικόνα που δεν μπορεί να διαβάσει το VectorCraft
|
||||
@msg graphs come in as plain art, without their data or settings τα γραφήματα εισάγονται ως απλή γραφική, χωρίς τα δεδομένα και τις ρυθμίσεις τους
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4782,6 +4782,9 @@ axis Both Ambos
|
||||
@msg its section ending with `%{end}` doesn't end su sección que termina con `%{end}` no termina
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) los pinceles que VectorCraft aún no puede leer se omitieron del panel Pinceles ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet los objetos con varios rellenos o trazos, efectos o pinceles se importaron con su aspecto dibujado ({_1}): su aspecto aún no se puede editar
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding el nombre `{name}` no está en UTF-8 y se leyó como {label}: si no se ve bien, abre el archivo con otro legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding el nombre `{name}` no está en UTF-8 y partes de él tampoco son {label} válido: abre el archivo con otro legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) se leyeron como {label} más nombres que no están en UTF-8 ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it el degradado “{name}” no está definido en el archivo: lo sustituye un color sólido
|
||||
@msg an image VectorCraft can't read was left out se omitió una imagen que VectorCraft no puede leer
|
||||
@msg graphs come in as plain art, without their data or settings los gráficos se importan como ilustración normal, sin sus datos ni sus ajustes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4992,6 +4992,9 @@ imageTrace Outline Ääriviiva
|
||||
@msg its section ending with `%{end}` doesn't end sen `%{end}`-merkintään päättyvä osio ei pääty
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) siveltimet, joita VectorCraft ei vielä osaa lukea, jätettiin pois Siveltimet-paneelista ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objektit, joilla on useita täyttöjä tai viivoja, tehosteita tai siveltimiä, tuotiin piirretyn näköisinä ({_1}): niiden ulkoasua ei voi vielä muokata
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nimi `{name}` ei ole UTF-8:aa, ja se luettiin koodauksella {label}: jos se näyttää väärältä, avaa tiedosto toisella legacyNameEncoding-arvolla
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nimi `{name}` ei ole UTF-8:aa, eivätkä kaikki sen osat ole kelvollista {label}-koodausta: avaa tiedosto toisella legacyNameEncoding-arvolla
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) muita nimiä, jotka eivät ole UTF-8:aa, luettiin koodauksella {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it liukuväriä “{name}” ei ole määritetty tiedostossa: sen tilalla on tasaväri
|
||||
@msg an image VectorCraft can't read was left out kuva, jota VectorCraft ei voi lukea, jätettiin pois
|
||||
@msg graphs come in as plain art, without their data or settings kaaviot tuodaan tavallisena grafiikkana ilman niiden tietoja ja asetuksia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4769,6 +4769,9 @@ axis Both Les deux
|
||||
@msg its section ending with `%{end}` doesn't end leur section qui se termine par `%{end}` ne se termine pas
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) les formes que VectorCraft ne sait pas encore lire ont été omises du panneau Formes ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet les objets à plusieurs fonds ou contours, effets ou formes ont été importés tels qu’ils sont dessinés ({_1}) : leur aspect n’est pas encore modifiable
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding le nom `{name}` n’est pas en UTF-8 et a été lu en {label} : s’il semble incorrect, ouvrez le fichier avec un autre legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding le nom `{name}` n’est pas en UTF-8, et certaines parties ne sont pas non plus du {label} valide : ouvrez le fichier avec un autre legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) d’autres noms qui ne sont pas en UTF-8 ont été lus en {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it le dégradé « {name} » n’est pas défini dans le fichier : une couleur unie le remplace
|
||||
@msg an image VectorCraft can't read was left out une image que VectorCraft ne peut pas lire a été ignorée
|
||||
@msg graphs come in as plain art, without their data or settings les graphes sont importés comme illustration ordinaire, sans leurs données ni leurs réglages
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -8076,6 +8076,9 @@ theme Appearance अपीयरेंस
|
||||
# Align to Pixel Grid
|
||||
Align to Pixel Grid पिक्सेल ग्रिड से संरेखित करें
|
||||
@msg only paths and compound paths can be aligned to the pixel grid केवल पाथ और कंपाउंड पाथ को पिक्सेल ग्रिड से संरेखित किया जा सकता है
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है और इसे {label} के रूप में पढ़ा गया: यदि यह गलत दिखे, तो फ़ाइल को किसी अन्य legacyNameEncoding से खोलें
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है, और इसके कुछ हिस्से {label} में भी मान्य नहीं हैं: फ़ाइल को किसी अन्य legacyNameEncoding से खोलें
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 में न होने वाले और नाम {label} के रूप में पढ़े गए ({count})
|
||||
Start Lists From Typed Markers टाइप किए गए चिह्नों से सूचियाँ शुरू करें
|
||||
Marquee Selection मार्की चयन
|
||||
Touching स्पर्श करते हुए
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5170,6 +5170,9 @@ imageTrace Outline Körvonal
|
||||
@msg object {id} is not a selected top-level object az objektum nem kijelölt legfelső szintű objektum: {id}
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) a VectorCraft által még nem olvasható ecsetek kimaradtak az Ecsetek panelről ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet a több kitöltésű vagy körvonalú, effektusos vagy ecsetes objektumok a megrajzolt kinézetükkel kerültek be ({_1}): a megjelenésük még nem szerkeszthető
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding a(z) `{name}` név nem UTF-8, ezért {label} kódolással lett beolvasva: ha hibásnak tűnik, nyissa meg a fájlt másik legacyNameEncoding értékkel
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding a(z) `{name}` név nem UTF-8, és egyes részei {label} kódolásban sem érvényesek: nyissa meg a fájlt másik legacyNameEncoding értékkel
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) további nem UTF-8 nevek lettek {label} kódolással beolvasva ({count})
|
||||
@msg Offset Path: select paths Útvonal eltolása: jelölj ki útvonalakat
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Útvonal eltolása: az eltolás után semmi sem marad a kijelölt útvonalakból (a befelé eltolás nagyobb az alakzatoknál)
|
||||
@msg onto must name a floating panel az onto egy lebegő panelt kell megnevezzen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4707,6 +4707,9 @@ imageTrace Outline Kontur
|
||||
@msg its section ending with `%{end}` doesn't end bagian yang berakhir dengan `%{end}` tidak berakhir
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) kuas yang belum dapat dibaca VectorCraft tidak dimasukkan ke panel Kuas ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objek dengan beberapa isian atau goresan, efek, atau kuas masuk sebagai tampilan gambarnya ({_1}): penampilannya belum dapat diedit
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nama `{name}` bukan UTF-8 dan dibaca sebagai {label}: jika terlihat salah, buka file dengan legacyNameEncoding lain
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nama `{name}` bukan UTF-8, dan sebagiannya juga bukan {label} yang valid: buka file dengan legacyNameEncoding lain
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) nama lain yang bukan UTF-8 dibaca sebagai {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradien “{name}” tidak didefinisikan dalam berkas: warna polos dipakai sebagai pengganti
|
||||
@msg an image VectorCraft can't read was left out gambar yang tidak dapat dibaca VectorCraft tidak disertakan
|
||||
@msg an image whose data is cut short was left out gambar yang datanya terpotong tidak disertakan
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2306 and column 5.
|
@@ -4824,6 +4824,9 @@ axis Both Entrambi
|
||||
@msg its section ending with `%{end}` doesn't end la loro sezione che termina con `%{end}` non termina
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) i pennelli che VectorCraft non sa ancora leggere sono stati esclusi dal pannello Pennelli ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet gli oggetti con più riempimenti o tratti, effetti o pennelli sono stati importati come appaiono disegnati ({_1}): il loro aspetto non è ancora modificabile
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding il nome `{name}` non è in UTF-8 ed è stato letto come {label}: se sembra errato, apri il file con un altro legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding il nome `{name}` non è in UTF-8 e alcune sue parti non sono valide nemmeno in {label}: apri il file con un altro legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) altri nomi non in UTF-8 sono stati letti come {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it la sfumatura “{name}” non è definita nel file: la sostituisce un colore pieno
|
||||
@msg an image VectorCraft can't read was left out un'immagine che VectorCraft non può leggere è stata omessa
|
||||
@msg graphs come in as plain art, without their data or settings i grafici vengono importati come grafica semplice, senza i loro dati e le loro impostazioni
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4515,6 +4515,9 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it グラデーション「{name}」はファイル内で定義されていません: 代わりに単色を使用しています
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft がまだ読み込めないブラシ {_1} 個はブラシパネルに追加されませんでした
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 塗りや線が複数ある、または効果やブラシを適用したオブジェクト {_1} 個は見た目のまま読み込まれました: アピアランスはまだ編集できません
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名前 `{name}` は UTF-8 ではないため {label} として読み込みました: 正しくない場合は別の legacyNameEncoding でファイルを開いてください
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名前 `{name}` は UTF-8 ではなく、一部は {label} としても無効です: 別の legacyNameEncoding でファイルを開いてください
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 ではないほかの名前を {label} として読み込みました ({count})
|
||||
@msg patterns nested too deeply are filled with mid-grey 入れ子が深すぎるパターンは中間のグレーで塗りつぶされています
|
||||
@msg shadings of an unknown type were left out 種類が不明なシェーディングは除外されました
|
||||
@msg shadings whose functions are of an unknown type are filled with their middle colour 関数の種類が不明なシェーディングは中間の色で塗りつぶされています
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4516,6 +4516,9 @@ font-technology Variable ცვლადი
|
||||
@msg its section ending with `%{end}` doesn't end მისი სექცია, რომელიც `%{end}`-ით მთავრდება, არ სრულდება
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) ფუნჯები, რომლებსაც VectorCraft ჯერ ვერ კითხულობს, ფუნჯების პანელში არ დაემატა ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet რამდენიმე შევსების ან კონტურის, ეფექტის ან ფუნჯის მქონე ობიექტები შემოვიდა დახატული სახით ({_1}): მათი იერსახე ჯერ არ რედაქტირდება
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding სახელი `{name}` არ არის UTF-8 და წაკითხულია როგორც {label}: თუ არასწორად გამოიყურება, გახსენით ფაილი სხვა legacyNameEncoding-ით
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding სახელი `{name}` არ არის UTF-8 და მისი ნაწილები არც {label}-ში არის სწორი: გახსენით ფაილი სხვა legacyNameEncoding-ით
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) სხვა სახელები, რომლებიც არ არის UTF-8, წაკითხულია როგორც {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it გრადიენტი „{name}“ ფაილში განსაზღვრული არ არის: მის ნაცვლად ერთგვაროვანი ფერია
|
||||
@msg an image VectorCraft can't read was left out გამოსახულება, რომელსაც VectorCraft ვერ კითხულობს, გამოტოვებულია
|
||||
@msg graphs come in as plain art, without their data or settings დიაგრამები შემოდის როგორც ჩვეულებრივი გრაფიკა, მათი მონაცემებისა და პარამეტრების გარეშე
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4442,6 +4442,9 @@ theme Appearance 외관
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 그레이디언트 “{name}”이(가) 파일에 정의되어 있지 않아 단색으로 대체했습니다
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft에서 아직 읽을 수 없는 브러시는 브러시 패널에서 제외되었습니다({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 칠이나 선이 여러 개인 개체, 효과, 브러시가 있는 개체는 그려진 모양({_1})으로 가져왔습니다: 모양새는 아직 편집할 수 없습니다
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 이름 `{name}`은(는) UTF-8이 아니어서 {label}(으)로 읽었습니다: 잘못 보이면 다른 legacyNameEncoding으로 파일을 여세요
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 이름 `{name}`은(는) UTF-8이 아니고 일부는 {label}에서도 올바르지 않습니다: 다른 legacyNameEncoding으로 파일을 여세요
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8이 아닌 다른 이름도 {label}(으)로 읽었습니다({count})
|
||||
@msg patterns nested too deeply are filled with mid-grey 너무 깊게 중첩된 패턴은 중간 회색으로 채웁니다
|
||||
@msg shadings of an unknown type were left out 알 수 없는 종류의 셰이딩은 제외했습니다
|
||||
@msg shadings whose functions are of an unknown type are filled with their middle colour 함수 종류를 알 수 없는 셰이딩은 중간 색상으로 채웁니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4719,6 +4719,9 @@ imageTrace Outline Contour
|
||||
@msg its section ending with `%{end}` doesn't end zijn sectie die op `%{end}` eindigt, eindigt niet
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penselen die VectorCraft nog niet kan lezen, zijn weggelaten uit het deelvenster Penselen ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objecten met meerdere vullingen of lijnen, effecten of penselen zijn als hun getekende uiterlijk binnengekomen ({_1}): hun vormgeving is nog niet bewerkbaar
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding de naam `{name}` is geen UTF-8 en is gelezen als {label}: als die er verkeerd uitziet, open het bestand dan met een andere legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding de naam `{name}` is geen UTF-8, en delen ervan zijn ook geen geldige {label}: open het bestand met een andere legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) meer namen die geen UTF-8 zijn, zijn gelezen als {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it verloop „{name}” is niet gedefinieerd in het bestand: er is een effen kleur voor gebruikt
|
||||
@msg an image VectorCraft can't read was left out een afbeelding die VectorCraft niet kan lezen, is weggelaten
|
||||
@msg an image whose data is cut short was left out een afbeelding waarvan de gegevens zijn afgekapt, is weggelaten
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4916,6 +4916,9 @@ imageTrace Outline Kontur
|
||||
@msg its section ending with `%{end}` doesn't end jego sekcja kończąca się na `%{end}` nie ma końca
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) pędzle, których VectorCraft jeszcze nie odczytuje, pominięto w panelu Pędzle ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet obiekty z kilkoma wypełnieniami lub obrysami, efektami albo pędzlami wczytano jako ich narysowany wygląd ({_1}): ich wyglądu nie można jeszcze edytować
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding nazwa `{name}` nie jest w UTF-8 i została odczytana jako {label}: jeśli wygląda źle, otwórz plik z innym legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding nazwa `{name}` nie jest w UTF-8, a jej części nie są poprawne również w {label}: otwórz plik z innym legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) kolejne nazwy spoza UTF-8 odczytano jako {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradient „{name}” nie jest zdefiniowany w pliku: zastąpiono go jednolitym kolorem
|
||||
@msg an image VectorCraft can't read was left out pominięto obraz, którego VectorCraft nie potrafi odczytać
|
||||
@msg graphs come in as plain art, without their data or settings wykresy są wczytywane jako zwykła grafika, bez ich danych i ustawień
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4351,6 +4351,9 @@ axis Both Ambos
|
||||
@msg its section ending with `%{end}` doesn't end sua seção que termina com `%{end}` não termina
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) os pincéis que o VectorCraft ainda não consegue ler ficaram fora do painel Pincéis ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objetos com vários preenchimentos ou traços, efeitos ou pincéis entraram com a aparência desenhada ({_1}): sua aparência ainda não é editável
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding o nome `{name}` não está em UTF-8 e foi lido como {label}: se parecer errado, abra o arquivo com outro legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding o nome `{name}` não está em UTF-8, e partes dele também não são {label} válido: abra o arquivo com outro legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) mais nomes que não estão em UTF-8 foram lidos como {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it o gradiente “{name}” não está definido no arquivo: uma cor sólida o substitui
|
||||
@msg an image VectorCraft can't read was left out uma imagem que o VectorCraft não consegue ler foi omitida
|
||||
@msg graphs come in as plain art, without their data or settings os gráficos são importados como arte comum, sem seus dados nem configurações
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4818,6 +4818,9 @@ axis Both Оба
|
||||
@msg its section ending with `%{end}` doesn't end раздел, заканчивающийся на `%{end}`, не заканчивается
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) кисти, которые VectorCraft пока не умеет читать, не добавлены в панель «Кисти» ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet объекты с несколькими заливками или обводками, эффектами или кистями импортированы в нарисованном виде ({_1}): их оформление пока нельзя редактировать
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding имя `{name}` не в UTF-8 и прочитано как {label}: если оно выглядит неверно, откройте файл с другим legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding имя `{name}` не в UTF-8, и его части недопустимы и в {label}: откройте файл с другим legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) другие имена не в UTF-8 прочитаны как {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градиент «{name}» не определён в файле: вместо него используется сплошной цвет
|
||||
@msg an image VectorCraft can't read was left out изображение, которое VectorCraft не может прочитать, пропущено
|
||||
@msg graphs come in as plain art, without their data or settings диаграммы импортируются как обычная графика, без их данных и настроек
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4680,6 +4680,9 @@ axis Both Båda
|
||||
@msg its section ending with `%{end}` doesn't end dess avsnitt som slutar med `%{end}` avslutas aldrig
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) penslar som VectorCraft ännu inte kan läsa utelämnades från panelen Penslar ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet objekt med flera fyllningar eller konturer, effekter eller penslar importerades som sitt ritade utseende ({_1}): deras utseende går inte att redigera än
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding namnet `{name}` är inte UTF-8 och lästes som {label}: om det ser fel ut, öppna filen med en annan legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding namnet `{name}` är inte UTF-8, och delar av det är inte heller giltig {label}: öppna filen med en annan legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) fler namn som inte är UTF-8 lästes som {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it gradienten ”{name}” är inte definierad i filen: en enfärgad fyllning används i stället
|
||||
@msg an image VectorCraft can't read was left out en bild som VectorCraft inte kan läsa utelämnades
|
||||
@msg graphs come in as plain art, without their data or settings diagram läses in som vanlig grafik, utan sina data och inställningar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2043,6 +2043,28 @@ fn saved_tracing_preset_errors_are_translated() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The notes on names in a `.ai`'s editing data that aren't UTF-8 are translated in every language
|
||||
/// that translates every message, keeping the name, the encoding and the param.
|
||||
#[test]
|
||||
fn legacy_name_notes_are_translated() {
|
||||
let notes = [
|
||||
"the name `·¹À̾î 1` isn't UTF-8 and was read as windows-1252: if it looks wrong, open the file with another legacyNameEncoding",
|
||||
"the name `레\u{fffd} 1` isn't UTF-8, and parts of it aren't valid windows-949 either: open the file with another legacyNameEncoding",
|
||||
"more names that aren't UTF-8 were read as macintosh (7)",
|
||||
];
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
for note in notes {
|
||||
let shown = message(lang, note);
|
||||
assert_ne!(shown, note, "{code}");
|
||||
assert!(shown.contains("UTF-8"), "{code}: {shown}");
|
||||
}
|
||||
let shown = message(lang, notes[0]);
|
||||
assert!(shown.contains("`·¹À̾î 1`") && shown.contains("windows-1252") && shown.contains("legacyNameEncoding"), "{code}: {shown}");
|
||||
assert!(message(lang, notes[2]).contains("macintosh") && message(lang, notes[2]).contains('7'), "{code}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export Data Sets' refusals of more data sets or variables than Import Data Sets reads are
|
||||
/// translated in every language that translates every message.
|
||||
#[test]
|
||||
|
||||
@@ -4517,6 +4517,9 @@ axis Both Her İkisi
|
||||
@msg its section ending with `%{end}` doesn't end `%{end}` ile biten bölümü bitmiyor
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft'ın henüz okuyamadığı fırçalar Fırçalar panelinin dışında bırakıldı ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet çeşitli dolgulara veya konturlara, efektlere veya fırçalara sahip nesneler çizilmiş görünümleri olarak geldi ({_1}): görünümleri henüz düzenlenemez
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding `{name}` adı UTF-8 değil ve {label} olarak okundu: yanlış görünüyorsa dosyayı başka bir legacyNameEncoding ile açın
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding `{name}` adı UTF-8 değil ve bazı bölümleri {label} olarak da geçerli değil: dosyayı başka bir legacyNameEncoding ile açın
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 olmayan diğer adlar {label} olarak okundu ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it “{name}” degradesi dosyada tanımlı değil: onun yerine düz bir renk geliyor
|
||||
@msg an image VectorCraft can't read was left out VectorCraft'ın okuyamadığı bir resim dışarıda bırakıldı
|
||||
@msg graphs come in as plain art, without their data or settings grafikler, verileri ve ayarları olmadan düz çizim olarak içe aktarılır
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4808,6 +4808,9 @@ axis Both Обидві
|
||||
@msg its section ending with `%{end}` doesn't end розділ, що закінчується на `%{end}`, не закінчується
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) пензлі, які VectorCraft поки не вміє читати, не додано до панелі «Пензлі» ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet об’єкти з кількома заливками або обведеннями, ефектами або пензлями імпортовані в намальованому вигляді ({_1}): їх оформлення поки що не можна редагувати
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding назва `{name}` не в UTF-8 і прочитана як {label}: якщо вона виглядає неправильно, відкрийте файл з іншим legacyNameEncoding
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding назва `{name}` не в UTF-8, і її частини недійсні також у {label}: відкрийте файл з іншим legacyNameEncoding
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) інші назви не в UTF-8 прочитано як {label} ({count})
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it градієнт «{name}» не визначено у файлі: замість нього використовується суцільний колір
|
||||
@msg an image VectorCraft can't read was left out зображення, яке VectorCraft не може прочитати, пропущено
|
||||
@msg graphs come in as plain art, without their data or settings діаграми імпортуються як звичайна графіка, без їхніх даних і налаштувань
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -4825,6 +4825,9 @@ theme Appearance Giao diện
|
||||
@msg object {id} is not a selected top-level object đối tượng {id} không phải là đối tượng cấp cao nhất đã chọn
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) các cọ mà VectorCraft chưa đọc được đã bị bỏ khỏi bảng Cọ ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet đối tượng có nhiều tô hoặc nét, hiệu ứng hoặc cọ đã vào dưới dạng bề ngoài ({_1}): diện mạo chưa thể chỉnh sửa
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding tên `{name}` không phải UTF-8 và đã được đọc theo {label}: nếu trông sai, hãy mở tệp với legacyNameEncoding khác
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding tên `{name}` không phải UTF-8, và một số phần cũng không hợp lệ trong {label}: hãy mở tệp với legacyNameEncoding khác
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) thêm các tên không phải UTF-8 đã được đọc theo {label} ({count})
|
||||
@msg Offset Path: select paths Đường dẫn độ lệch: hãy chọn đường dẫn
|
||||
@msg Offset Path: the offset leaves nothing of the selected paths (an inset larger than the shapes) Đường dẫn độ lệch: độ lệch không để lại gì từ các đường dẫn đã chọn (độ lệch vào trong lớn hơn hình)
|
||||
@msg onto must name a floating panel onto phải đặt tên bảng đang thả nổi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -4228,6 +4228,9 @@ imageTrace Outline 轮廓
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 文件中未定义渐变“{name}”:已改用纯色代替
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚无法读取的画笔未加入“画笔”面板 ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 带有多个填色或描边、效果或画笔的对象已按其绘制外观导入 ({_1}):其外观尚不可编辑
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名称 `{name}` 不是 UTF-8,已按 {label} 读取:如果显示不正确,请使用其他 legacyNameEncoding 打开文件
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名称 `{name}` 不是 UTF-8,且部分内容在 {label} 中也无效:请使用其他 legacyNameEncoding 打开文件
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) 其他非 UTF-8 名称已按 {label} 读取 ({count})
|
||||
@msg patterns nested too deeply are filled with mid-grey 嵌套过深的图案以中灰色填充
|
||||
@msg shadings of an unknown type were left out 已略去未知类型的着色
|
||||
@msg shadings whose functions are of an unknown type are filled with their middle colour 函数类型未知的着色以其中间颜色填充
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2069,6 +2069,9 @@ file-filter Templates 範本
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it 檔案中未定義漸層「{name}」:已改用單色替代
|
||||
@msg brushes VectorCraft can't read yet were left out of the Brushes panel ({_1}) VectorCraft 尚無法讀取的筆刷未加入「筆刷」面板 ({_1})
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet 具有多個填色或筆畫、效果或筆刷的物件已依其繪製外觀匯入 ({_1}):其外觀目前尚無法編輯
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding 名稱 `{name}` 不是 UTF-8,已依 {label} 讀取:若顯示不正確,請以其他 legacyNameEncoding 開啟檔案
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding 名稱 `{name}` 不是 UTF-8,且部分內容在 {label} 中也無效:請以其他 legacyNameEncoding 開啟檔案
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) 其他非 UTF-8 名稱已依 {label} 讀取 ({count})
|
||||
@msg patterns nested too deeply are filled with mid-grey 巢狀層級過深的圖樣以中灰色填滿
|
||||
@msg shadings of an unknown type were left out 已略過類型未知的著色
|
||||
@msg shadings whose functions are of an unknown type are filled with their middle colour 函數類型未知的著色以其中間顏色填滿
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -29,7 +29,13 @@ pub(crate) fn opt_bool(p: &Value, key: &str) -> Result<Option<bool>, String> {
|
||||
/// `params` has only keys the params description of engine command `id` names, or the parameter
|
||||
/// error naming the one it doesn't (#1442). The UI's own commands aren't checked yet: their
|
||||
/// descriptions haven't been audited against what they read.
|
||||
/// `file.save` and `file.exportForScreens` take the params of the engine command they run.
|
||||
pub fn check_params(session: &vectorcraft_engine::Session, id: &str, params: &Value) -> Result<(), String> {
|
||||
let id = match id {
|
||||
"file.save" => "document.save",
|
||||
"file.exportForScreens" => "document.exportForScreens",
|
||||
id => id,
|
||||
};
|
||||
if UI_COMMANDS.iter().any(|c| c.0 == id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ use vectorcraft_engine::cmd::fileio::{FORMATS_3D, SCREEN_SETTINGS_KEYS, format_3
|
||||
|
||||
use super::{pstate, set_pstate};
|
||||
use crate::dialogs::{
|
||||
EXPORT_FOR_SCREENS, ScreenPresetPick, open_export_for_screens_assets, screen_export_preset, screen_formats, screen_import_presets,
|
||||
screen_preset_list, screen_saved_options, screen_saved_rows,
|
||||
EXPORT_FOR_SCREENS, ScreenPresetPick, drop_unread_screen_settings, open_export_for_screens_assets, screen_export_preset, screen_formats,
|
||||
screen_import_presets, screen_preset_list, screen_saved_options, screen_saved_rows,
|
||||
};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
@@ -240,7 +240,10 @@ fn export_now(app: &mut VectorcraftApp, ids: Vec<u64>, files_each: usize) {
|
||||
let saved = &st.doc.export_settings;
|
||||
let mut settings = saved.get("settings").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
settings.insert("tiff".into(), Value::Object(screen_saved_options(app, saved, "tiff")));
|
||||
p["settings"] = Value::Object(settings);
|
||||
// Without the keys the export no longer reads, which it would refuse.
|
||||
let mut given = serde_json::Map::from_iter([("settings".to_string(), Value::Object(settings))]);
|
||||
drop_unread_screen_settings(&mut given);
|
||||
p["settings"] = given.remove("settings").unwrap_or_default();
|
||||
}
|
||||
if io::is_web(app) {
|
||||
p["zip"] = json!(ids.len() * files_each > 1);
|
||||
@@ -532,6 +535,7 @@ fn preset_settings(app: &VectorcraftApp, ctx: &egui::Context, doc: u64) -> Value
|
||||
settings.remove("preset");
|
||||
}
|
||||
settings.extend(shared);
|
||||
drop_unread_screen_settings(&mut settings);
|
||||
Value::Object(settings)
|
||||
}
|
||||
|
||||
@@ -554,6 +558,7 @@ fn shared_settings(app: &VectorcraftApp) -> Value {
|
||||
settings.insert("formats".into(), json!(rows));
|
||||
}
|
||||
}
|
||||
drop_unread_screen_settings(&mut settings);
|
||||
Value::Object(settings)
|
||||
}
|
||||
|
||||
|
||||
@@ -598,6 +598,29 @@ fn tiff_photometric(bytes: &[u8]) -> u16 {
|
||||
short(entry + 8)
|
||||
}
|
||||
|
||||
/// Remembered format settings that hold keys the export no longer reads, and settings for a format
|
||||
/// it doesn't write, are left out of the panel's export, so it is not refused.
|
||||
#[test]
|
||||
fn export_leaves_out_remembered_keys_no_longer_read() {
|
||||
let (mut app, log, [a, _]) = app(true);
|
||||
app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap();
|
||||
let serde_json::Value::Object(saved) = json!({
|
||||
"formats": [{"format": "tiff", "scale": "1x", "noSuchOption": 1}],
|
||||
"settings": {"png": {"noSuchSetting": true}, "dxf": {}, "tiff": {"lzw": false, "noSuchSetting": 1}}
|
||||
}) else {
|
||||
panic!("an object")
|
||||
};
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).export_settings = saved;
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export")), false);
|
||||
let log = log.borrow();
|
||||
let [(name, _)] = log.as_slice() else { panic!("expected one TIFF: {}", app.ui.status) };
|
||||
assert_eq!(name, "Badge.tif");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_cmyk_defaults_match_screens_and_preserve_explicit_rgb() {
|
||||
for rgb_override in [false, true] {
|
||||
|
||||
@@ -181,6 +181,40 @@ fn control_export_refuses_an_option_no_format_takes() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `app.save`, and `file.save` and `file.exportForScreens` run as commands, refuse a key no save
|
||||
/// format or no format takes before anything is written.
|
||||
#[test]
|
||||
fn control_save_refuses_an_option_no_format_takes() {
|
||||
let (mut app, tx, base) = confined("save-options");
|
||||
let r = request(&mut app, &tx, "app.open", json!({"path": s(base.join("in/a.svg"))}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let path = base.join("out/a.pdf");
|
||||
let refused = |r: &Value| r["error"].as_str().is_some_and(|e| e.contains("unknown parameter"));
|
||||
for params in [json!({"noSuchOption": true}), json!({"options": {"marks": {"useDocumentBleed": true}}})] {
|
||||
let mut params = params;
|
||||
params["path"] = json!(s(path.clone()));
|
||||
let r = request(&mut app, &tx, "engine.execute", json!({"command": "file.save", "params": params.clone()}));
|
||||
assert!(refused(&r), "{r}");
|
||||
assert!(!path.exists());
|
||||
params["wait"] = json!(true);
|
||||
let r = request(&mut app, &tx, "app.save", params);
|
||||
assert!(refused(&r), "{r}");
|
||||
assert!(!path.exists());
|
||||
}
|
||||
let folder = base.join("out/screens");
|
||||
for params in [json!({"noSuchOption": true}), json!({"formats": [{"format": "png", "noSuchOption": 1}]})] {
|
||||
let mut params = params;
|
||||
params["folder"] = json!(s(folder.clone()));
|
||||
let r = request(&mut app, &tx, "engine.execute", json!({"command": "file.exportForScreens", "params": params}));
|
||||
assert!(refused(&r), "{r}");
|
||||
assert!(!folder.exists());
|
||||
}
|
||||
let r = request(&mut app, &tx, "app.save", json!({"path": s(path.clone()), "wait": true}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(path.exists());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// An action set another app saved (`.aia`) is read only from inside the read root, whichever way a
|
||||
/// control request names it: `actionSet.read` and Load Actions….
|
||||
#[test]
|
||||
|
||||
@@ -107,7 +107,7 @@ the two can still win that race.
|
||||
| `ui.render` | `{path?, scale?, artboard?, data?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
| `ui.resize` / `ui.focus` | | |
|
||||
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?, params?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. With `importer` (an importer plug-in's format id, `plugin.<id>`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension, with the importer's parameters `params` (their defaults when omitted; no dialog asks for them). Without `importer`, `params` go to the importer that opens the file, and a built-in format does not read them. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. It refuses a key that no format takes, and an unknown key inside a PDF or EPS option object, before it writes anything (`docs/mcp.md`, Parameter keys). `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
|
||||
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?, params?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. With `importer` (an importer plug-in's format id, `plugin.<id>`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension, with the importer's parameters `params` (their defaults when omitted; no dialog asks for them). Without `importer`, `params` go to the importer that opens the file, and a built-in format does not read them. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. It refuses a key that no format takes, and an unknown key inside a PDF or EPS option object, before it writes anything (`docs/mcp.md`, Parameter keys). `app.save` refuses a key that no save format takes, beside the path or in `options`, the same way. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
|
||||
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
|
||||
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
|
||||
|
||||
@@ -266,6 +266,8 @@ A Scatter brush in the editing data whose last 15 numbers start with 1 comes int
|
||||
|
||||
An object in a pattern's art or in a brush's art takes the file's art styles in the same way as an object on a layer: with a style VectorCraft reads, it comes in as itself with its effects live, not as the look it draws. An object in a pattern's art with a style in the file's style list keeps that graphic style's id. An art styles section inside a symbol's, a pattern's or a brush's art is skipped.
|
||||
|
||||
Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets) are read as Windows-1252 unless `legacyNameEncoding` (`convert --legacy-name-encoding`) picks `macintosh`, `windows-949`, `shift_jis`, `gbk` or `big5`; the open result's warnings list the first three such names as read and count the rest. The text values of data sets are read in the same encoding.
|
||||
|
||||
## Format by format
|
||||
|
||||
| Format | Illustrator | Ours: open/place | Ours: save/export | Fidelity and tests |
|
||||
|
||||
+34
-7
@@ -464,7 +464,7 @@ opens the picker through `run_command` with the remote backend.
|
||||
| `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. |
|
||||
| `open_file` | `{path, importer?, params?}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), HEIC/HEIF (`.heic`/`.heif`), JPEG XL (`.jxl`), PSD (`.psd`/`.pdd`)/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image; a JPEG XL file as its first frame). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). A file of an extension only an installed importer plug-in lists opens with that importer ([Plug-ins](#plug-ins)); `importer: "plugin.<id>"` opens the file with the importer it names, which must list the file's extension, also one a built-in format reads (in the app, as File › Open With does), and `params` gives that importer's parameters (their defaults when omitted). `run_command document.formats` lists the formats. |
|
||||
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warning for a top-level `compress`, `version`, `preview` or `precision`. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
|
||||
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
|
||||
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. A key in `options` that no save format takes is an error, and nothing is written (see Parameter keys). The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
|
||||
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
|
||||
| `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`; SVG export writes type on a path with any effect other than `rainbow` as outlines, with a warning); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. |
|
||||
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
|
||||
@@ -877,6 +877,12 @@ an EPS as its page, a `.ai` file from its PDF content (a `.ai` saved without PDF
|
||||
{"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/label.eps","editingData":false}}}
|
||||
```
|
||||
|
||||
Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles,
|
||||
swatches, brushes, symbols, variables and data sets, and the text values of data sets) are read as Windows-1252 unless
|
||||
`document.open` takes `legacyNameEncoding`: `macintosh`, `windows-949` (also `euc-kr`), `shift_jis`, `gbk` or `big5`. An
|
||||
unknown label is refused. The `warnings` of the result name the first three such names as read and count the rest.
|
||||
`vectorcraft-cli convert --legacy-name-encoding ENC` passes it on.
|
||||
|
||||
Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based),
|
||||
`cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets;
|
||||
with `trim` the bleed box round it becomes the document's bleed, and a PDF-compatible `.ai` or a PDF saved with its editing data opens on its trim boxes for
|
||||
@@ -1166,7 +1172,7 @@ refused before the command runs:
|
||||
→ invalid parameters for `file.new`: unknown parameter `widht`; did you mean `width`?
|
||||
```
|
||||
|
||||
Only the top level is checked, except for the export commands below: the keys of an object inside a value
|
||||
Only the top level is checked, except for the export and save commands below: the keys of an object inside a value
|
||||
(`artboardLayout: {…}`) are the command's to read. Another exception is an effect's `params`: `effect.apply` (and `appearance.addEffect`) and `effect.setParams`
|
||||
check them against the effect's own entry in `effect.list`, its `defaults` and the keys its `params` text names. A key
|
||||
the effect doesn't take, or a value of another type than its default (a string for a number, a number for a switch),
|
||||
@@ -1205,8 +1211,8 @@ A number outside the range the effect documents is still stored as given and cla
|
||||
plug-in effect's parameters are resolved against its manifest instead. `effect.setParams` as a step of
|
||||
`command.batch` is checked when the step runs, against the effects the steps before it added, and the batch then
|
||||
fails with no change. A description with a `…` entry takes more keys than it lists: those of the command it names
|
||||
(`…settings as file.new`), or, except for the export commands below, any key when it names none, as `document.save` does for the options
|
||||
of the format they write (one object can carry several formats' options). A command that takes its own reply back
|
||||
(`…settings as file.new`), or, except for the export and save commands below, any key when it names none (one
|
||||
object can carry several formats' options). A command that takes its own reply back
|
||||
(`document.setup`, `text.areaOptions`) says so the same way.
|
||||
|
||||
Saved actions aren't checked this way. When the Actions panel plays an action (Play at any speed, an action's function
|
||||
@@ -1219,15 +1225,36 @@ and `file.export` refuse a key that no format takes, and ignore a key that only
|
||||
(`{"format": "pdf", "quality": 80}` writes the PDF). `document.exportPdf`, `document.pdfSettings` and `pdf.preset.save`
|
||||
refuse a key that PDF doesn't take, `document.exportEps` one that EPS doesn't take and `document.exportDxf` one that DXF
|
||||
doesn't take. Each of them refuses an unknown key inside an option object it takes (`marks`, `bleed`, `compression`,
|
||||
`output`, `advanced`, `security`, `flattener`), naming its path:
|
||||
`output`, `advanced`, `security`, `flattener`, `svg`), naming its path:
|
||||
|
||||
```json
|
||||
{"id": "document.export", "params": {"format": "pdf", "marks": {"useDocumentBleed": true}}}
|
||||
→ invalid parameters for `document.export`: unknown parameter `marks.useDocumentBleed`; `marks` takes: …
|
||||
```
|
||||
|
||||
A saved PDF preset that holds a key that is no longer read keeps loading. `document.save`, `file.saveAs`, `file.saveCopy`
|
||||
and `document.exportForScreens` don't apply this check.
|
||||
The save commands limit them the same way. `document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`,
|
||||
`app.save` and `file.save` refuse a key that no save format takes, given beside the path or in `options`, and an
|
||||
unknown key inside a PDF option object or the `svg` object, also when the file is not SVG. A save still leaves out a key
|
||||
that only another save format takes. It warns when that key is given in `options` or is one of `compress`, `version`,
|
||||
`preview` and `precision`:
|
||||
|
||||
```json
|
||||
{"id": "document.save", "params": {"path": "a.pdf", "options": {"noSuchOption": true}}}
|
||||
→ invalid parameters for `document.save`: unknown parameter `options.noSuchOption`; no save format takes it (see file.formatOptions)
|
||||
```
|
||||
|
||||
`document.exportForScreens` and `file.exportForScreens` refuse a key the command doesn't take. In each row of `formats`
|
||||
and in the options `settings` gives a format, they refuse a key that no format takes, as `document.export` does, and an
|
||||
unknown key inside a PDF, EPS or SVG option object; `settings` takes only the formats the command writes. A row and a
|
||||
format's settings also take `includeArtboardFill`, `richBlack`, `params` (an exporter plug-in's own) and
|
||||
`security.passwordRequired` (which remembered settings hold where PDF passwords were left out), and `settings`
|
||||
takes an exporter plug-in's format. The error names the row (`formats[1].marks.useDocumentBleed`).
|
||||
`document.exportPresets.save` checks its `settings`, with the rows and settings in it, the same way, and `assets.export`
|
||||
and `assets.settings.set` check the rows and settings they are given.
|
||||
|
||||
A saved PDF preset or Export for Screens preset that holds a key that is no longer read keeps loading. The Export for
|
||||
Screens dialog opens remembered settings and presets without such keys, and the Asset Export panel leaves them out of
|
||||
the settings it sends, so their export is not refused.
|
||||
|
||||
An unknown command id is matched the same way as an unknown parameter key: the error names the nearest command id when
|
||||
one is near, ignoring case. A step of `command.batch` or `action.batch` gets the same suggestion in its step error:
|
||||
|
||||
+3
-1
@@ -245,7 +245,9 @@ Every run gets a **fresh instance**, so no state survives between calls except w
|
||||
[settings and cache](#settings-and-cache). Traps, exhausted budgets, malformed
|
||||
modules, bad manifests, out-of-range pointers, negative return codes and invalid output all become errors, and the
|
||||
document is left untouched: a filter's output is checked completely before one edit applies it. A live effect that
|
||||
fails draws the object's geometry unchanged; `plugin.info` reports why (`lastError`).
|
||||
fails draws the object's geometry unchanged; `plugin.info` reports why (`lastError`). A run that succeeds clears the
|
||||
error of a failed run. A check that finds every swatch and pattern a painted result names clears only the error of a
|
||||
refused swatch or pattern.
|
||||
|
||||
wasmi is built with its *portable* (loop) dispatcher: the default tail-call dispatcher relies on LLVM
|
||||
sibling-call optimisation, and when that doesn't happen a long-running module overflows the host stack, which aborts
|
||||
|
||||
@@ -745,6 +745,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).
|
||||
- The errors of the parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
|
||||
- Export commands run through MCP, the control channel or the CLI refuse a key that no format takes, and a command that writes one format (`document.exportPdf`, `document.exportEps`, `document.exportDxf`) or takes one format's options (`document.pdfSettings`, `pdf.preset.save`) refuses a key that format doesn't take. An unknown key inside `marks`, `bleed`, `compression`, `output`, `advanced`, `security` or `flattener` is refused by its path.
|
||||
- Save commands run through MCP, the control channel or the CLI (`document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`, `app.save`) refuse a key that no save format takes, beside the path or in `options`. Export for Screens refuses a key it doesn't take, and a key that no format takes in a format row or in a format's settings, by its path (`formats[1].marks.useDocumentBleed`); Export Assets (`assets.export`, `assets.settings.set`) checks the rows and settings it is given the same way.
|
||||
- Commands that read their `ids` (or `slices`) list themselves refuse a list with an entry that is not a non-negative integer, and a value that is not a list, and change nothing; they no longer drop the entry and act on the rest (Copy Appearance From, the Edit Colors commands and Overprint Black, Find and Replace, `layer.highlight`, `layer.move`, `library.add`, the Links panel commands, `object.transform`, Make Pattern, the Repeat commands, the slice commands, Join, Average and Join, `plugin.run`, `symbol.edit`, Make Opacity Mask; `tests::ids_without_selection::an_ids_entry_that_is_not_an_id_is_refused_and_changes_nothing`). The error for an entry of an object id list names the key, the entry and its value ("`ids` entry 1 is true, not a non-negative integer object id"), with a row in every catalog that has the other object id errors.
|
||||
- The errors of Enter Isolation Mode (`object.isolate`) for a type object, for hidden or locked art and for a call with no `id` while nothing is selected ("missing id", which other commands that take an `id` also give), and the error of the Layers panel's target circle (`layer.target`) for an object outside the isolated group, show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch.
|
||||
- An EPS or PostScript file that stops on a PostScript error gives the error's name as PostScript spells it (`typecheck`, `rangecheck` and the other names the reader raises, except `undefined`, whose message names the unknown name) followed by what it means, in the warning after opening and in the error when the file can't be opened. The status bar and the open-error dialog show the meaning in the interface language: each meaning has a row in every catalog (`postscript_error_meanings_are_translated`).
|
||||
|
||||
Reference in New Issue
Block a user