mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 11:54:03 +00:00
Merge pull request #5158 from dotCooCoo/fix/ai-os23-2831
.ai import: names that aren't UTF-8 can be read in a legacy code page
This commit is contained in:
Generated
+1
@@ -6511,6 +6511,7 @@ name = "vectorcraft-eps"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"encoding_rs",
|
||||
"fax",
|
||||
"flate2",
|
||||
"hayro-ccitt",
|
||||
|
||||
@@ -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]
|
||||
@@ -101,7 +101,7 @@ USAGE:
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--legacy-name-encoding ENC] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
|
||||
formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects
|
||||
resolution; other formats leave them out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
|
||||
@@ -114,7 +114,10 @@ USAGE:
|
||||
IN left things out (hidden text, art or layers it could not read; the notes say which) unless
|
||||
--replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy
|
||||
even where that draws differently from its PDF part or page (which can be older), as long as
|
||||
it can be read. A PDF OUT omits the embedded native document unless --preserve-editing is
|
||||
it can be read. --legacy-name-encoding reads the names in a .ai's or an EPS's editing data
|
||||
that aren't UTF-8 (layers, objects, artboards, colors, gradients, patterns, graphic styles,
|
||||
swatches, brushes, symbols, variables and data sets, and data sets' text values) in
|
||||
windows-1252 (the default), macintosh, windows-949, shift_jis, gbk or big5. A PDF OUT omits the embedded native document unless --preserve-editing is
|
||||
given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the
|
||||
attachment is written the result's warnings say so (it is left out, with its own warning, for
|
||||
--artboard or a --range without every artboard).
|
||||
@@ -302,8 +305,20 @@ fn commands() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn convert(args: &[String]) -> Result<(), String> {
|
||||
let valued =
|
||||
["--scale", "-s", "--ppi", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"];
|
||||
let valued = [
|
||||
"--scale",
|
||||
"-s",
|
||||
"--ppi",
|
||||
"--artboard",
|
||||
"-a",
|
||||
"--range",
|
||||
"-r",
|
||||
"--decimals",
|
||||
"--ai-version",
|
||||
"--hidden-objects",
|
||||
"--transparent-images",
|
||||
"--legacy-name-encoding",
|
||||
];
|
||||
if help_requested(args, &valued) {
|
||||
return help();
|
||||
}
|
||||
@@ -312,6 +327,9 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
(None::<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![];
|
||||
@@ -334,6 +352,9 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
"--replace-lossy" => replace_lossy = true,
|
||||
"--editing-data" => editing_data = true,
|
||||
"--preserve-editing" => preserve_editing = true,
|
||||
"--legacy-name-encoding" => {
|
||||
legacy_names = Some(it.next().cloned().ok_or("--legacy-name-encoding needs an encoding such as windows-949")?);
|
||||
}
|
||||
"--ai-version" => legacy.push(("aiVersion", it.next().cloned().ok_or("--ai-version needs 7 or pdf")?)),
|
||||
"--hidden-objects" => legacy.push(("hiddenObjects", it.next().cloned().ok_or("--hidden-objects needs layer, omit or visible")?)),
|
||||
"--transparent-images" => legacy.push(("transparentImages", it.next().cloned().ok_or("--transparent-images needs flatten or omit")?)),
|
||||
@@ -346,8 +367,14 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
return Err("a .ait template holds every artboard: --artboard and --range don't apply".into());
|
||||
}
|
||||
let mut h = Headless::new();
|
||||
let opened = if editing_data {
|
||||
let params = json!({"path": input, "editingData": "always"});
|
||||
let opened = if editing_data || legacy_names.is_some() {
|
||||
let mut params = json!({"path": input});
|
||||
if editing_data {
|
||||
params["editingData"] = json!("always");
|
||||
}
|
||||
if let Some(e) = &legacy_names {
|
||||
params["legacyNameEncoding"] = json!(e);
|
||||
}
|
||||
h.call("engine.execute", json!({"command": "document.open", "params": params})).map_err(|e| format!("open {input}: {e}"))?
|
||||
} else {
|
||||
h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))?
|
||||
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
@@ -8074,6 +8074,9 @@ theme Appearance अपीयरेंस
|
||||
# Align to Pixel Grid
|
||||
Align to Pixel Grid पिक्सेल ग्रिड से संरेखित करें
|
||||
@msg only paths and compound paths can be aligned to the pixel grid केवल पाथ और कंपाउंड पाथ को पिक्सेल ग्रिड से संरेखित किया जा सकता है
|
||||
@msg the name `{name}` isn't UTF-8 and was read as {label}: if it looks wrong, open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है और इसे {label} के रूप में पढ़ा गया: यदि यह गलत दिखे, तो फ़ाइल को किसी अन्य legacyNameEncoding से खोलें
|
||||
@msg the name `{name}` isn't UTF-8, and parts of it aren't valid {label} either: open the file with another legacyNameEncoding नाम `{name}` UTF-8 नहीं है, और इसके कुछ हिस्से {label} में भी मान्य नहीं हैं: फ़ाइल को किसी अन्य legacyNameEncoding से खोलें
|
||||
@msg more names that aren't UTF-8 were read as {label} ({count}) UTF-8 में न होने वाले और नाम {label} के रूप में पढ़े गए ({count})
|
||||
Start Lists From Typed Markers टाइप किए गए चिह्नों से सूचियाँ शुरू करें
|
||||
Marquee Selection मार्की चयन
|
||||
Touching स्पर्श करते हुए
|
||||
|
||||
|
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.
|
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user