diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index d84338094..4db08ea21 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -281,6 +281,8 @@ fn swatch_names(data: &[u8]) -> ShownNames { 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 { @@ -288,13 +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 end = skip.map(|s| s.1).or(palette_at.map(|_| "EndPalette")).or(section.map(|s| s.1)); - dictionaries.pass(&t.tok, &mut lex, end); + 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); @@ -310,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" { diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index fbc889c54..154f917e8 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -342,7 +342,7 @@ impl<'a> Reader<'a> { loop { let before = self.lex.offset(); let Some(t) = self.lex.next_token() else { break }; - dictionaries.pass(&t.tok, &mut self.lex, Some(end)); + 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); diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 5510663ca..5b2f86102 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -1133,7 +1133,7 @@ pub(super) fn ascii85_follows(ty: &str, key: Option<&str>) -> bool { /// 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(Default)] +#[derive(Clone, Default)] pub(super) struct Dictionaries<'s> { /// The open dictionaries' types, innermost last, up to [`MAX_DEPTH`]. types: Vec<&'s [u8]>, @@ -1143,38 +1143,43 @@ pub(super) struct Dictionaries<'s> { name: Option<&'s [u8]>, /// Whether the string just before the token taken is a key that ASCII85 data follows. string_key: bool, - /// Data that starts before this offset has no `~>`, so it isn't searched for again. - no_end_before: usize, + /// The rest of the data has no `~>` or marker to stop at, so they aren't searched for again. + no_end: bool, } -/// The offset just past the `~>` of ASCII85 data at the start of `rest`. `Err` holds the offset of -/// the comment with marker `end` that comes first, or the end of `rest`. A line of the data that -/// reads as that marker counts as the marker. -fn data_end(rest: &[u8], end: Option<&str>) -> Result { +/// 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 { let mut at = 0; for line in rest.split(|b| matches!(b, b'\r' | b'\n')) { - let comment = line.strip_prefix(b"%").and_then(|c| std::str::from_utf8(c).ok()); - if comment.is_some_and(|c| Some(super::marker(c)) == end) { - return Err(at); + // 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 Ok(at + i + 2); + return Some(at + i + 2); } at += line.len() + 1; } - Err(rest.len()) + None } impl<'s> Dictionaries<'s> { - /// Take `tok`, the token `lex` just gave, and skip past the ASCII85 data that follows it. Data - /// without its `~>` before `end`, the end marker of the section being scanned ([`data_end`]), - /// isn't skipped, so the scan still finds that marker. - pub(super) fn pass(&mut self, tok: &super::Tok<'s>, lex: &mut super::Lexer<'s>, end: Option<&str>) { - let at = lex.offset(); - if self.data_follows(tok) && at >= self.no_end_before { - match data_end(lex.rest(), end) { - Ok(end) => lex.seek(at + end), - Err(stop) => self.no_end_before = at + stop, + /// 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)>) { + if self.data_follows(tok) && !self.no_end { + match data_end(lex.rest(), section) { + Some(stop) => lex.seek(lex.offset() + stop), + None => self.no_end = true, } } } @@ -1221,6 +1226,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. diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 30b53a3e2..33e8e10f0 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -4583,6 +4583,55 @@ fn data_without_its_end_does_not_hide_the_sections_end() { } } +/// 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@\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}"); + } +} + /// 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]