mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 12:13:19 +00:00
Merge pull request #5959 from dotCooCoo/fix/ai-symbol-section-strings
.ai import: ASCII85 data in a section does not hide the section's end
This commit is contained in:
@@ -3633,8 +3633,8 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Editing data with two symbols (S2's art holds an instance of S1), the symbol list and a layer of
|
||||
/// instances, as the format writes them.
|
||||
/// Editing data with symbols (S2's art holds an instance of S1, F's holds non-native art), the
|
||||
/// symbol list and a layer of instances, as the format writes them.
|
||||
fn symbols_text() -> String {
|
||||
use vectorcraft_testkit::ai;
|
||||
let def = |name: &str, art: &str, reg: u32| {
|
||||
@@ -3643,7 +3643,12 @@ fn symbols_text() -> String {
|
||||
)
|
||||
};
|
||||
let inst = |name: &str, m: &str| format!("/SymbolInstance :\n({name}) /SymbolRef ,\n{m} /RTransformMatrix\n;\n9 () XW\n");
|
||||
let defs = def("S1", "0 1 1 0 1 0 0 Xa\n-2 -2 m\n2 -2 L\n2 2 L\n-2 2 L\nf\n", 5) + &def("S2", &inst("S1", "1 0 0 -1 -8081 8237"), 1);
|
||||
// Non-native art keeps ASCII85 data in comment lines, some of them `%_` lines holding a `(`.
|
||||
let foreign = "/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data ,\n\
|
||||
%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n";
|
||||
let defs = def("F", &format!("{foreign}-2 -2 m\n2 -2 L\n2 2 L\nf\n"), 5)
|
||||
+ &def("S1", "0 1 1 0 1 0 0 Xa\n-2 -2 m\n2 -2 L\n2 2 L\n-2 2 L\nf\n", 5)
|
||||
+ &def("S2", &inst("S1", "1 0 0 -1 -8081 8237"), 1);
|
||||
let list = "%AI24_BeginSymbolList\n(S2)\n(S1)\n%AI24_EndSymbolList\n";
|
||||
let page = ai::layer(
|
||||
"L",
|
||||
@@ -3668,7 +3673,7 @@ fn the_symbols_fuzz_data_opens_live() {
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&symbols_text())].concat();
|
||||
let (doc, warnings, layered) = vectorcraft_eps::layered_ai(&private, Document::new(200.0, 100.0), vec![], false, true, 1.0);
|
||||
assert!(layered, "{warnings:?}");
|
||||
let names: Vec<&str> = doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
let names: Vec<&str> = doc.symbols.iter().map(|s| s.name.as_str()).filter(|n| *n != "F").collect();
|
||||
assert_eq!(names, ["S2", "S1"]);
|
||||
let mut instances = 0;
|
||||
doc.walk(|n| instances += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. })));
|
||||
@@ -4165,6 +4170,7 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"\n%_/AI11Text :\n%_0 /StoryIndex ,\n%_;\n",
|
||||
"/SymbolInstance :",
|
||||
"/Binary : /ASCII85Decode ,",
|
||||
"/AI11TextDocument : /ASCII85Decode ,",
|
||||
"~>",
|
||||
"p",
|
||||
"P",
|
||||
|
||||
@@ -133,7 +133,8 @@ impl<'a> Lexer<'a> {
|
||||
self.src.get(self.pos).copied()
|
||||
}
|
||||
|
||||
fn rest(&self) -> &'a [u8] {
|
||||
/// The data from where the next token is looked for.
|
||||
pub fn rest(&self) -> &'a [u8] {
|
||||
self.src.get(self.pos..).unwrap_or_default()
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,10 @@ fn swatch_names(data: &[u8]) -> ShownNames {
|
||||
let (mut palette_at, mut palette) = (None, None);
|
||||
// A pattern's name is the first string of its definition.
|
||||
let mut head = false;
|
||||
// ASCII85 data in comment lines is skipped past, as the reader skips it.
|
||||
let mut dictionaries = ops::Dictionaries::default();
|
||||
// The scan's state where the versioned content being skipped starts, to go back to.
|
||||
let mut versioned = None;
|
||||
loop {
|
||||
let before = lex.offset();
|
||||
let Some(t) = lex.next_token() else {
|
||||
@@ -286,11 +290,14 @@ fn swatch_names(data: &[u8]) -> ShownNames {
|
||||
match skip.take() {
|
||||
Some((.., Some(at))) => {
|
||||
lex.seek(at);
|
||||
dictionaries = versioned.take().unwrap_or_default();
|
||||
continue;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
};
|
||||
let markers = skip.map(|s| (s.0, s.1)).or(palette_at.map(|_| ("", "EndPalette"))).or(section.map(|s| (s.0, s.1)));
|
||||
dictionaries.pass(&t.tok, &mut lex, markers);
|
||||
match &t.tok {
|
||||
Tok::Comment(c) => {
|
||||
let line = String::from_utf8_lossy(c);
|
||||
@@ -306,6 +313,7 @@ fn swatch_names(data: &[u8]) -> ShownNames {
|
||||
}
|
||||
} else if m.starts_with("Alternate_Content") && section.is_none_or(|s| s.3) {
|
||||
skip = Some(("Begin_Content_if_version_gt", "End_Versioned_Content", 0, Some(lex.offset())));
|
||||
versioned = Some(dictionaries.clone());
|
||||
} else if m == "%BeginProlog" {
|
||||
skip = Some(("", "%EndProlog", 0, None));
|
||||
} else if m == "%BeginResource" {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use super::super::brush;
|
||||
use super::art::{four, sane, two};
|
||||
use super::ops::Dictionaries;
|
||||
use super::types::{
|
||||
FLIP, GState, Kind, MAX_DEPTH, MAX_HEX, MAX_LISTED, MAX_NODES, MAX_NOTE, MAX_PATTERNS, MAX_PLACED_FOLDER, MAX_STACK, MAX_SUBREADERS, MAX_SYMBOLS,
|
||||
PatternHead, Place, Raster, SKIPPED, SymbolHead, VERSIONED_UNENDED,
|
||||
@@ -330,15 +331,18 @@ impl<'a> Reader<'a> {
|
||||
self.section(end).map(|_| ())
|
||||
}
|
||||
|
||||
/// [`Self::skip_to`] → what was skipped, up to the comment that ends it.
|
||||
/// [`Self::skip_to`] → what was skipped, up to the comment that ends it. ASCII85 data in comment
|
||||
/// lines is skipped past as [`Self::add_object_entry`] skips it.
|
||||
fn section(&mut self, end: &str) -> Result<&'a [u8], String> {
|
||||
let begin = SKIPPED.iter().find(|(_, e)| *e == end).map_or("", |(b, _)| *b);
|
||||
let begin = if end == "End_Versioned_Content" { "Begin_Content_if_version_gt" } else { begin };
|
||||
let start = self.lex.offset();
|
||||
let mut depth = 0usize;
|
||||
let mut dictionaries = Dictionaries::default();
|
||||
loop {
|
||||
let before = self.lex.offset();
|
||||
let Some(t) = self.lex.next_token() else { break };
|
||||
dictionaries.pass(&t.tok, &mut self.lex, Some((begin, end)));
|
||||
if let Tok::Comment(c) = t.tok {
|
||||
let line = String::from_utf8_lossy(c);
|
||||
let m = marker(&line);
|
||||
|
||||
@@ -34,10 +34,11 @@ impl<'a> Reader<'a> {
|
||||
// of it may start with `%_` and read as hidden tokens: a `(` there would open
|
||||
// a string that swallows the rest of the layer.
|
||||
let key = o.entries.last().and_then(|(k, _)| k.as_deref());
|
||||
if o.ty == "Binary" && key == Some("ASCII85Decode") {
|
||||
self.lex.skip_past(b"~>");
|
||||
} else if o.ty == "ForeignObject" && key == Some("Data") {
|
||||
self.foreign_data = Some(self.lex.skip_past(b"~>").to_vec());
|
||||
if ascii85_follows(&o.ty, key) {
|
||||
let data = self.lex.skip_past(b"~>");
|
||||
if o.ty == "ForeignObject" {
|
||||
self.foreign_data = Some(data.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1123,101 @@ fn strip(n: &mut Node, hidden: &HashSet<NodeId>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether ASCII85 data in comment lines, up to its `~>`, follows the entry keyed `key` of a
|
||||
/// dictionary of type `ty`: the `/ASCII85Decode` of a `/Binary` dictionary or a text document, or a
|
||||
/// non-native art object's (`/ForeignObject`'s) `/Data`.
|
||||
pub(super) fn ascii85_follows(ty: &str, key: Option<&str>) -> bool {
|
||||
matches!((ty, key), ("Binary" | "AI11TextDocument" | "AI11UndoFreeTextDocument", Some("ASCII85Decode")) | ("ForeignObject", Some("Data")))
|
||||
}
|
||||
|
||||
/// The dictionaries (`/Type :` … `;`) a scan of tokens passes that the reader doesn't read, kept to
|
||||
/// tell where an entry's ASCII85 data follows ([`ascii85_follows`]), so the scan can skip past it
|
||||
/// as the reader does: a `(` on a `%_` line of the data would otherwise open a string.
|
||||
#[derive(Clone, Default)]
|
||||
pub(super) struct Dictionaries<'s> {
|
||||
/// The open dictionaries' types, innermost last, up to [`MAX_DEPTH`].
|
||||
types: Vec<&'s [u8]>,
|
||||
/// How many more are open inside the last of `types`.
|
||||
deeper: usize,
|
||||
/// The name just before the token taken.
|
||||
name: Option<&'s [u8]>,
|
||||
/// Whether the string just before the token taken is a key that ASCII85 data follows.
|
||||
string_key: bool,
|
||||
/// The end markers of the sections being scanned (`None`: no section) where the rest of the data
|
||||
/// had no `~>` or marker to stop at, so they aren't searched for again in those sections.
|
||||
no_end: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
/// Where the ASCII85 data at the start of `rest` stops: just past its `~>`, or at the comment that
|
||||
/// comes first with a marker of `section`, the begin and end markers of the section being scanned
|
||||
/// (no begin marker when it's empty). A line of the data that reads as one of them counts as the
|
||||
/// marker. `None` when `rest` has neither.
|
||||
fn data_end(rest: &[u8], section: Option<(&str, &str)>) -> Option<usize> {
|
||||
let mut at = 0;
|
||||
for line in rest.split(|b| matches!(b, b'\r' | b'\n')) {
|
||||
// The first line is what is left of the key's line, where a `%` starts no marker.
|
||||
if at > 0
|
||||
&& let Some((comment, (begin, end))) = line.strip_prefix(b"%").zip(section)
|
||||
{
|
||||
let comment = String::from_utf8_lossy(comment);
|
||||
let marker = super::marker(&comment);
|
||||
if marker == end || (!begin.is_empty() && marker == begin) {
|
||||
return Some(at);
|
||||
}
|
||||
}
|
||||
if let Some(i) = line.windows(2).position(|w| w == b"~>") {
|
||||
return Some(at + i + 2);
|
||||
}
|
||||
at += line.len() + 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl<'s> Dictionaries<'s> {
|
||||
/// Take `tok`, the token `lex` just gave, and skip the ASCII85 data that follows it, up to where
|
||||
/// [`data_end`] stops for `section`, so the scan still finds that section's markers.
|
||||
pub(super) fn pass(&mut self, tok: &super::Tok<'s>, lex: &mut super::Lexer<'s>, section: Option<(&str, &str)>) {
|
||||
let end = section.map(|(_, end)| end);
|
||||
if self.data_follows(tok) && !self.no_end.iter().any(|e| e.as_deref() == end) {
|
||||
match data_end(lex.rest(), section) {
|
||||
Some(stop) => lex.seek(lex.offset() + stop),
|
||||
None => self.no_end.push(end.map(str::to_owned)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the next token → whether ASCII85 data follows it.
|
||||
fn data_follows(&mut self, tok: &super::Tok<'s>) -> bool {
|
||||
// Comments between a name and what follows it are passed over, as the reader passes them.
|
||||
if matches!(tok, super::Tok::Comment(_)) {
|
||||
return false;
|
||||
}
|
||||
let name = self.name.take();
|
||||
let string_key = std::mem::take(&mut self.string_key);
|
||||
match tok {
|
||||
super::Tok::Name(n) => self.name = Some(n),
|
||||
// A string keys an entry as a name does, but gives a dictionary no type.
|
||||
super::Tok::Str(s) | super::Tok::Hex(s) => self.string_key = self.keys_data(s),
|
||||
super::Tok::Word(b":") if self.types.len() < MAX_DEPTH => self.types.push(name.unwrap_or_default()),
|
||||
super::Tok::Word(b":") => self.deeper = self.deeper.saturating_add(1),
|
||||
super::Tok::Word(b";") if self.deeper > 0 => self.deeper -= 1,
|
||||
super::Tok::Word(b";") => {
|
||||
self.types.pop();
|
||||
}
|
||||
super::Tok::Word(b",") => return string_key || name.is_some_and(|key| self.keys_data(key)),
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether ASCII85 data follows an entry keyed `key` of the innermost dictionary.
|
||||
fn keys_data(&self, key: &[u8]) -> bool {
|
||||
let Some(ty) = self.types.last().filter(|_| self.deeper == 0) else { return false };
|
||||
let (Ok(ty), Ok(key)) = (std::str::from_utf8(ty), std::str::from_utf8(key)) else { return false };
|
||||
ascii85_follows(ty, Some(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1132,6 +1228,20 @@ mod tests {
|
||||
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(fill, Paint::None, 0.0)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_stops_at_its_end_or_a_marker_of_the_section() {
|
||||
let section = Some(("BeginTextDocument", "EndTextDocument"));
|
||||
assert_eq!(data_end(b"\n%_(a~>\n%AI11_EndTextDocument\n", section), Some(7));
|
||||
assert_eq!(data_end(b"\n%_(a\n%AI11_EndTextDocument\n", section), Some(6));
|
||||
assert_eq!(data_end(b"\n%_(a\n%AI11_BeginTextDocument\n", section), Some(6));
|
||||
// Bytes that aren't UTF-8 after the marker, as the scan reads a comment.
|
||||
assert_eq!(data_end(b"\n%_(a\n%AI11_EndTextDocument \xff\n", section), Some(6));
|
||||
// Right after the key a `%` starts no marker, and an empty begin marker matches no comment.
|
||||
assert_eq!(data_end(b"%AI11_EndTextDocument\n%_(a\n", section), None);
|
||||
assert_eq!(data_end(b"\n%\n", Some(("", "EndPalette"))), None);
|
||||
assert_eq!(data_end(b"\n%AI11_EndTextDocument\n", None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_outlines_without_gradients_move_alike() {
|
||||
// GUARDS: an image's pixels and a placed gradient stay behind when an effect moves the outline.
|
||||
|
||||
@@ -4503,6 +4503,166 @@ fn non_native_art_that_is_not_a_pdf_is_left_out_with_a_note() {
|
||||
assert!(s.warnings.iter().any(|w| w.starts_with(read::NON_NATIVE_ART_UNREAD)), "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// The same data in a symbol's definition: its `%_` line with a `(` opens no string, so the
|
||||
/// definition ends at its own end marker and the symbol after it comes in.
|
||||
#[test]
|
||||
fn non_native_art_in_a_symbol_ends_at_the_definitions_end() {
|
||||
let foreign = "/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data ,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n";
|
||||
let defs = symbols(&(symbol("Pdf", &format!("{foreign}{}", dot()), 5) + &symbol("Dot", &dot(), 5)));
|
||||
let s = read_ok(&(defs + &layer(&instance("Dot", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
let names: Vec<&str> = s.doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert!(names.contains(&"Dot"), "{names:?}");
|
||||
let a = art(&s.doc);
|
||||
assert_eq!(a.len(), 1);
|
||||
assert!(matches!(&a[0].kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Dot"), "{:?}", a[0].kind);
|
||||
}
|
||||
|
||||
/// Comment lines between a dictionary's name and its `:`, or an entry's key and its `,`, don't hide
|
||||
/// the data from the scan of a symbol's definition.
|
||||
#[test]
|
||||
fn non_native_art_with_comments_in_a_symbol_ends_at_the_definitions_end() {
|
||||
let foreign = "/ForeignObject\n%note\n:\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data\n%note\n,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n";
|
||||
let defs = symbols(&(symbol("Pdf", &format!("{foreign}{}", dot()), 5) + &symbol("Dot", &dot(), 5)));
|
||||
let s = read_ok(&(defs + &layer(&instance("Dot", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
let names: Vec<&str> = s.doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert!(names.contains(&"Dot"), "{names:?}");
|
||||
}
|
||||
|
||||
/// A string keys an entry as a name does: `(Data) ,` doesn't hide the data from the scan of a
|
||||
/// symbol's definition either.
|
||||
#[test]
|
||||
fn non_native_art_keyed_by_a_string_in_a_symbol_ends_at_the_definitions_end() {
|
||||
for key in ["(Data)", "<44617461>"] {
|
||||
let foreign = format!(
|
||||
"/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n{key} ,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n"
|
||||
);
|
||||
let defs = symbols(&(symbol("Pdf", &format!("{foreign}{}", dot()), 5) + &symbol("Dot", &dot(), 5)));
|
||||
let s = read_ok(&(defs + &layer(&instance("Dot", [1.0, 0.0, 0.0, -1.0, 20.0, 50.0]))));
|
||||
let names: Vec<&str> = s.doc.symbols.iter().map(|s| s.name.as_str()).collect();
|
||||
assert!(names.contains(&"Dot"), "{key}: {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A `/Binary` dictionary's data in a skipped alternate: its `%_` line with a `(` opens no string,
|
||||
/// so the block ends at its own end marker.
|
||||
#[test]
|
||||
fn binary_data_in_a_skipped_alternate_ends_at_the_blocks_end() {
|
||||
let binary = "%_/Document :\n%_/Binary : /ASCII85Decode ,\n%_(]<[%\n%;;;~>\n%_; (Profile) ,\n%_;\n";
|
||||
let body = format!(
|
||||
"%AI17_Begin_Content_if_version_gt:24 4\n%AI17_Alternate_Content\n{binary}frobnicate\n%AI17_End_Versioned_Content\n{}",
|
||||
layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n"))
|
||||
);
|
||||
let s = read_ok(&body);
|
||||
assert_eq!(art(&s.doc).len(), 1);
|
||||
assert!(!s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// A text document's data, the free type's undo copy included: its `%_` line with a `(` opens no
|
||||
/// string, even after a line that reads as another section's end marker, so the section ends at
|
||||
/// its own end marker and the art after it comes in.
|
||||
#[test]
|
||||
fn text_document_data_ends_at_the_sections_end() {
|
||||
for ty in ["AI11TextDocument", "AI11UndoFreeTextDocument"] {
|
||||
let document =
|
||||
format!("%AI11_BeginTextDocument\n/{ty} : /ASCII85Decode ,\n%,u@!!/MSk8\n%AI3_EndPattern\n%_(]<[%\n%;;;~>\n%_;\n%AI11_EndTextDocument\n");
|
||||
let s = read_ok(&format!("{document}{}", layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n"))));
|
||||
assert_eq!(art(&s.doc).len(), 1, "{ty}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Data without its `~>` isn't skipped: the section still ends at its own end marker, with no `~>`
|
||||
/// after it, with one in a later section, and with later data that holds a `(` on a `%_` line.
|
||||
#[test]
|
||||
fn data_without_its_end_does_not_hide_the_sections_end() {
|
||||
let unended = "%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n%,u@!!/MSk8\n%AI11_EndTextDocument\n";
|
||||
let ended = "%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n%,u@!!/MSk8\n%_(]<[%\n%;;;~>\n%_;\n%AI11_EndTextDocument\n";
|
||||
let art_layer = layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n"));
|
||||
for body in [format!("{unended}{art_layer}"), format!("{unended}{art_layer}{ended}"), format!("{unended}{ended}{art_layer}")] {
|
||||
let s = read_ok(&body);
|
||||
assert_eq!(art(&s.doc).len(), 1, "{body}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Data without its `~>` is skipped up to the section's end marker: its `%_` line with a `(` opens
|
||||
/// no string, so the section ends there and the art after it comes in. A `%` right after the key
|
||||
/// starts no marker, so it doesn't end the data.
|
||||
#[test]
|
||||
fn data_without_its_end_holding_a_string_ends_at_the_sections_end() {
|
||||
let art_layer = layer(&(rect_path(0.0, 0.0, 1.0, 1.0) + "f\n"));
|
||||
for document in [
|
||||
"%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n%,u@!!/MSk8\n%_(]<[%\n%AI11_EndTextDocument\n",
|
||||
"%AI11_BeginTextDocument\r\n/AI11TextDocument : /ASCII85Decode ,\r\n%_(]<[%\r\n%AI11_EndTextDocument\r\n",
|
||||
"%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,%AI11_EndTextDocument\n%_(]<[%\n%AI11_EndTextDocument\n",
|
||||
] {
|
||||
let s = read_ok(&format!("{document}{art_layer}"));
|
||||
assert_eq!(art(&s.doc).len(), 1, "{document}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Data without its `~>` stops at the start of a section of the same kind nested after it too, so
|
||||
/// the outer section still ends at its own end marker and not at the nested one's.
|
||||
#[test]
|
||||
fn data_without_its_end_stops_at_a_nested_sections_start() {
|
||||
let square = rect_path(0.0, 0.0, 1.0, 1.0) + "f\n";
|
||||
let body = format!(
|
||||
"%AI17_Begin_Content_if_version_gt:24 4\n%AI17_Alternate_Content\n/Binary : /ASCII85Decode ,\n%_(]<[%\n\
|
||||
%AI17_Begin_Content_if_version_gt:23 1\nfrobnicate\n%AI17_End_Versioned_Content\n{square}%AI17_End_Versioned_Content\n{}",
|
||||
layer(&square)
|
||||
);
|
||||
let s = read_ok(&body);
|
||||
assert_eq!(art(&s.doc).len(), 1);
|
||||
assert!(!s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// Versioned content that doesn't end is scanned again for the swatches' names: data with its `~>`
|
||||
/// is still skipped, though data with none comes after it, and the dictionary open where the
|
||||
/// content starts still counts.
|
||||
#[test]
|
||||
fn swatch_names_are_found_past_data_in_versioned_content_that_does_not_end() {
|
||||
let entries = "1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data ,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n0 0 Xd\n6 () XW\n0 Ae\n";
|
||||
let alternate = "%AI17_Begin_Content_if_version_gt:24 4\n%AI17_Alternate_Content\n";
|
||||
let sea = format!("0 1 1 0 (Sea) 0 x\n{}f\n", rect_path(0.0, 0.0, 5.0, 5.0));
|
||||
let unended = "%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n%,u@!!/MSk8\n%AI11_EndTextDocument\n";
|
||||
for art_text in [format!("{alternate}/ForeignObject :\n{entries}{sea}{unended}"), format!("/ForeignObject :\n{alternate}{entries}{sea}{unended}")]
|
||||
{
|
||||
let body = format!("{}{}", DOTS.replace("(Dots)", "(Sea\\n1)"), layer(&art_text));
|
||||
let d = read(stream(&body).as_bytes()).unwrap().doc;
|
||||
let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Sea 2"], "{art_text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A search for where data stops that found nothing outside a section doesn't stop a later
|
||||
/// section's data at that section's end, so the named color after it still takes its name first.
|
||||
/// The `/Binary` holds a group, where the reader doesn't take what follows its key as data.
|
||||
#[test]
|
||||
fn swatch_names_are_found_past_data_in_a_section_after_a_search_outside_sections_found_nothing() {
|
||||
let sea = format!("0 1 1 0 (Sea) 0 x\n{}f\n", rect_path(0.0, 0.0, 5.0, 5.0));
|
||||
let before = "/Binary :\nu\n/ASCII85Decode ,\nU\n;\n";
|
||||
for section in [
|
||||
"%AI17_Begin_Content_if_version_gt:24 4\n%AI17_Alternate_Content\n/Binary : /ASCII85Decode ,\n%_(]<[%\n%AI17_End_Versioned_Content\n",
|
||||
"%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n%_(]<[%\n%AI11_EndTextDocument\n",
|
||||
] {
|
||||
let art_text = format!("{before}{section}{sea}");
|
||||
let body = format!("{}{}", DOTS.replace("(Dots)", "(Sea\\n1)"), layer(&art_text));
|
||||
let d = read(stream(&body).as_bytes()).unwrap().doc;
|
||||
let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Sea 2"], "{art_text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The names the swatches take are found past the same data: a named color after it still takes
|
||||
/// its name first, and the pattern named after it is numbered.
|
||||
#[test]
|
||||
fn swatch_names_are_found_past_non_native_art_data() {
|
||||
let foreign = "/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n10 20 /Origin ,\n1 2 5 8 /Bounds ,\n/Data ,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n0 0 Xd\n6 () XW\n0 Ae\n";
|
||||
let sea = format!("0 1 1 0 (Sea) 0 x\n{}f\n", rect_path(0.0, 0.0, 5.0, 5.0));
|
||||
let body = format!("{}{}", DOTS.replace("(Dots)", "(Sea\\n1)"), layer(&format!("{foreign}{sea}")));
|
||||
let d = read(stream(&body).as_bytes()).unwrap().doc;
|
||||
let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Sea 2"]);
|
||||
}
|
||||
|
||||
/// A PDF with one page of `size` points holding `content`.
|
||||
fn tiny_pdf(size: f64, content: &str) -> Vec<u8> {
|
||||
let objects = [
|
||||
|
||||
Reference in New Issue
Block a user