.ai import: objects that draw differently from the page come in as its art, and the layers stay

When the layers fail the comparison with the file's own page because some objects draw
differently (an object in another color, or one that only one side draws), the import finds
each top-level object whose bounds hold a patch of such pixels (the smallest one that holds it)
and replaces it with the page's art in its bounds, in the same way as an unread object. It then
compares the layers with the page again. When that comparison passes, the layers stay, and a
warning names the layer or sublayer of each replaced object (`<Layer>` for one without a name)
and how many were replaced on it. Like the warning for unread objects, this warning counts as
a loss, so an export or Save As to the file's own path needs `acknowledgeLoss: true`. A
replaced wrap object keeps its wrap options, and area type is wrapped again around the page's
art. Placed files at their other placement are tried too when the first placement can't be
kept.

The file still comes in as its PDF part, with the same reason, when only edges or type differ,
when a difference lies outside every top-level object, when more than half of the top-level
objects (or more than 64) hold one, when such an object is or holds type or holds a wrap object
inside a group, when the page's type of a text object that stays would be copied into a
replacement, when the page's art can't replace an object for the reasons it can't replace an
unread one, when matching the differences to the objects would take too much work, or when the
second comparison still fails. Trusting the editing data keeps the objects as read.
This commit is contained in:
dotCooCoo
2026-11-04 09:43:28 +07:00
committed by bflatastic
parent 8cf4b90a65
commit 5e495cf900
32 changed files with 505 additions and 16 deletions
@@ -127,6 +127,45 @@ fn an_ai_unread_group_keeps_its_layers_and_live_neighbor() {
assert!(result["warnings"].to_string().contains("`ZZ` (1)"), "{result}");
}
#[test]
fn a_differing_object_keeps_the_layers_with_a_note() {
let square = |cmyk: &str, x: f64| format!("{cmyk} k\n{}f\n", ai::rect(x, 40.0, x + 16.0, 56.0));
let front = square("0 0 0 1", 90.0) + &square("0 0 1 0", 160.0);
let data = ai::editing_data(200.0, 100.0, &(ai::layer("Back", &square("0 0 1 0", 20.0)) + &ai::layer("Front", &front)));
let page = "0 0 1 0 k 20 40 16 16 re f 1 0 0 0 k 90 40 16 16 re f 0 0 1 0 k 160 40 16 16 re f";
let mut session = Session::new();
let result = open(&mut session, "differs.ai", &ai::ai(&compressed(data.as_bytes()), page), json!({})).unwrap();
let doc = &session.doc().unwrap().doc;
assert_eq!(names(&doc.layers), ["Back", "Front"], "{result}");
let front = doc.layers[1].children().unwrap();
assert!(matches!(front[0].kind, NodeKind::Group { .. }) && matches!(front[1].kind, NodeKind::Path { .. }), "{result}");
let replacement = &front[0].children().unwrap()[0];
assert!(
matches!(replacement.appearance.items.first(), Some(vectorcraft_doc::AppearanceItem::Fill(fill))
if fill.paint == vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0))),
"{result}"
);
assert!(
result["warnings"].to_string().contains("objects that drew differently from the file's own page came in as its art: Front (1)"),
"{result}"
);
// An export to the file's own path is refused unless it is given `acknowledgeLoss`.
let d = std::env::temp_dir().join(format!("vc-aidiffers-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let path = d.join("differs.ai");
let file = ai::ai(&compressed(data.as_bytes()), page);
std::fs::write(&path, &file).unwrap();
let path = path.to_string_lossy().to_string();
let mut s = Session::new();
s.execute("document.open", &json!({"path": path})).unwrap();
let e = s.execute("document.export", &json!({"path": path})).unwrap_err().to_string();
assert!(e.contains("left things out") && e.contains("objects that drew differently"), "{e}");
assert_eq!(std::fs::read(&path).unwrap(), file);
s.execute("document.export", &json!({"path": path, "acknowledgeLoss": true})).unwrap();
assert_ne!(std::fs::read(&path).unwrap(), file);
std::fs::remove_dir_all(d).ok();
}
/// A palette of three swatches and a color group `Inks` holding a fourth.
const INKS_PALETTE: &str = "%AI5_BeginPalette\n0 0 Pb\n0 0 0 0 1 1 1 Xa\n(White)\nPc\n0.04 0.3 0 0 81.96 21 -8 (Pink ink) 0 2 Xx\n(Pink ink)\nPc\n\
0.06 0.98 1 0 (Red ink) 0 x\n(Red ink)\nPc\n1 (Inks) 1 Pg\n0.02 0.1 1 0 (Yellow ink) 0 x\n(Yellow ink)\nPc\nPB\n%AI5_EndPalette\n";
+148 -7
View File
@@ -1,8 +1,8 @@
use super::compare::{MAX_DIFFERENCE, PageView, compare, difference};
use super::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, compare, difference};
use super::plan::{made_only, make_slots, plan, readable_all};
use super::slots::{
ART_LEFT_OUT, Content, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, extent, few,
fill_slots, letters, text_objects, words,
ART_LEFT_OUT, Content, DREW_DIFFERENTLY, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots,
extent, few, fill_slots, letters, text_objects, words,
};
use super::{Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, Vec2, ai};
@@ -341,10 +341,22 @@ pub(super) fn build_from(
doc = other;
notes.extend(unseen_note(unseen).into_iter().chain(note));
}
_ => {
let more = drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, &notes, unseen, trust, why, reflow)?;
notes.extend(more);
}
other => match drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, &notes, unseen, trust, why, reflow) {
Ok(more) => notes.extend(more),
// The objects that draw differently from the page come in as its art, from
// the placement that draws closer to it.
Err(why) => {
let other = other.map(|(_, other, unseen)| (other, unseen));
let placements = closer_first(&frame, &mut view, (doc, unseen), other, reflow);
let Some((kept, unseen, kept_notes)) = placements.into_iter().find_map(|(base, unseen)| {
keep_differing(&base, &frame, &mut view, &threaded_frames, &shown, reflow).map(|(kept, n)| (kept, unseen, n))
}) else {
return Err(why);
};
doc = kept;
notes.extend(unseen_note(unseen).into_iter().chain(kept_notes));
}
},
},
}
}
@@ -766,6 +778,135 @@ fn replace_unread(
Ok(counts.into_iter().map(|(kind, count)| format!("{kind} ({count})")).collect())
}
/// Most objects of the layers that come in as the page's art where they draw differently from it.
const MAX_DIFFERING: usize = 64;
/// Most pairs of a patch where the layers and the page differ and a top-level object that may hold
/// it, weighed to find the objects that come in as the page's art.
const MAX_PATCH_PAIRS: usize = 4_000_000;
/// The layers as read and at the placed files' other placement (each with its count of text
/// objects the page doesn't draw), the one that draws closer to the page first. `reflow` as in
/// [`PageView::difference`].
fn closer_first(
frame: &Frame<'_>,
view: &mut PageView,
doc: (Document, usize),
other: Option<(Document, usize)>,
reflow: bool,
) -> Vec<(Document, usize)> {
match other {
Some(other) if view.difference(&other.0, frame.rect, reflow).any < view.difference(&doc.0, frame.rect, reflow).any => vec![other, doc],
Some(other) => vec![doc, other],
None => vec![doc],
}
}
/// `doc`, whose layers draw differently from the page (an object in another color, or one that only
/// one of them draws), with each of its top-level objects that holds such a difference replaced by the
/// page's art in its bounds, when that makes the two look alike → the document and its notes: the
/// comparison's, and how many objects were replaced on each layer (by name, in the layers' order).
/// `None` when only edges or type differ, a difference lies outside every object, more than half
/// of the objects (or more than [`MAX_DIFFERING`]) hold one, one of them holds type or a wrap
/// object, the page's art can't replace one of them ([`replace_unread`]), the page's type of a text
/// object that stays is copied into a replacement, or the two still differ. `shown` is the page's
/// type of each text object that shows. `reflow` as in [`PageView::difference`].
fn keep_differing(
doc: &Document,
frame: &Frame<'_>,
view: &mut PageView,
threaded_frames: &BTreeSet<NodeId>,
shown: &[Vec<Arc<Node>>],
reflow: bool,
) -> Option<(Document, Vec<String>)> {
/// The top-level objects that show and print (the outermost objects under a layer or
/// sublayer), with the name of that layer or sublayer as the Layers panel shows it and their
/// bounds.
fn top_level(nodes: &[Arc<Node>], layer: &str, out: &mut Vec<(String, NodeId, Rect)>) {
for n in nodes.iter().filter(|n| n.visible) {
match &n.kind {
NodeKind::Layer { printable: false, .. } => {}
NodeKind::Layer { .. } => {
let name = Some(n.display_name()).filter(|name| !name.is_empty()).unwrap_or_else(|| format!("<{}>", n.kind_label()));
top_level(n.children().map_or(&[], Vec::as_slice), &name, out);
}
NodeKind::Path { guide: true, .. } => {}
_ => {
if let Some(b) = n.visual_bounds().filter(|b| b.area().is_finite() && b.area() > 0.0) {
out.push((layer.to_string(), n.id, b));
}
}
}
}
}
// Where only spread edges or type differ, or only the art around the page, no object is
// replaced.
let (diff, regions) = view.solid_regions(doc, frame.rect, reflow)?;
if diff.solid <= MAX_SOLID_DIFFERENCE {
return None;
}
let mut objects = vec![];
top_level(&doc.layers, "", &mut objects);
objects.retain(|(_, _, b)| !b.intersect(frame.rect).is_zero_area());
if regions.len().saturating_mul(objects.len()) > MAX_PATCH_PAIRS {
return None;
}
// Each patch is the smallest object's that holds it.
let most = MAX_DIFFERING.min(objects.len() / 2);
let mut targets = BTreeSet::new();
for region in regions {
let (_, id, _) = objects.iter().filter(|(_, _, b)| inside(region, *b)).min_by(|a, b| a.2.area().total_cmp(&b.2.area()))?;
targets.insert(*id);
if targets.len() > most {
return None;
}
}
if targets.is_empty() {
return None;
}
// A replaced wrap object keeps its wrap options, and the area type is wrapped around the page's
// art that replaces it. An object inside a replaced group can't keep them, so such a group isn't
// replaced. Neither is an object that holds type: the page's art that replaces it would hold
// type the page draws for other text objects too.
let mut wraps = vec![];
for id in &targets {
let node = doc.node(*id)?;
let mut held = matches!(node.kind, NodeKind::Text(_));
for child in node.children().into_iter().flatten() {
child.walk(&mut |n| held |= n.extras().wrap.is_some() || matches!(n.kind, NodeKind::Text(_)));
}
if held {
return None;
}
if let Some(wrap) = node.extras().wrap {
wraps.push((*id, wrap));
}
}
let mut kept = doc.clone();
let replace: Vec<(NodeId, Vec<String>)> = targets.iter().map(|id| (*id, vec![])).collect();
let mut copied = BTreeSet::new();
replace_unread(&mut kept, frame, &replace, &[], threaded_frames, &mut copied).ok()?;
// A text object that stays draws its page type, so a copy of that type would draw it twice.
if shown.iter().flatten().any(|n| copied.contains(&n.id)) {
return None;
}
for (id, wrap) in wraps {
kept.node_mut(id)?.extras_mut().wrap = Some(wrap);
}
resolve_text_wrap(&mut kept).ok()?;
let note = compare(frame, view, &kept, reflow).ok()?;
let mut layers: Vec<(String, usize)> = vec![];
for (layer, _, _) in objects.iter().filter(|(_, id, _)| targets.contains(id)) {
match layers.iter_mut().find(|(name, _)| name == layer) {
Some((_, count)) => *count += 1,
None => layers.push((layer.clone(), 1)),
}
}
let layers: Vec<String> = layers.into_iter().map(|(layer, count)| format!("{layer} ({count})")).collect();
Some((kept, note.into_iter().chain([format!("{DREW_DIFFERENTLY}: {}", layers.join(", "))]).collect()))
}
/// Copy page drawing IDs, including mask art outside the ordinary child tree.
fn reid_page_art(doc: &mut Document, source: &Node) -> Node {
let mut copy = doc.reid(source);
+75 -7
View File
@@ -182,6 +182,12 @@ impl PageView {
let printed = as_printed(b);
let b = printed.as_ref().unwrap_or(b);
let y = comparison_render(&mut self.layers, b, rb, self.scale, self.samples);
let areas = self.counted_type(b, rb, reflow);
compared(&self.page, &y, &areas, &self.restyled)
}
/// Where [`Self::difference`] counts `b`'s and the page's pixels as type, in the page's pixels.
fn counted_type(&self, b: &Document, rb: Rect, reflow: bool) -> Vec<Rect> {
let mut areas = vec![];
type_areas(&b.layers, rb, self.scale, &mut areas);
if reflow {
@@ -194,7 +200,7 @@ impl PageView {
} else {
areas.extend(&self.page_type);
}
compared(&self.page, &y, &areas, &self.restyled)
areas
}
/// The share (0 to 1) of the pixels where `b` on `rb` draws another colour than the page
@@ -210,6 +216,52 @@ impl PageView {
let differing = self.page.pixels.as_chunks::<4>().0.iter().zip(y.pixels.as_chunks::<4>().0).filter(|(p, q)| differ_in_colour(p, q)).count();
differing as f64 / (f64::from(y.width) * f64::from(y.height)).max(1.0)
}
/// How `b` on `rb` draws differently from the page ([`Self::difference`]), and the boxes (in
/// `b`'s terms) around each patch of the pixels counted in its `solid` share (they and all
/// their neighbors differ, away from type), neighbors touching corner to corner counting as one patch. `None` when the two
/// drawings aren't the same size.
pub(super) fn solid_regions(&mut self, b: &Document, rb: Rect, reflow: bool) -> Option<(Difference, Vec<Rect>)> {
let printed = as_printed(b);
let b = printed.as_ref().unwrap_or(b);
let y = comparison_render(&mut self.layers, b, rb, self.scale, self.samples);
let areas = self.counted_type(b, rb, reflow);
let (diff, mut solid) = compared_with_solid(&self.page, &y, &areas, &self.restyled);
let (w, h) = (y.width as usize, y.height as usize);
if solid.len() != w * h {
return None;
}
let mut regions = vec![];
let mut stack = vec![];
for start in 0..solid.len() {
if !solid.get(start).copied().unwrap_or(false) {
continue;
}
// The patch's pixels are cleared as they are taken, so each is taken once.
if let Some(s) = solid.get_mut(start) {
*s = false;
}
stack.push(start);
let (mut i0, mut j0, mut i1, mut j1) = (start % w, start / w, start % w, start / w);
while let Some(k) = stack.pop() {
let (i, j) = (k % w, k / w);
(i0, j0, i1, j1) = (i0.min(i), j0.min(j), i1.max(i), j1.max(j));
for n in j.saturating_sub(1)..=(j + 1).min(h - 1) {
for m in i.saturating_sub(1)..=(i + 1).min(w - 1) {
if let Some(s) = solid.get_mut(n * w + m).filter(|s| **s) {
*s = false;
stack.push(n * w + m);
}
}
}
}
let s = self.scale;
let at = |i: usize, j: usize| (rb.x0 + i as f64 / s, rb.y0 + j as f64 / s);
let ((x0, y0), (x1, y1)) = (at(i0, j0), at(i1 + 1, j1 + 1));
regions.push(Rect::new(x0, y0, x1, y1));
}
Some((diff, regions))
}
}
/// How `a` on `ra` and `b` on `rb` (rectangles of the same size) draw differently ([`compared`]),
@@ -233,8 +285,14 @@ pub(super) fn difference(a: &Document, ra: Rect, b: &Document, rb: Rect) -> Diff
/// at all ([`differ`]): the page of an RGB document is written in the colours' CMYK equivalents,
/// and what the comparison looks for is art that is missing or out of place.
pub(super) fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect], skip: &[Rect]) -> Difference {
compared_with_solid(x, y, areas, skip).0
}
/// [`compared`], and which of the pixels (row by row) are counted in its `solid` share: none when
/// the drawings can't be compared.
fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect], skip: &[Rect]) -> (Difference, Vec<bool>) {
if x.width == 0 || x.height == 0 || x.width != y.width || x.height != y.height {
return Difference { any: 1.0, art: 1.0, solid: 1.0 };
return (Difference { any: 1.0, art: 1.0, solid: 1.0 }, vec![]);
}
let (w, h) = (x.width as usize, x.height as usize);
// Pixels in `skip` count as alike.
@@ -245,12 +303,22 @@ pub(super) fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render:
let typed = in_areas(areas, w, h);
let art = differs.iter().zip(&typed).filter(|(d, t)| **d && !**t).count();
// A pixel on the region's edge has neighbours outside it, which don't count as differing.
let solid = (1..h.saturating_sub(1))
.flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j)))
.filter(|&(i, j)| !typed.get(j * w + i).copied().unwrap_or(true) && (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n))))
.count();
let mut solid = vec![false; w * h];
for (i, j) in (1..h.saturating_sub(1)).flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j))) {
if !typed.get(j * w + i).copied().unwrap_or(true)
&& (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n)))
&& let Some(s) = solid.get_mut(j * w + i)
{
*s = true;
}
}
let area = (w as f64 * h as f64).max(1.0);
Difference { any: differs.iter().filter(|d| **d).count() as f64 / area, art: art as f64 / area, solid: solid as f64 / area }
let shares = Difference {
any: differs.iter().filter(|d| **d).count() as f64 / area,
art: art as f64 / area,
solid: solid.iter().filter(|s| **s).count() as f64 / area,
};
(shares, solid)
}
/// Where the type that shows in `nodes` is, in pixels of `region` drawn at `scale` (with a margin
+7 -1
View File
@@ -18,6 +18,10 @@ pub(super) const PAGE_TYPE_UNCHECKED: &str =
pub(super) const LAYERS_UNREAD: &str = "the file's layers weren't read";
/// Objects of the layers replaced with the page's art where they drew differently from it,
/// followed by the layers they are on.
pub(super) const DREW_DIFFERENTLY: &str = "objects that drew differently from the file's own page came in as its art";
/// Does this import note say that the file has something the document doesn't (hidden text, art or
/// layers that could not be read, or the page's type that no text object takes or may not take)?
/// Writing the document over the file would lose it for good.
@@ -27,7 +31,9 @@ pub fn is_loss(note: &str) -> bool {
|| note.ends_with("they came in as the page's art")
|| note == PAGE_TYPE_LEFT_OUT
|| note == PAGE_TYPE_UNCHECKED
|| [ART_LEFT_OUT, LAYERS_UNREAD, crate::import::graphics::TOO_MUCH, crate::import::graphics::FAR_AWAY].iter().any(|n| note.starts_with(n))
|| [ART_LEFT_OUT, LAYERS_UNREAD, DREW_DIFFERENTLY, crate::import::graphics::TOO_MUCH, crate::import::graphics::FAR_AWAY]
.iter()
.any(|n| note.starts_with(n))
}
/// Most text objects in the layers, and most pieces of type in one place on the page, that are matched
+209
View File
@@ -824,6 +824,215 @@ fn top_level_placed_file_without_page_art_falls_back() {
assert!(notes.iter().any(|n| n.contains("page art wasn't found")), "{notes:?}");
}
const BLACK: &str = "0 0 0 1";
const YELLOW: &str = "0 0 1 0";
const PAGE_CYAN: [f32; 4] = [1.0, 0.0, 0.0, 0.0];
const PAGE_YELLOW: [f32; 4] = [0.0, 0.0, 1.0, 0.0];
/// A square of 16 points at (`x`, 10) in the format's operators, painted in CMYK `cmyk`.
fn square_16(x: u32, cmyk: &str) -> String {
format!("{cmyk} k\n{x} 10 m\n{} 10 L\n{} 26 L\n{x} 26 L\nf", x + 16, x + 16)
}
/// A 100 × 100 page with a square of 16 points at each (`x`, `y`, CMYK) of `squares`: at `y` 74
/// where [`square_16`] puts the layers' squares.
fn page_16(squares: &[(f64, f64, [f32; 4])]) -> Document {
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
for (i, (x, y, [c, m, ye, k])) in squares.iter().enumerate() {
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(*x, *y, x + 16.0, y + 16.0));
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(*c, *m, *ye, *k));
let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint));
let n = Node::path(d.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() });
d.insert(Some(layer), i, n).unwrap();
}
d
}
/// Three squares on two layers, the middle one black in the layers and cyan on the page.
fn middle_differs() -> (String, Document) {
let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, YELLOW));
let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front)));
(art, page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]))
}
#[test]
fn an_object_that_draws_differently_comes_in_as_the_page_art() {
let (art, page) = middle_differs();
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(read, "{notes:?}");
assert_eq!(names(&doc), ["Back", "Front"]);
assert_eq!(kinds(&doc.layers[0]), ["path"]);
assert_eq!(kinds(&doc.layers[1]), ["group[1]", "path"]);
replacement_is_cyan(&doc, 1, 0);
let note = "objects that drew differently from the file's own page came in as its art: Front (1)";
assert!(notes.iter().any(|n| n == note && crate::is_loss(n)), "{notes:?}");
assert!(!notes.iter().any(|n| n.contains("weren't read")), "{notes:?}");
}
#[test]
fn two_differing_objects_on_different_layers_are_both_replaced() {
let back = format!("{}\n{}", square_16(4, BLACK), square_16(28, YELLOW));
let front = format!("{}\n{}", square_16(52, YELLOW), square_16(76, BLACK));
let art = native(&(layer("Back", true, &back) + &layer("Front", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (28.0, 74.0, PAGE_YELLOW), (52.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_CYAN)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(read, "{notes:?}");
assert_eq!(names(&doc), ["Back", "Front"]);
assert_eq!(kinds(&doc.layers[0]), ["group[1]", "path"]);
assert_eq!(kinds(&doc.layers[1]), ["path", "group[1]"]);
replacement_is_cyan(&doc, 0, 0);
replacement_is_cyan(&doc, 1, 1);
let note = "objects that drew differently from the file's own page came in as its art: Back (1), Front (1)";
assert!(notes.iter().any(|n| n == note), "{notes:?}");
}
/// The text wrap options of a 2-point offset, for the object before them.
const WRAP_OPTIONS: &str = "%_/ArtDictionary :\n%_0 /Bool (AI11TextWrapInvert) ,\n%_2 /Real (AI11TextWrapOffset) ,\n%_;\n%_\n";
#[test]
fn a_differing_wrap_object_keeps_its_wrap_options() {
let front = format!("{}\n{WRAP_OPTIONS}{}", square_16(40, BLACK), square_16(76, YELLOW));
let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(read, "{notes:?}");
assert_eq!(kinds(&doc.layers[1]), ["group[1]", "path"]);
replacement_is_cyan(&doc, 1, 0);
let group = &doc.layers[1].children().unwrap()[0];
assert_eq!(group.extras().wrap, Some(vectorcraft_doc::TextWrap { offset: 2.0, invert: false }));
}
#[test]
fn a_differing_group_holding_a_wrap_object_still_falls_back() {
let group = os1_group(&format!("{}\n{WRAP_OPTIONS}", square_16(40, BLACK)));
let front = format!("{group}\n{}", square_16(76, YELLOW));
let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}");
}
#[test]
fn a_page_that_differs_everywhere_still_comes_in_as_its_pdf_part() {
let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, BLACK));
let art = native(&(layer("Back", true, &square_16(4, BLACK)) + &layer("Front", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_CYAN)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(notes.iter().any(|n| n.contains("weren't read") && n.contains("differs from the page's")), "{notes:?}");
assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}");
}
#[test]
fn a_page_object_outside_every_layer_object_still_falls_back() {
let front = format!("{}\n{}", square_16(40, YELLOW), square_16(76, YELLOW));
let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("Front", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_YELLOW), (40.0, 20.0, PAGE_CYAN)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(notes.iter().any(|n| n.contains("differs from the page's")), "{notes:?}");
}
#[test]
fn a_differing_object_with_live_art_inside_falls_back() {
// The group reaches over the square between its own two.
let group = os1_group(&format!("{}\n{}", square_16(4, BLACK), square_16(40, YELLOW)));
let front = format!("{group}\n{}\n{}", square_16(22, YELLOW), square_16(76, YELLOW));
let art = native(&layer("Front", true, &front));
let page = page_16(&[(4.0, 74.0, PAGE_CYAN), (22.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_YELLOW), (76.0, 74.0, PAGE_YELLOW)]);
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(notes.iter().any(|n| n.contains("differs from the page's")), "{notes:?}");
}
#[test]
fn trusted_editing_data_keeps_a_differing_object() {
let (art, page) = middle_differs();
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, true, 1.0);
assert!(read, "{notes:?}");
assert_eq!(names(&doc), ["Back", "Front"]);
assert_eq!(kinds(&doc.layers[1]), ["path", "path"]);
assert!(notes.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{notes:?}");
assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}");
}
#[test]
fn a_differing_object_on_an_unnamed_layer_is_counted_under_its_panel_name() {
let front = format!("{}\n{}", square_16(40, BLACK), square_16(76, YELLOW));
let art = native(&(layer("Back", true, &square_16(4, YELLOW)) + &layer("", true, &front)));
let page = page_16(&[(4.0, 74.0, PAGE_YELLOW), (40.0, 74.0, PAGE_CYAN), (76.0, 74.0, PAGE_YELLOW)]);
let (_, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(read, "{notes:?}");
let note = "objects that drew differently from the file's own page came in as its art: <Layer> (1)";
assert!(notes.iter().any(|n| n == note), "{notes:?}");
}
#[test]
fn at_most_64_differing_objects_come_in_as_the_page_art() {
// 130 squares of 4 points, the first `black` of them black in the layers and cyan on the page.
let grid = |black: usize| {
let at = |i: usize| (4 + 7 * (i % 13) as u32, 4 + 7 * (i / 13) as u32);
let art: String = (0..130)
.map(|i| {
let (x, y) = at(i);
if i < black { format!("{BLACK} k\n{}", square_path(x, y)) } else { square(x, y) }
})
.collect::<Vec<_>>()
.join("\n");
let mut page = Document::new(100.0, 100.0);
let layer_id = page.layers[0].id;
for i in 0..130 {
let (x, y) = at(i);
let (x, y) = (f64::from(x), 96.0 - f64::from(y));
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, y, x + 4.0, y + 4.0));
let color =
if i < black { vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0) } else { vectorcraft_color::Color::cmyk(0.0, 0.0, 1.0, 0.0) };
let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(vectorcraft_color::Paint::solid(color)));
let n = Node::path(page.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() });
page.insert(Some(layer_id), i, n).unwrap();
}
crate::layered_ai(&private(&native(&layer("Grid", true, &art))), page, vec![], false, false, 1.0)
};
let (_, notes, read) = grid(64);
assert!(read, "{notes:?}");
let note = "objects that drew differently from the file's own page came in as its art: Grid (64)";
assert!(notes.iter().any(|n| n == note), "{notes:?}");
let (doc, notes, read) = grid(65);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}");
}
#[test]
fn a_differing_object_under_the_type_of_a_text_object_that_stays_falls_back() {
// A black rectangle under area type whose frame reaches past it; the page draws the rectangle
// cyan, with the type on it.
let big = format!("{BLACK} k\n10 2 m\n60 2 L\n60 40 L\n10 40 L\nf");
let doc = document_of(&[area_frame(34.0, 26.0, 95.0, 38.0)], &[story("Hi\\r", &[0], "/0 0", "/1 8.0")]);
let art = native_with_text(&(layer("Art", true, &format!("{big}\n{}", frame_object(0, 0))) + &layer("Top", true, &square_16(76, YELLOW))), &doc);
let mut page = page_16(&[(76.0, 74.0, PAGE_YELLOW)]);
let layer_id = page.layers[0].id;
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(10.0, 60.0, 60.0, 98.0));
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(1.0, 0.0, 0.0, 0.0));
let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint));
let square = Node::path(page.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() });
page.insert(Some(layer_id), 0, square).unwrap();
let style = vectorcraft_doc::CharStyle { size: 8.0, ..Default::default() };
let text = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(34.0, 69.0), "Hi", style);
let text = Node::new(page.alloc_id(), NodeKind::Text(Box::new(text)));
page.insert(Some(layer_id), 1, text).unwrap();
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
assert!(!read, "{notes:?}");
assert_eq!(names(&doc), ["Layer 1"]);
assert!(!notes.iter().any(|n| n.starts_with("objects that drew")), "{notes:?}");
}
#[test]
fn unread_object_on_second_artboard_takes_second_page_art() {
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
+1
View File
@@ -6026,6 +6026,7 @@ breakText Glyphs Глифи
Gap 5 Разстояние 5
@msg {_1}: they came in as the page's art {_1}: те са внесени като изображението от страницата
@msg objects that drew differently from the file's own page came in as its art: {_1} обектите, изобразени различно от собствената страница на файла, са внесени като изображението от страницата: {_1}
@msg You must choose a side pattern. Трябва да изберете страничен шаблон.
@msg invalid pattern tile choice невалиден избор на плочка за шаблон
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6060,6 +6060,7 @@ shape Open Otvoren
@msg the paths have nothing to merge there putanje tu nemaju šta spojiti
@msg the path is locked or hidden putanja je zaključana ili skrivena
@msg {_1}: they came in as the page's art {_1}: uvezeni su kao ilustracija stranice
@msg objects that drew differently from the file's own page came in as its art: {_1} objekti koji su se iscrtali drugačije od vlastite stranice datoteke uvezeni su kao ilustracija stranice: {_1}
@msg You must choose a side pattern. Morate odabrati bočni uzorak.
@msg invalid pattern tile choice nevažeći izbor pločice uzorka
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6158,6 +6158,7 @@ breakText Glyphs Glifs
Gap 5 Separació 5
@msg {_1}: they came in as the page's art {_1}: han entrat com a art de la pàgina
@msg objects that drew differently from the file's own page came in as its art: {_1} els objectes que es dibuixaven diferent de la pàgina pròpia del fitxer han entrat com a art de la pàgina: {_1}
@msg the JPEG 2000 file can't be read no es pot llegir el fitxer JPEG 2000
@msg the JPEG 2000 file has an unsupported color space or JPX content el fitxer JPEG 2000 té un espai de color o contingut JPX no compatible
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6174,6 +6174,7 @@ breakText Glyphs Glyfy
Gap 5 Mezera 5
@msg {_1}: they came in as the page's art {_1}: byly načteny jako kresba ze stránky
@msg objects that drew differently from the file's own page came in as its art: {_1} objekty, které se vykreslily jinak než vlastní stránka souboru, byly načteny jako kresba ze stránky: {_1}
@msg You must choose a side pattern. Musíte zvolit boční vzorek.
@msg invalid pattern tile choice neplatná volba dlaždice vzorku
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6042,6 +6042,7 @@ breakText Glyphs Glyffer
Gap 5 Mellemrum 5
@msg {_1}: they came in as the page's art {_1}: de blev importeret som sidens grafik
@msg objects that drew differently from the file's own page came in as its art: {_1} objekter, der blev tegnet anderledes end filens egen side, blev importeret som sidens grafik: {_1}
@msg You must choose a side pattern. Du skal vælge et sidemønster.
@msg invalid pattern tile choice ugyldigt valg af mønsterflise
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5760,6 +5760,7 @@ breakText Glyphs Glyphen
Gap 5 Lücke 5
@msg {_1}: they came in as the page's art {_1}: sie wurden als Seitengrafik importiert
@msg objects that drew differently from the file's own page came in as its art: {_1} Objekte, die anders als die eigene Seite der Datei gezeichnet wurden, wurden als Seitengrafik importiert: {_1}
@msg You must choose a side pattern. Sie müssen ein Seitenmuster auswählen.
@msg invalid pattern tile choice ungültige Auswahl einer Musterkachel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6126,6 +6126,7 @@ breakText Glyphs Γλύφοι
Gap 5 Διάκενο 5
@msg {_1}: they came in as the page's art {_1}: εισήχθησαν ως γραφικά της σελίδας
@msg objects that drew differently from the file's own page came in as its art: {_1} τα αντικείμενα που σχεδιάστηκαν διαφορετικά από την ίδια τη σελίδα του αρχείου εισήχθησαν ως γραφικά της σελίδας: {_1}
@msg You must choose a side pattern. Πρέπει να επιλέξετε πλευρικό μοτίβο.
@msg invalid pattern tile choice μη έγκυρη επιλογή πλακιδίου μοτίβου
Can't render this file because it contains an unexpected character in line 163 and column 5.
+1
View File
@@ -6116,6 +6116,7 @@ breakText Glyphs Glifos
Gap 5 Hueco 5
@msg {_1}: they came in as the page's art {_1}: se importaron como el arte de la página
@msg objects that drew differently from the file's own page came in as its art: {_1} los objetos que se dibujaban de forma distinta a la página del propio archivo se importaron como el arte de la página: {_1}
@msg You must choose a side pattern. Debe elegir un motivo lateral.
@msg invalid pattern tile choice selección de mosaico de motivo no válida
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6204,6 +6204,7 @@ breakText Glyphs Merkit
Gap 5 Väli 5
@msg {_1}: they came in as the page's art {_1}: ne tuotiin sivun piirroksena
@msg objects that drew differently from the file's own page came in as its art: {_1} objektit, jotka piirtyivät eri tavoin kuin tiedoston oma sivu, tuotiin sivun piirroksena: {_1}
@msg You must choose a side pattern. Valitse sivukuvio.
@msg invalid pattern tile choice virheellinen kuviolaatan valinta
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6169,6 +6169,7 @@ breakText Glyphs Glyphes
Gap 5 Écart 5
@msg {_1}: they came in as the page's art {_1} : ils ont été importés comme le dessin de la page
@msg objects that drew differently from the file's own page came in as its art: {_1} les objets dessinés différemment de la page du fichier lui-même ont été importés comme le dessin de la page : {_1}
@msg You must choose a side pattern. Vous devez choisir un motif latéral.
@msg invalid pattern tile choice choix de tuile de motif non valide
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6158,6 +6158,7 @@ breakText Glyphs Glifák
Gap 5 Hézag 5
@msg {_1}: they came in as the page's art {_1}: az oldal rajzaként lettek importálva
@msg objects that drew differently from the file's own page came in as its art: {_1} a fájl saját oldalától eltérően rajzolt objektumok az oldal rajzaként lettek importálva: {_1}
@msg You must choose a side pattern. Válasszon oldalsó mintát.
@msg invalid pattern tile choice érvénytelen mintacsempe-választás
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6042,6 +6042,7 @@ breakText Glyphs Glif
Gap 5 Jarak 5
@msg {_1}: they came in as the page's art {_1}: diimpor sebagai gambar halaman
@msg objects that drew differently from the file's own page came in as its art: {_1} objek yang tergambar berbeda dari halaman file itu sendiri diimpor sebagai gambar halaman: {_1}
@msg You must choose a side pattern. Anda harus memilih pola sisi.
@msg invalid pattern tile choice pilihan ubin pola tidak valid
Can't render this file because it contains an unexpected character in line 2214 and column 5.
+1
View File
@@ -5794,6 +5794,7 @@ breakText Glyphs Glifi
Gap 5 Spazio 5
@msg {_1}: they came in as the page's art {_1}: sono stati importati come elementi grafici della pagina
@msg objects that drew differently from the file's own page came in as its art: {_1} gli oggetti disegnati in modo diverso dalla pagina del file stesso sono stati importati come elementi grafici della pagina: {_1}
@msg You must choose a side pattern. Devi scegliere un motivo laterale.
@msg invalid pattern tile choice scelta della tessera motivo non valida
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5859,6 +5859,7 @@ breakText Glyphs 字形
Gap 5 間隔 5
@msg {_1}: they came in as the page's art {_1}: ページのアートとして読み込みました
@msg objects that drew differently from the file's own page came in as its art: {_1} ファイル自身のページと描画が異なるオブジェクトをページのアートとして読み込みました: {_1}
@msg You must choose a side pattern. 側面パターンを選択してください。
@msg invalid pattern tile choice パターンタイルの選択が無効です
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6162,6 +6162,7 @@ breakText Glyphs გლიფები
Gap 5 შუალედი 5
@msg {_1}: they came in as the page's art {_1}: იმპორტირებულია გვერდის გამოსახულების სახით
@msg objects that drew differently from the file's own page came in as its art: {_1} ობიექტები, რომლებიც ფაილის საკუთარი გვერდისგან განსხვავებულად იხატებოდა, იმპორტირებულია გვერდის გამოსახულების სახით: {_1}
@msg You must choose a side pattern. აირჩიეთ გვერდითი ნიმუში.
@msg invalid pattern tile choice ნიმუშის ფილა არასწორად არის არჩეული
Can't render this file because it is too large.
+1
View File
@@ -5755,6 +5755,7 @@ breakText Glyphs 글리프
Gap 5 간격 5
@msg {_1}: they came in as the page's art {_1}: 페이지의 아트로 가져왔습니다
@msg objects that drew differently from the file's own page came in as its art: {_1} 파일 자체 페이지와 다르게 그려진 오브젝트를 페이지의 아트로 가져왔습니다: {_1}
@msg You must choose a side pattern. 측면 패턴을 선택해야 합니다.
@msg invalid pattern tile choice 잘못된 패턴 타일 선택
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6086,6 +6086,7 @@ breakText Glyphs Glyphs
Gap 5 Tussenruimte 5
@msg {_1}: they came in as the page's art {_1}: ze zijn geïmporteerd als pagina-afbeelding
@msg objects that drew differently from the file's own page came in as its art: {_1} objecten die anders werden getekend dan de eigen pagina van het bestand, zijn geïmporteerd als pagina-afbeelding: {_1}
@msg You must choose a side pattern. Kies een zijpatroon.
@msg invalid pattern tile choice ongeldige keuze voor patroontegel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6029,6 +6029,7 @@ breakText Glyphs Glify
Gap 5 Odstęp 5
@msg {_1}: they came in as the page's art {_1}: zaimportowano je jako grafikę strony
@msg objects that drew differently from the file's own page came in as its art: {_1} obiekty rysowane inaczej niż własna strona pliku zaimportowano jako grafikę strony: {_1}
@msg You must choose a side pattern. Musisz wybrać wzór boczny.
@msg invalid pattern tile choice nieprawidłowy wybór kafelka wzoru
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6092,6 +6092,7 @@ breakText Glyphs Glifos
Gap 5 Lacuna 5
@msg {_1}: they came in as the page's art {_1}: foram importados como arte da página
@msg objects that drew differently from the file's own page came in as its art: {_1} os objetos desenhados de forma diferente da própria página do arquivo foram importados como arte da página: {_1}
@msg You must choose a side pattern. Você deve escolher um padrão lateral.
@msg invalid pattern tile choice escolha inválida de ladrilho de padrão
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -6151,6 +6151,7 @@ breakText Glyphs Глифы
Gap 5 Зазор 5
@msg {_1}: they came in as the page's art {_1}: импортированы как графика страницы
@msg objects that drew differently from the file's own page came in as its art: {_1} объекты, которые отрисовывались иначе, чем собственная страница файла, импортированы как графика страницы: {_1}
@msg You must choose a side pattern. Выберите боковой узор.
@msg invalid pattern tile choice недопустимый выбор плитки узора
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6226,6 +6226,7 @@ breakText Glyphs Glyfer
Gap 5 Mellanrum 5
@msg {_1}: they came in as the page's art {_1}: de importerades som sidans bild
@msg objects that drew differently from the file's own page came in as its art: {_1} objekt som ritades annorlunda än filens egen sida importerades som sidans bild: {_1}
@msg You must choose a side pattern. Du måste välja ett sidomönster.
@msg invalid pattern tile choice ogiltigt val av mönsterplatta
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -6131,6 +6131,7 @@ breakText Glyphs Glifler
Gap 5 Boşluk 5
@msg {_1}: they came in as the page's art {_1}: sayfanın çizimi olarak içe aktarıldılar
@msg objects that drew differently from the file's own page came in as its art: {_1} dosyanın kendi sayfasından farklı çizilen nesneler sayfanın çizimi olarak içe aktarıldı: {_1}
@msg You must choose a side pattern. Bir yan desen seçmelisiniz.
@msg invalid pattern tile choice geçersiz desen döşemesi seçimi
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -5774,6 +5774,7 @@ breakText Glyphs Гліфи
Gap 5 Проміжок 5
@msg {_1}: they came in as the page's art {_1}: їх імпортовано як графіку сторінки
@msg objects that drew differently from the file's own page came in as its art: {_1} об’єкти, що малювалися інакше, ніж власна сторінка файлу, імпортовано як графіку сторінки: {_1}
@msg You must choose a side pattern. Виберіть бічний візерунок.
@msg invalid pattern tile choice недійсний вибір плитки візерунка
Can't render this file because it contains an unexpected character in line 177 and column 5.
+1
View File
@@ -5775,6 +5775,7 @@ breakText Glyphs Glyph
Gap 5 Khoảng hở 5
@msg {_1}: they came in as the page's art {_1}: đã được nhập dưới dạng hình vẽ của trang
@msg objects that drew differently from the file's own page came in as its art: {_1} các đối tượng được vẽ khác với trang riêng của tệp đã được nhập dưới dạng hình vẽ của trang: {_1}
@msg You must choose a side pattern. Bạn phải chọn mẫu bên.
@msg invalid pattern tile choice lựa chọn ô mẫu không hợp lệ
Can't render this file because it contains an unexpected character in line 199 and column 5.
+1
View File
@@ -6015,6 +6015,7 @@ breakText Glyphs 字形
Gap 5 间隔 5
@msg {_1}: they came in as the page's art {_1}:已作为页面图稿导入
@msg objects that drew differently from the file's own page came in as its art: {_1} 与文件自身页面绘制不同的对象已作为页面图稿导入:{_1}
@msg You must choose a side pattern. 必须选择侧边图案。
@msg invalid pattern tile choice 图案拼块选项无效
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5812,6 +5812,7 @@ breakText Glyphs 字符
Gap 5 間隙 5
@msg {_1}: they came in as the page's art {_1}:已作為頁面圖稿匯入
@msg objects that drew differently from the file's own page came in as its art: {_1} 與檔案本身頁面繪製不同的物件已作為頁面圖稿匯入:{_1}
@msg You must choose a side pattern. 必須選擇側邊圖樣。
@msg invalid pattern tile choice 圖樣拼塊選項無效
Can't render this file because it contains an unexpected character in line 6 and column 47.
File diff suppressed because one or more lines are too long