mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 23:00:35 +00:00
.ai import: paths keep their notes and Show Center
Squashed from #5129 onto main.
This commit is contained in:
@@ -3117,7 +3117,7 @@ fn on_path_data(
|
||||
/// not included, so its program can't be run and its layers are read from it.
|
||||
const NAMED_PROLOG_AI: &str = "%!PS-Adobe-3.0\n%%BoundingBox: 0 0 200 100\n%%DocumentNeededResources: procset Adobe_packedarray 2.0 0\n%AI3_TemplateBox: 288 384 288 384\n%%EndComments\n\
|
||||
%%BeginProlog\n%%IncludeResource: procset Adobe_packedarray 2.0 0\nAdobe_packedarray /initialize get exec\n%%EndProlog\n%%BeginSetup\n%AI5_Begin_NonPrinting\nNp\n%AI5_End_NonPrinting--\n%%EndSetup\n\
|
||||
%AI5_BeginLayer\n1 1 1 1 0 0 -1 191 63 255 Lb\n(A::B) Ln\n0 A\n0 R\n0 0 0 1 K\n0 i 1 J 1 j 0.8 w 4 M [2 1]0 d\n0 D\n10 10 m\n90 10 L\n90 90 L\n10 90 L\n10 10 L\nS\n\
|
||||
%AI5_BeginLayer\n1 1 1 1 0 0 -1 191 63 255 Lb\n(A::B) Ln\n0 A\n0 R\n0 0 0 1 K\n0 i 1 J 1 j 0.8 w 4 M [2 1]0 d\n0 D\n%AI3_Note:a (note)\n%AI5_NoteMore:more\n1 Ap\n10 10 m\n90 10 L\n90 90 L\n10 90 L\n10 10 L\nS\n%%Note:\n0 Ap\n\
|
||||
u\n0.5 g\n20 20 m\n40 20 L\n40 40 L\nf\nU\n%AI5_BeginLayer\n0 1 1 1 0 0 3 0 0 0 Lb\n(Sub) Ln\n110 10 m\n190 90 L\nS\nLB\n%AI5_EndLayer--\nLB\n%AI5_EndLayer--\n\
|
||||
%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n";
|
||||
|
||||
@@ -3136,7 +3136,7 @@ proptest! {
|
||||
/// A `.ai` in PostScript form whose prolog is named, not included, damaged: read from its
|
||||
/// layers, or refused.
|
||||
#[test]
|
||||
fn ps_form_ai_never_panics(cut in 0usize..1_200, edits in prop::collection::vec((0usize..1_200, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', '[', ']', '%', 'u', 'U', 'L', 'b', 'B', 'i', 'A', 'K'])), 0..12)) {
|
||||
fn ps_form_ai_never_panics(cut in 0usize..1_200, edits in prop::collection::vec((0usize..1_200, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', '[', ']', '%', 'u', 'U', 'L', 'b', 'B', 'i', 'A', 'K', 'p', ':', '\u{e9}'])), 0..12)) {
|
||||
let text = mutate_text(NAMED_PROLOG_AI, cut, &edits);
|
||||
survive("mutated PostScript-form .ai", || vectorcraft_engine::cmd::fileio::load("x.ai", text.as_bytes()).ok().map(|l| l.doc))?;
|
||||
}
|
||||
@@ -3211,6 +3211,13 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"O",
|
||||
"R",
|
||||
"XR",
|
||||
// A path's note and Show Center (version 7 §5.3.1, §12).
|
||||
"Ap",
|
||||
"1 Ap",
|
||||
"\n%AI3_Note: a (note) \n",
|
||||
"\n%AI3_Note:\n",
|
||||
"\n%AI5_NoteMore:more\n",
|
||||
"\n%%Note:old\n",
|
||||
"w",
|
||||
"J",
|
||||
"j",
|
||||
|
||||
@@ -42,6 +42,8 @@ pub(super) struct Token<'a> {
|
||||
const PREVIEW: (&[u8], &[u8]) = (b"%AI26_BeginPlacedObjectPreview", b"%AI26_EndPlacedObjectPreview");
|
||||
/// A placed file's markers, which a hidden one has on `%_` lines.
|
||||
const PLACE: [&[u8]; 2] = [b"%AI5_BeginPlace", b"%AI5_EndPlace"];
|
||||
/// A path's note comments, also read on `%_` lines.
|
||||
const NOTES: [&[u8]; 3] = [b"%AI3_Note:", b"%%Note:", b"%AI5_NoteMore:"];
|
||||
/// A binary section skipped whole: (begin, end) markers.
|
||||
const DOCUMENT: (&[u8], &[u8]) = (b"%%BeginDocument", b"%%EndDocument");
|
||||
|
||||
@@ -144,8 +146,9 @@ impl<'a> Lexer<'a> {
|
||||
if start {
|
||||
return Some(Token { tok: Tok::Comment(line.get(1..).unwrap_or_default()), hidden: false });
|
||||
}
|
||||
// So does a hidden placed file: its `~` ends what `%_%AI5_BeginPlace` begins.
|
||||
if prefixed && PLACE.iter().any(|m| line.starts_with(m)) {
|
||||
// So does a hidden placed file: its `~` ends what `%_%AI5_BeginPlace` begins. And a
|
||||
// hidden path's note.
|
||||
if prefixed && PLACE.iter().chain(&NOTES).any(|m| line.starts_with(m)) {
|
||||
return Some(Token { tok: Tok::Comment(line.get(1..).unwrap_or_default()), hidden: true });
|
||||
}
|
||||
// A comment later in a line.
|
||||
|
||||
@@ -233,6 +233,11 @@ impl<'a> Reader<'a> {
|
||||
.into();
|
||||
copy
|
||||
});
|
||||
// The note and Show Center are the path's, not its painted copy's.
|
||||
node.edit_attrs(|a| {
|
||||
a.note = gs.note.clone().unwrap_or_default();
|
||||
a.show_center = gs.show_center;
|
||||
});
|
||||
let unread_bounds = unread.then(|| node.visual_bounds()).flatten();
|
||||
let unread_id = node.id;
|
||||
self.add(node, hidden)?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::art::{four, sane, two};
|
||||
use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, MAX_SUBREADERS, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED};
|
||||
use super::types::{FLIP, Kind, MAX_HEX, MAX_NOTE, MAX_PATTERNS, MAX_STACK, MAX_SUBREADERS, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED};
|
||||
use super::{Arc, Node, PatternDef, Reader, Rect, Tok, V, marker};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -49,6 +49,9 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if self.note_comment(c) {
|
||||
return Ok(());
|
||||
}
|
||||
let m = marker(line);
|
||||
// A pattern's definition (one inside another's isn't read).
|
||||
if m == "BeginPattern" && self.pattern.is_none() {
|
||||
@@ -157,6 +160,44 @@ impl<'a> Reader<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `%AI3_Note:` (or `%%Note:`) sets the note of the paths that follow, an empty one clearing it,
|
||||
/// and `%AI5_NoteMore:` adds a line to it. The text is kept as written, up to [`MAX_NOTE`]
|
||||
/// characters.
|
||||
fn note_comment(&mut self, c: &[u8]) -> bool {
|
||||
let (text, more) = if let Some(t) = c.strip_prefix(b"AI3_Note:").or_else(|| c.strip_prefix(b"%Note:")) {
|
||||
(t, false)
|
||||
} else if let Some(t) = c.strip_prefix(b"AI5_NoteMore:") {
|
||||
(t, true)
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
// Only as much of the line as the note can keep is decoded; a UTF-8 character the cut
|
||||
// splits is left out.
|
||||
let mut head = text.get(..text.len().min(4 * MAX_NOTE)).unwrap_or(text);
|
||||
if head.len() < text.len()
|
||||
&& let Err(e) = std::str::from_utf8(head)
|
||||
&& e.error_len().is_none()
|
||||
{
|
||||
head = head.get(..e.valid_up_to()).unwrap_or(head);
|
||||
}
|
||||
let text = super::obj::text(head);
|
||||
match (&mut self.gs.note, more) {
|
||||
(Some(note), true) => {
|
||||
note.push('\n');
|
||||
note.push_str(&text);
|
||||
}
|
||||
// A continuation with no note to continue.
|
||||
(None, true) => return true,
|
||||
(note, false) => *note = Some(text),
|
||||
}
|
||||
if let Some(note) = self.gs.note.as_mut()
|
||||
&& let Some((at, _)) = note.char_indices().nth(MAX_NOTE)
|
||||
{
|
||||
note.truncate(at);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Skip to the comment with marker `end` (sections of the same kind nested in it count, and
|
||||
/// versioned content in an alternate).
|
||||
fn skip_to(&mut self, end: &str) -> Result<(), String> {
|
||||
|
||||
@@ -158,6 +158,13 @@ impl<'a> Reader<'a> {
|
||||
"O" => self.gs.overprint_fill = Self::nums(&vals).last() == Some(&1.0),
|
||||
"R" => self.gs.overprint_stroke = Self::nums(&vals).last() == Some(&1.0),
|
||||
"XR" => self.gs.rule = if Self::nums(&vals).last() == Some(&1.0) { FillRule::EvenOdd } else { FillRule::NonZero },
|
||||
// Show Center (§12): 1 shows the center point of the paths that follow, and 0 leaves
|
||||
// them at their default, which hides it on an imported path.
|
||||
"Ap" => match Self::nums(&vals).last().copied() {
|
||||
Some(1.0) => self.gs.show_center = Some(true),
|
||||
Some(0.0) => self.gs.show_center = None,
|
||||
_ => {}
|
||||
},
|
||||
"w" => {
|
||||
if let Some(v) = Self::nums(&vals).last().filter(|v| v.is_finite() && **v >= 0.0) {
|
||||
self.gs.width = *v;
|
||||
|
||||
@@ -40,6 +40,9 @@ pub(crate) const FLIP: Affine = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
/// Most patterns read (others can't fill art).
|
||||
pub(super) const MAX_PATTERNS: usize = 4096;
|
||||
|
||||
/// Most characters of a path's note read (the version 7 specification allows 254, §5.3.1).
|
||||
pub(super) const MAX_NOTE: usize = 1024;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GState {
|
||||
pub(crate) fill: Paint,
|
||||
@@ -59,6 +62,11 @@ pub(crate) struct GState {
|
||||
pub(crate) overprint_stroke: bool,
|
||||
pub(super) locked: bool,
|
||||
pub(super) hidden: bool,
|
||||
/// The note (`%AI3_Note:`) the paths that follow get, until another replaces it (`None`: no
|
||||
/// note comment yet; empty: none).
|
||||
pub(super) note: Option<String>,
|
||||
/// Show Center (`1 Ap`) for the paths that follow (`None`: the path's default).
|
||||
pub(super) show_center: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for GState {
|
||||
@@ -82,6 +90,8 @@ impl Default for GState {
|
||||
overprint_stroke: false,
|
||||
locked: false,
|
||||
hidden: false,
|
||||
note: None,
|
||||
show_center: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +232,6 @@ pub(super) const LINKED_PREVIEWS: &str =
|
||||
pub(super) const IGNORED: &[&str] = &[
|
||||
"Ae",
|
||||
"AE",
|
||||
"Ap",
|
||||
"As",
|
||||
"Xd",
|
||||
"Xr",
|
||||
|
||||
@@ -118,6 +118,109 @@ fn groups_names_hidden_and_locked() {
|
||||
assert!(!hidden.visible);
|
||||
}
|
||||
|
||||
/// A node's Attributes panel note (empty: none).
|
||||
fn note(n: &Node) -> String {
|
||||
n.extras().attrs.as_deref().map(|a| a.note.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The notes of the first layer's objects, in order.
|
||||
fn notes(doc: &Document) -> Vec<String> {
|
||||
art(doc).iter().map(|n| note(n)).collect()
|
||||
}
|
||||
|
||||
/// A note comment names the paths after it until another one replaces it, and an empty one clears
|
||||
/// it (§5.3.1: `%AI3_Note: <note>`).
|
||||
#[test]
|
||||
fn a_note_comment_is_the_note_of_the_paths_after_it() {
|
||||
let [a, b, c, d] = [0.0, 20.0, 40.0, 60.0].map(|x| rect_path(x, 0.0, x + 10.0, 10.0));
|
||||
let body = format!("%AI3_Note:hello\n{a}f\n{b}f\n%AI3_Note:\n{c}f\n%AI3_Note:world\n{d}f\n");
|
||||
assert_eq!(notes(&read_ok(&layer(&body)).doc), ["hello", "hello", "", "world"]);
|
||||
// `%%Note:` sets it too.
|
||||
assert_eq!(notes(&read_ok(&layer(&format!("%%Note:old style\n{a}f\n"))).doc), ["old style"]);
|
||||
// A path without a note has no attributes.
|
||||
assert!(art(&read_ok(&layer(&format!("{a}f\n"))).doc)[0].extras().attrs.is_none());
|
||||
}
|
||||
|
||||
/// `%AI5_NoteMore:` adds a line to the note; the text is kept as written, and its bytes are read as
|
||||
/// UTF-8, else Windows-1252.
|
||||
#[test]
|
||||
fn a_note_continues_on_more_lines_and_keeps_its_text() {
|
||||
let a = rect_path(0.0, 0.0, 10.0, 10.0);
|
||||
let body = format!("%AI3_Note: two (words) caf@ \n%AI5_NoteMore:more\n{a}f\n");
|
||||
let data = stream(&layer(&body)).replace('@', "\u{1}").into_bytes();
|
||||
let data: Vec<u8> = data.into_iter().map(|b| if b == 1 { 0xE9 } else { b }).collect();
|
||||
assert_eq!(notes(&read(&data).unwrap().doc), [" two (words) café \nmore"]);
|
||||
// A continuation of an empty note is its second line; one with no note before it is ignored.
|
||||
assert_eq!(notes(&read_ok(&layer(&format!("%AI3_Note:\n%AI5_NoteMore:x\n{a}f\n"))).doc), ["\nx"]);
|
||||
assert_eq!(notes(&read_ok(&layer(&format!("%AI5_NoteMore:x\n{a}f\n"))).doc), [""]);
|
||||
// A note too long is cut at 1,024 characters, however many lines it has.
|
||||
let long = format!("%AI3_Note:{}\n{}{a}f\n", "x".repeat(5000), "%AI5_NoteMore:yy\n".repeat(3000));
|
||||
assert_eq!(notes(&read_ok(&layer(&long)).doc)[0].chars().count(), 1024);
|
||||
let long = format!("%AI3_Note:x\n{}{a}f\n", "%AI5_NoteMore:yy\n".repeat(3000));
|
||||
assert_eq!(notes(&read_ok(&layer(&long)).doc)[0].chars().count(), 1024);
|
||||
// A long UTF-8 line stays UTF-8 where it is cut.
|
||||
let long = format!("%AI3_Note:{}\n{a}f\n", "é".repeat(3000));
|
||||
assert_eq!(notes(&read_ok(&layer(&long)).doc), ["é".repeat(1024)]);
|
||||
// A note on a `%_` line is read too.
|
||||
assert_eq!(notes(&read_ok(&layer(&format!("%_%AI3_Note:hidden\n{a}f\n"))).doc), ["hidden"]);
|
||||
}
|
||||
|
||||
/// A note before a group or a compound path isn't theirs: their paths keep their own.
|
||||
#[test]
|
||||
fn a_note_before_a_group_or_compound_path_is_not_theirs() {
|
||||
let [a, b] = [0.0, 20.0].map(|x| rect_path(x, 0.0, x + 10.0, 10.0));
|
||||
for (open, close) in [("u", "U"), ("*u", "*U")] {
|
||||
let body = format!("%AI3_Note:outer\n0 Ae\n{open}\n%AI3_Note:inner\n{a}f\n{b}f\n{close}\n");
|
||||
let doc = read_ok(&layer(&body)).doc;
|
||||
let top = &art(&doc)[0];
|
||||
assert_eq!(note(top), "", "{open}");
|
||||
let kids: Vec<String> = top.children().unwrap().iter().map(|k| note(k)).collect();
|
||||
assert_eq!(kids, ["inner", "inner"], "{open}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipping path's note is its own, not that of the copy that paints its fill.
|
||||
#[test]
|
||||
fn a_clipping_paths_note_is_not_copied_to_its_painted_fill() {
|
||||
let body =
|
||||
format!("0 Ae\nq\n{RED}%AI3_Note:clip\n{}W\nf\n%AI3_Note:\n{}f\nQ\n", rect_path(5.0, 5.0, 25.0, 25.0), rect_path(0.0, 0.0, 30.0, 30.0));
|
||||
let doc = read_ok(&layer(&body)).doc;
|
||||
fn walk(nodes: &[Arc<Node>], out: &mut Vec<(bool, String)>) {
|
||||
for n in nodes {
|
||||
out.push((matches!(n.kind, NodeKind::Path { clipping: true, .. }), note(n)));
|
||||
walk(n.children().map(Vec::as_slice).unwrap_or_default(), out);
|
||||
}
|
||||
}
|
||||
let mut all = Vec::new();
|
||||
walk(&art(&doc), &mut all);
|
||||
let noted: Vec<_> = all.iter().filter(|(_, n)| n == "clip").collect();
|
||||
assert_eq!(noted, [&(true, "clip".to_string())], "{all:?}");
|
||||
assert!(all.len() >= 3, "the painted copy is there: {all:?}");
|
||||
}
|
||||
|
||||
/// `1 Ap` shows the center point of the paths after it until `0 Ap` (§12, showCenter), which leaves
|
||||
/// them at an imported path's default: hidden.
|
||||
#[test]
|
||||
fn ap_shows_the_center_of_the_paths_after_it() {
|
||||
let [a, b, c] = [0.0, 20.0, 40.0].map(|x| rect_path(x, 0.0, x + 10.0, 10.0));
|
||||
let ellipse = "50 0 m\n50 5.5 45.5 10 40 10 c\n34.5 10 30 5.5 30 0 c\n30 -5.5 34.5 -10 40 -10 c\n45.5 -10 50 -5.5 50 0 c\n";
|
||||
let body = format!("1 Ap\n{a}f\n{b}f\n0 Ap\n{c}f\n{ellipse}f\n2 Ap\n{a}f\n");
|
||||
let doc = read_ok(&layer(&body)).doc;
|
||||
let shown: Vec<_> = art(&doc).iter().map(|n| (n.extras().attrs.as_deref().and_then(|a| a.show_center), n.shows_center())).collect();
|
||||
assert_eq!(shown, [(Some(true), true), (Some(true), true), (None, false), (None, false), (None, false)]);
|
||||
}
|
||||
|
||||
/// What a skipped section sets doesn't reach the art after it.
|
||||
#[test]
|
||||
fn a_note_or_ap_in_a_skipped_section_does_not_reach_the_art_after_it() {
|
||||
let a = rect_path(0.0, 0.0, 10.0, 10.0);
|
||||
let body = format!("%AI3_BeginBrushPattern\n1 Ap\n%AI3_Note:brush\n{a}f\n%AI3_EndBrushPattern\n{}", layer(&format!("{a}f\n")));
|
||||
let doc = read_ok(&body).doc;
|
||||
let n = &art(&doc)[0];
|
||||
assert_eq!(note(n), "");
|
||||
assert!(!n.shows_center());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paths_colours_and_strokes() {
|
||||
let art_ = format!(
|
||||
|
||||
@@ -257,6 +257,22 @@ fn a_version_7_palette_keeps_its_swatches_without_names() {
|
||||
assert_eq!(d.swatch("Gray K=0").unwrap().paint.color(), Some(Color::gray(0.0)), "1 g is white");
|
||||
}
|
||||
|
||||
/// A version 8 file's paths keep their notes and Show Center.
|
||||
#[test]
|
||||
fn a_legacy_files_paths_keep_their_notes_and_show_center() {
|
||||
let body = format!(
|
||||
"%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Notes) Ln\n0 A\n%AI3_Note:first\n%AI5_NoteMore:second line\n1 Ap\n0 0 0 1 k\n{}\n\
|
||||
%AI3_Note:\n0 Ap\n{}\nLB\n%AI5_EndLayer--\n",
|
||||
tri(10),
|
||||
tri(40)
|
||||
);
|
||||
let r = import(&ai(AI8, "", &body)).unwrap();
|
||||
let kids = r.document.layers[0].children().unwrap();
|
||||
let attrs: Vec<_> = kids.iter().map(|n| n.extras().attrs.as_deref().map(|a| (a.note.clone(), a.show_center))).collect();
|
||||
assert_eq!(attrs, [Some(("first\nsecond line".into(), Some(true))), None], "{:?}", r.warnings);
|
||||
assert!(kids[0].shows_center() && !kids[1].shows_center());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbalanced_and_runaway_groups_are_read_safely() {
|
||||
// Ends without a beginning end nothing; groups left open hold the rest.
|
||||
|
||||
@@ -1079,6 +1079,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points.
|
||||
Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests/links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).
|
||||
`.ai` and EPS editing-data import keeps template layers, and opens canvas and artboard ruler guides as ruler guides on their layers; guide objects stay editable guide paths (#779; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
`.ai` and EPS editing-data import keeps each path's Attributes panel note (`%AI3_Note:`, `%%Note:`, more lines in `%AI5_NoteMore:`, up to 1,024 characters) and Show Center (`1 Ap`); a note or Show Center applies to every path after it until the next one, and a note before a group or compound path is not theirs (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_illustrator.rs`).
|
||||
A `.ai` saved without PDF compatibility (its PDF part only a placeholder page) is placed and pasted from its editing data, as it opens: File › Place places the art of the layers that show on the first artboard (`file.place`'s `page` places another one), and Paste of it as PDF data (`clipboard.importPdf`) takes every object. `document.open` with `pages` naming every page of such a file opens it the same way (`crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part; CCITT Group 3 and planar files with extra channels are not read); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
|
||||
- The Open and Place dialogs list PostScript `.ps` files and `.epsf` files. A `.ps` file opens and places through the EPS reader (its first page), also when its header is only `%!`, as the `ps` format, which exports don't write (Print to PostScript does); an `.epsf` file opens and places as EPS. Copied in a file manager, these files paste as EPS, and `clipboard.importEps` also reads PostScript whose header is only `%!` (`crates/engine/src/cmd/fileio/tests_epsimport.rs`).
|
||||
|
||||
Reference in New Issue
Block a user