mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Import: read one alternative of versioned content, not two
Versioned content can hold three alternatives: newer files write a Dimension tool group, the same group again after `%AI17_Alternate_Content_if_version_gt:…` for a version in between, then plain art after `%AI17_Alternate_Content`. The reader skipped from the plain `Alternate_Content` marker only, so the middle alternative was read as art too and those objects came in twice. The reader now reads the first alternative and skips from the next one, whatever its marker, to the end of the block, nested blocks included. A block that doesn't end is read whole with a note instead of failing the file. The palette reader tracked alternatives with one flag that a block nested in an alternative reset; it now tracks the depth.
This commit is contained in:
@@ -92,6 +92,11 @@ impl<'a> Lexer<'a> {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Look for the next token at `pos` again (the end of the data at most).
|
||||
pub fn seek(&mut self, pos: usize) {
|
||||
self.pos = pos.min(self.src.len());
|
||||
}
|
||||
|
||||
/// The data from `from` to `to` (empty where that isn't in it).
|
||||
pub fn slice(&self, from: usize, to: usize) -> &'a [u8] {
|
||||
self.src.get(from..to).unwrap_or_default()
|
||||
|
||||
@@ -69,19 +69,25 @@ pub(super) fn palette(data: &[u8]) -> Palette {
|
||||
let mut cell: Option<Cell> = None;
|
||||
// Inside a gradient's `Bb` … `BB`.
|
||||
let mut in_gradient = false;
|
||||
// Inside versioned content's alternate, which repeats what came before it.
|
||||
let mut alternate = false;
|
||||
// How deep in versioned content, and the depth of the block whose later alternatives (which
|
||||
// repeat its first) are being skipped, nested blocks in them included.
|
||||
let (mut depth, mut skipping) = (0usize, None::<usize>);
|
||||
while let Some(t) = lex.next_token() {
|
||||
if let Tok::Comment(c) = t.tok {
|
||||
let line = String::from_utf8_lossy(c);
|
||||
if line.contains("_Alternate_Content") {
|
||||
alternate = true;
|
||||
} else if line.contains("_End_Versioned_Content") || line.contains("_Begin_Content_if_version") {
|
||||
alternate = false;
|
||||
if line.contains("_Begin_Content_if_version") {
|
||||
depth = depth.saturating_add(1);
|
||||
} else if line.contains("_Alternate_Content") {
|
||||
skipping = skipping.or(Some(depth));
|
||||
} else if line.contains("_End_Versioned_Content") {
|
||||
if skipping == Some(depth) {
|
||||
skipping = None;
|
||||
}
|
||||
depth = depth.saturating_sub(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if alternate {
|
||||
if skipping.is_some() {
|
||||
continue;
|
||||
}
|
||||
let w = match t.tok {
|
||||
@@ -211,6 +217,19 @@ mod tests {
|
||||
assert_eq!(p.swatches.len(), 1);
|
||||
}
|
||||
|
||||
/// Three alternatives, and versioned content nested in a later one: still one swatch.
|
||||
#[test]
|
||||
fn versioned_swatches_with_more_alternatives_are_read_once() {
|
||||
let p = palette(
|
||||
b"0 0 Pb\n%AI17_Begin_Content_if_version_gt:24 15\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n\
|
||||
%AI17_Alternate_Content_if_version_gt:24 10\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n\
|
||||
%AI17_Begin_Content_if_version_gt:23 1\n0 0 0 1 k\n(Black)\nPc\n%AI17_Alternate_Content\n%AI17_End_Versioned_Content\n\
|
||||
0 0 1 0 k\n(Yellow)\nPc\n\
|
||||
%AI17_Alternate_Content\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n%AI17_End_Versioned_Content\n0 0 0 1 k\n(After)\nPc\nPB\n",
|
||||
);
|
||||
assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["Red ink", "After"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_it_doesnt_know_leaves_the_other_swatches() {
|
||||
let p = palette(b"0 0 Pb\n1 2 3 Zz\n(Odd)\nPc\n(no paint) Pc\n0 0 0 1 k\n(Black)\nPc\nPB\n");
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) use document::{slot_frame, slot_of};
|
||||
pub use types::IMAGE_TOO_LARGE;
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, Place, Raster};
|
||||
#[cfg(test)]
|
||||
pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD};
|
||||
pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED};
|
||||
pub(super) use types::{blend_mode, blend_name};
|
||||
|
||||
#[path = "legacy_text.rs"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::art::{four, sane, two};
|
||||
use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, PatternHead, Place, Raster, SKIPPED};
|
||||
use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED};
|
||||
use super::{Arc, Node, PatternDef, Reader, Rect, Tok, V, marker};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -69,6 +69,18 @@ impl<'a> Reader<'a> {
|
||||
if m == "BeginEncoding" {
|
||||
self.type_state.encoding_comment(line);
|
||||
}
|
||||
// Versioned content holds alternatives of the same art, the newest first: the first is
|
||||
// read, and the next one (`%AI17_Alternate_Content`, or `…_if_version_gt:` for a version
|
||||
// in between) starts what is skipped, up to the end of the block, nested blocks included.
|
||||
if m.starts_with("Alternate_Content") {
|
||||
let at = self.lex.offset();
|
||||
if self.skip_to("End_Versioned_Content").is_err() {
|
||||
// A block that doesn't end: the rest of the file is read rather than lost.
|
||||
self.lex.seek(at);
|
||||
self.warn(VERSIONED_UNENDED);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some((_, end)) = SKIPPED.iter().find(|(b, _)| *b == m) {
|
||||
return self.skip_to(end);
|
||||
}
|
||||
|
||||
@@ -244,9 +244,12 @@ pub(super) const SKIPPED: &[(&str, &str)] = &[
|
||||
("BeginSymbolList", "EndSymbolList"),
|
||||
("BeginTextDocument", "EndTextDocument"),
|
||||
("BeginEncoding", "EndEncoding"),
|
||||
("Alternate_Content", "End_Versioned_Content"),
|
||||
];
|
||||
|
||||
/// The note for versioned content whose `%AI17_End_Versioned_Content` is missing: every
|
||||
/// alternative after the first is read too, so some art may come in twice.
|
||||
pub const VERSIONED_UNENDED: &str = "versioned content that doesn't end was read whole: some art may come in twice";
|
||||
|
||||
/// The note for a `/ForeignObject` (non-native art, the content of a placed PDF, which the editor
|
||||
/// shows but doesn't edit) whose PDF can't be read.
|
||||
pub const NON_NATIVE_ART_UNREAD: &str = "non-native art (a placed PDF's content) that VectorCraft can't read is left out";
|
||||
|
||||
@@ -953,6 +953,66 @@ fn sections_it_skips() {
|
||||
assert_eq!(art(&doc).len(), 1);
|
||||
}
|
||||
|
||||
/// A filled square at x = `10 × i` (5 pt wide), to tell the alternatives of versioned content apart.
|
||||
fn alt_square(i: u32) -> String {
|
||||
let x = f64::from(i) * 10.0;
|
||||
rect_path(x, 0.0, x + 5.0, 5.0) + "f\n"
|
||||
}
|
||||
|
||||
/// The left edges of the layer's art, in order: which [`alt_square`]s came in.
|
||||
fn squares(doc: &Document) -> Vec<f64> {
|
||||
art(doc).iter().filter_map(|n| n.geometric_bounds()).map(|b| b.x0.round()).collect()
|
||||
}
|
||||
|
||||
/// Versioned content holds alternatives of the same art for newer and older versions (newer
|
||||
/// files write a Dimension tool group, then the same group again for a version in between, then
|
||||
/// plain art): only the first alternative is read, so the art comes in once, however many
|
||||
/// alternatives follow and whatever versioned content is nested in them.
|
||||
#[test]
|
||||
fn versioned_content_reads_one_alternative() {
|
||||
let (begin, between, alternate, end) = (
|
||||
"%AI17_Begin_Content_if_version_gt:24 11\n",
|
||||
"%AI17_Alternate_Content_if_version_gt:24 10\n",
|
||||
"%AI17_Alternate_Content\n",
|
||||
"%AI17_End_Versioned_Content\n",
|
||||
);
|
||||
let (a, b, c) = (alt_square(1), alt_square(2), alt_square(3));
|
||||
let cases = [
|
||||
("one alternative", format!("{begin}{a}{end}"), vec![10.0]),
|
||||
("two alternatives", format!("{begin}{a}{alternate}{b}{end}"), vec![10.0]),
|
||||
("three alternatives", format!("{begin}{a}{between}{b}{alternate}{c}{end}"), vec![10.0]),
|
||||
// Blocks nested in the first alternative read their own first; those in a later one aren't read.
|
||||
(
|
||||
"nested",
|
||||
format!(
|
||||
"{begin}{a}{begin}{b}{alternate}{c}{end}{between}{}{begin}{}{alternate}{}{end}{alternate}{}{end}",
|
||||
alt_square(4),
|
||||
alt_square(5),
|
||||
alt_square(6),
|
||||
alt_square(7)
|
||||
),
|
||||
vec![10.0, 20.0],
|
||||
),
|
||||
// Art after the block is read as usual.
|
||||
("art after it", format!("{begin}{a}{between}{b}{alternate}{c}{end}{}", alt_square(8)), vec![10.0, 80.0]),
|
||||
];
|
||||
for (case, body, want) in cases {
|
||||
let s = read_ok(&layer(&body));
|
||||
assert_eq!(squares(&s.doc), want, "{case}");
|
||||
assert!(!s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{case}: {:?}", s.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Versioned content whose end is missing is read whole, with a note, rather than losing the rest
|
||||
/// of the file.
|
||||
#[test]
|
||||
fn versioned_content_that_does_not_end_is_read_with_a_note() {
|
||||
let body = format!("%AI17_Begin_Content_if_version_gt:24 11\n{}%AI17_Alternate_Content_if_version_gt:24 10\n{}", alt_square(1), alt_square(2));
|
||||
let s = read_ok(&layer(&body));
|
||||
assert_eq!(squares(&s.doc), [10.0, 20.0]);
|
||||
assert!(s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{:?}", s.warnings);
|
||||
}
|
||||
|
||||
/// Non-native art (a placed PDF's content) keeps its PDF as ASCII85 in comment lines after
|
||||
/// `/Data ,`. ASCII85 has `_`, so a line may start with `%_`, which otherwise reads as hidden
|
||||
/// tokens, and a `(` in it would swallow the rest of the layer (a letterhead saved by Illustrator
|
||||
|
||||
Reference in New Issue
Block a user