mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 01:50:22 -06:00
.ai import: a wraparound group's graphics wrap with no offset
Every graphic in a version 7 wraparound group (`*w` … `*W`), before or after its type, is now a wrap object with an offset of 0 instead of the 6 pt default, and a group with no type in it marks all of its graphics. A graphic before the type is below it, so the type flows around it only after it is moved above. Operands on `*w` or `*W`, and a `*w` inside a compound path, are errors. Docs: docs/file-format-parity.md.
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
//! canvas (16,383 points around the template box's centre) or from edge to edge of one artboard
|
||||
//! is a ruler guide on that layer ([`Document::guides`]); other guide paths stay paths.
|
||||
//! - Groups `u` … `U`, compound paths `*u` … `*U`, clipping groups `q` … `Q` (the path painted
|
||||
//! with `W` clips); a group's transparency and name follow its end.
|
||||
//! with `W` clips); a group's transparency and name follow its end. A wraparound group `*w` …
|
||||
//! `*W` (version 7 specification §15.4.2) is a group whose objects other than its type wrap
|
||||
//! area type, with no offset.
|
||||
//! - Paths: `m`, `l`/`L`, `c`/`C`, `v`/`V`, `y`/`Y`, painted by `N n F f S s B b` (lower case
|
||||
//! closes), `(op) *` for a guide. Stroke `w J j M d`, fill rule `XR`, overprint `O`/`R`.
|
||||
//! - State: hidden `Xw`, locked `A`, transparency `mode opacity isolate knockout shape Xy`.
|
||||
|
||||
@@ -98,13 +98,13 @@ impl<'a> Reader<'a> {
|
||||
self.pending_unread_fill = pending_unread_fill;
|
||||
self.pending_unread_stroke = pending_unread_stroke;
|
||||
}
|
||||
// Wraparound text: the objects after its text object are wrap objects, at the default
|
||||
// options (the format gives none) unless their dictionaries give theirs.
|
||||
if matches!(kind, Kind::Wrap)
|
||||
&& let Some(text) = kids.iter().position(|k| matches!(k.kind, NodeKind::Text(_)) || slot_of(k.name.as_deref()).is_some())
|
||||
{
|
||||
for k in kids.iter_mut().skip(text + 1).filter(|k| k.extras().wrap.is_none()) {
|
||||
k.extras_mut().wrap = Some(TextWrap::default());
|
||||
// Wraparound text: every object in it other than type is a wrap object, before the text or
|
||||
// after it, with no offset (the format gives none) unless its dictionary gives its options.
|
||||
// One before the text is below it, so the text flows around it only once it is moved above.
|
||||
if matches!(kind, Kind::Wrap) {
|
||||
let graphics = kids.iter_mut().filter(|k| !matches!(k.kind, NodeKind::Text(_)) && slot_of(k.name.as_deref()).is_none());
|
||||
for k in graphics.filter(|k| k.extras().wrap.is_none()) {
|
||||
k.extras_mut().wrap = Some(TextWrap { offset: 0.0, invert: false });
|
||||
}
|
||||
}
|
||||
// A group holding only clipping paths (`q … u <path> W n U … Q`) doesn't clip on its own: `u`/`U` don't save
|
||||
|
||||
@@ -125,6 +125,11 @@ impl<'a> Reader<'a> {
|
||||
self.end_path_quietly();
|
||||
self.open(Kind::Compound, hidden)?
|
||||
}
|
||||
// A wraparound group takes no operands and isn't an element of a compound path.
|
||||
"*w" | "*W" if !vals.is_empty() => return Err(format!("`{w}` takes no operands")),
|
||||
"*w" if self.frames.iter().any(|f| matches!(f.kind, Kind::Compound)) => {
|
||||
return Err("a wraparound group is inside a compound path".into());
|
||||
}
|
||||
"*w" => {
|
||||
self.end_path_quietly();
|
||||
self.open(Kind::Wrap, hidden)?
|
||||
|
||||
@@ -142,8 +142,8 @@ pub(super) struct LayerAttrs {
|
||||
pub(super) enum Kind {
|
||||
Layer(Box<LayerAttrs>),
|
||||
Group,
|
||||
/// Wraparound text (`*w` … `*W`, version 7 specification §15.4.2): a group whose objects after
|
||||
/// its text object are wrap objects.
|
||||
/// Wraparound text (`*w` … `*W`, version 7 specification §15.4.2): a group whose objects other
|
||||
/// than its type are wrap objects.
|
||||
Wrap,
|
||||
Compound,
|
||||
Clip,
|
||||
|
||||
@@ -3337,12 +3337,41 @@ fn legacy_wrap_group() {
|
||||
assert_eq!(kids.len(), 1, "{:?}", r.warnings);
|
||||
let NodeKind::Group { children, clip: false } = &kids[0].kind else { panic!("a group: {:?}", kids[0].kind) };
|
||||
let wraps: Vec<(bool, bool)> = children.iter().map(|c| (matches!(c.kind, NodeKind::Text(_)), wrap_of(c).is_some())).collect();
|
||||
assert_eq!(wraps, [(false, false), (true, false), (false, true), (false, true)], "{:?}", r.warnings);
|
||||
assert_eq!(wrap_of(&children[2]), Some(vectorcraft_doc::TextWrap::default()));
|
||||
assert_eq!(wraps, [(false, true), (true, false), (false, true), (false, true)], "objects before the text are wrap objects too: {:?}", r.warnings);
|
||||
let none = Some(vectorcraft_doc::TextWrap { offset: 0.0, invert: false });
|
||||
assert_eq!(wrap_of(&children[0]), none, "no offset");
|
||||
assert_eq!(wrap_of(&children[2]), none, "no offset");
|
||||
assert_eq!(wrap_of(&children[3]), Some(vectorcraft_doc::TextWrap { offset: 3.0, invert: true }));
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("*w") || w.contains("*W")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A wraparound group with no type in it: each of its objects is a wrap object, with no offset.
|
||||
#[test]
|
||||
fn legacy_wrap_group_without_text() {
|
||||
let body = format!("*w\n{RED}{}f\n{}f\n*W\n", rect_path(0.0, 0.0, 5.0, 5.0), rect_path(30.0, 30.0, 50.0, 50.0));
|
||||
let r = read_ok(&layer(&body));
|
||||
let kids = art(&r.doc);
|
||||
assert_eq!(kids.len(), 1, "{:?}", r.warnings);
|
||||
let NodeKind::Group { children, clip: false } = &kids[0].kind else { panic!("a group: {:?}", kids[0].kind) };
|
||||
let wraps: Vec<_> = children.iter().map(|c| wrap_of(c)).collect();
|
||||
assert_eq!(wraps, [Some(vectorcraft_doc::TextWrap { offset: 0.0, invert: false }); 2]);
|
||||
}
|
||||
|
||||
/// `*w` and `*W` take no operands, and a wraparound group can't be an element of a compound path.
|
||||
#[test]
|
||||
fn legacy_wrap_group_errors() {
|
||||
let square = format!("{RED}{}f\n", rect_path(0.0, 0.0, 5.0, 5.0));
|
||||
for (body, error) in [
|
||||
(format!("1 *w\n{square}*W\n"), "`*w` takes no operands"),
|
||||
(format!("*w\n{square}0 *W\n"), "`*W` takes no operands"),
|
||||
(format!("*u\n*w\n{square}*W\n*U\n"), "a wraparound group is inside a compound path"),
|
||||
(format!("*u\nu\n*w\n{square}*W\nU\n*U\n"), "a wraparound group is inside a compound path"),
|
||||
] {
|
||||
let e = read(stream(&layer(&body)).as_bytes()).unwrap_err();
|
||||
assert!(e.contains(error), "{body:?}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The printer's crop marks Illustrator draws after the last layer (`%AI3_BeginCrops`, inside
|
||||
/// `userdict /AI3_noCropMarks known not { … } if`) aren't art outside the layers: the file reads
|
||||
/// from its layers, without them (the crop area is `%AI3_Cropmarks`).
|
||||
|
||||
Reference in New Issue
Block a user