mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 10:02:38 +00:00
Merge pull request #5533 from dotCooCoo/fix/ai-layers-checked-on-every-page
.ai import: the layers are compared with every page of a multi-artboard file
This commit is contained in:
@@ -91,7 +91,7 @@ mod tests {
|
||||
ring(&mut layers, Some(MADE), red, 6.0, square);
|
||||
ring(&mut layers, Some(MADE), red, 2.0, Rect::new(40.0, 40.0, 60.0, 60.0));
|
||||
let rect = page.artboards[0].rect;
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let mut view = PageView::new(&frame);
|
||||
assert_eq!(prune_unseen(&frame, &mut view, &mut layers, false), 1, "only the ring the page doesn't draw");
|
||||
let widths: Vec<f64> = layers.layers[0].children().unwrap().iter().filter_map(|n| n.appearance.stroke().map(|s| s.width)).collect();
|
||||
@@ -193,15 +193,15 @@ mod tests {
|
||||
let a = comparison_render(&mut renderer, &page, rect, 0.4, 1);
|
||||
let b = comparison_render(&mut vectorcraft_render::Renderer::new(), &vector, rect, 0.4, 1);
|
||||
assert!(compared(&a, &b, &[], &[]).solid > MAX_SOLID_DIFFERENCE, "the old point sampling falsely refused this page");
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let mut view = PageView::new(&frame);
|
||||
assert!(compare(&frame, &mut view, &vector, false).is_ok());
|
||||
assert!(compare(&frame, &mut view, &Document::new(1000.0, 500.0), false).is_err(), "missing artwork remains an error");
|
||||
let shifted = Rect::new(0.0, 0.0, 500.0, 500.0);
|
||||
assert!(view.difference(&vector, shifted, false).any > MAX_DIFFERENCE, "wrong framing remains an error");
|
||||
assert!(view.difference_on_every_page(&vector, shifted, false).any > MAX_DIFFERENCE, "wrong framing remains an error");
|
||||
}
|
||||
|
||||
/// The boxes `solid_regions` gives for a page of images are in the page's points: the
|
||||
/// The boxes `solid_regions_on_every_page` gives for a page of images are in the page's points: the
|
||||
/// comparison is drawn at a multiple of the scale and resized, and the two sizes round
|
||||
/// differently on the short side, so a box at the far edge does not drift out of the object
|
||||
/// that makes it.
|
||||
@@ -218,7 +218,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The boxes `solid_regions` gives for a `width` × `height` page that is one image of a black
|
||||
/// The boxes `solid_regions_on_every_page` gives for a `width` × `height` page that is one image of a black
|
||||
/// `object`, compared with layers that hold nothing.
|
||||
fn raster_page_regions(object: Rect, width: f64, height: f64) -> Vec<Rect> {
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
@@ -256,9 +256,9 @@ mod tests {
|
||||
);
|
||||
page.insert(Some(page.layers[0].id), usize::MAX, image).unwrap();
|
||||
assert_eq!(comparison_samples(&page), 4);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let mut view = PageView::new(&frame);
|
||||
let (_, regions) = view.solid_regions(&Document::new(width, height), rect, false).unwrap();
|
||||
let (_, regions) = view.solid_regions_on_every_page(&Document::new(width, height), false).unwrap();
|
||||
assert!(!regions.is_empty());
|
||||
regions
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, compare, difference};
|
||||
use super::compare::{
|
||||
MAX_DIFFERENCE, MAX_PAGES_COMPARED, MAX_PIXELS_COMPARED, MAX_SOLID_DIFFERENCE, PageView, compare, comparison_samples, difference, pixels_compared,
|
||||
};
|
||||
use super::plan::{made_only, make_slots, plan, readable_all};
|
||||
use super::slots::{
|
||||
ART_LEFT_OUT, Content, DREW_DIFFERENTLY, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, SlotInfo, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots,
|
||||
@@ -32,6 +34,10 @@ pub(super) struct Frame<'a> {
|
||||
pub(super) whole: bool,
|
||||
/// The PDF page areas and their moves into the editing document.
|
||||
pub(super) pages: Vec<(Rect, Affine)>,
|
||||
/// The pages after the first that show artboards of the layers' document, each with that
|
||||
/// artboard (as many as [`MAX_PIXELS_COMPARED`] allows): the layers are compared with them as
|
||||
/// with the first.
|
||||
pub(super) more: Vec<(Rect, Rect)>,
|
||||
}
|
||||
|
||||
/// The document of the editing copy `data`, with the text (and strokes round it) of `visible`, the
|
||||
@@ -211,6 +217,9 @@ pub(super) fn build_from(
|
||||
Arc::make_mut(n).transform(shift, PAGE_MOVE);
|
||||
}
|
||||
}
|
||||
// Pages cropped to the art are copies of the first, and a PDF saved with some of the
|
||||
// artboards can hold any of them.
|
||||
let more = more_pages(boards.filter(|b| !cropped && b.len() == doc.artboards.len()), &pages, comparison_samples(visible));
|
||||
// An EPS saved as its artboard has the art outside it left out of its page.
|
||||
let artboard_page =
|
||||
artboard.is_some_and(|a| [a.x0 - art_box.x0, a.y0 - art_box.y0, a.x1 - art_box.x1, a.y1 - art_box.y1].iter().all(|d| d.abs() <= 1.0));
|
||||
@@ -224,7 +233,7 @@ pub(super) fn build_from(
|
||||
}
|
||||
drop_guides_off_boards(&mut doc);
|
||||
}
|
||||
frame = Some(Frame { page: visible, page_rect, rect, whole, pages: page_moves });
|
||||
frame = Some(Frame { page: visible, page_rect, rect, whole, pages: page_moves, more });
|
||||
}
|
||||
let mut infos = vec![];
|
||||
collect_slots(&doc.layers, true, &mut infos);
|
||||
@@ -326,7 +335,8 @@ pub(super) fn build_from(
|
||||
Ok(note) => {
|
||||
let closer = other.and_then(|mut other| {
|
||||
let unseen = prune_unseen(&frame, &mut view, &mut other, reflow);
|
||||
let closer = view.difference(&other, frame.rect, reflow).any < view.difference(&doc, frame.rect, reflow).any;
|
||||
let closer = view.difference_on_every_page(&other, frame.rect, reflow).any
|
||||
< view.difference_on_every_page(&doc, frame.rect, reflow).any;
|
||||
closer.then(|| compare(&frame, &mut view, &other, reflow).ok()).flatten().map(|note| (other, unseen, note))
|
||||
});
|
||||
match closer {
|
||||
@@ -801,7 +811,11 @@ fn closer_first(
|
||||
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)
|
||||
if view.difference_on_every_page(&other.0, frame.rect, reflow).any < view.difference_on_every_page(&doc.0, frame.rect, reflow).any =>
|
||||
{
|
||||
vec![other, doc]
|
||||
}
|
||||
Some(other) => vec![doc, other],
|
||||
None => vec![doc],
|
||||
}
|
||||
@@ -847,13 +861,14 @@ fn keep_differing(
|
||||
|
||||
// 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)?;
|
||||
let (diff, regions) = view.solid_regions_on_every_page(doc, 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());
|
||||
let pages = view.regions(frame.rect);
|
||||
objects.retain(|(_, _, b)| pages.iter().any(|page| !b.intersect(*page).is_zero_area()));
|
||||
if regions.len().saturating_mul(objects.len()) > MAX_PATCH_PAIRS {
|
||||
return None;
|
||||
}
|
||||
@@ -1200,6 +1215,32 @@ fn shown_artboard(doc: &Document, visible: &Document, page: Page, page_rect: Rec
|
||||
best.map_or(0, |(i, _)| i)
|
||||
}
|
||||
|
||||
/// The pages after the first, each with the artboard it shows ([`page_boards`]), as [`Frame::more`]
|
||||
/// holds them: none when the pages don't show the artboards. They stop before the page that would
|
||||
/// take the pixels the comparisons draw (`samples`, [`comparison_samples`]), the first page's
|
||||
/// included, past [`MAX_PIXELS_COMPARED`], and after [`MAX_PAGES_COMPARED`] pages.
|
||||
fn more_pages(boards: Option<&[vectorcraft_doc::Artboard]>, pages: &[Rect], samples: u32) -> Vec<(Rect, Rect)> {
|
||||
let Some(boards) = boards else { return vec![] };
|
||||
let mut left = MAX_PIXELS_COMPARED;
|
||||
pages
|
||||
.iter()
|
||||
.zip(boards)
|
||||
.map_while(|(page, board)| {
|
||||
left = left.checked_sub(pixels_compared(*page, samples))?;
|
||||
Some((*page, board.rect))
|
||||
})
|
||||
.take(MAX_PAGES_COMPARED)
|
||||
.skip(1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The artboards (or other areas) of `pages` that `bounds` lies on, with `margin` (points) round it:
|
||||
/// the pages whose drawings taking it out can change.
|
||||
fn pages_touched(bounds: Rect, pages: &[Rect], margin: f64) -> Vec<usize> {
|
||||
let b = bounds.inflate(margin, margin);
|
||||
pages.iter().enumerate().filter(|(_, p)| !b.intersect(**p).is_zero_area()).map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
/// The artboards of the layers' document (`boards`) that the pages show, one a page from the
|
||||
/// first page's (`first`), when they are the pages' sizes: every artboard, or those of a PDF saved
|
||||
/// with some of them (`first` after the first artboard).
|
||||
@@ -1310,35 +1351,51 @@ fn take_out(nodes: &mut Vec<Arc<Node>>, id: NodeId, hide: bool) -> bool {
|
||||
nodes.iter_mut().any(|n| !matches!(n.kind, NodeKind::Compound { .. }) && Arc::make_mut(n).children_mut().is_some_and(|c| take_out(c, id, hide)))
|
||||
}
|
||||
|
||||
/// The text objects made from the file's text document that show and print on the page's area,
|
||||
/// where the page has no type to match them, and that the layers look no worse without, are not
|
||||
/// drawn by the app that wrote the file (which paints every shown text object; it keeps the type
|
||||
/// of what was turned to outlines): they are taken out. How many. `reflow`: the type is all laid
|
||||
/// out again from the text document (see [`compare`]).
|
||||
/// The text objects made from the file's text document that show and print on a page's area (the
|
||||
/// frame's first page or another one, [`Frame::more`]), where the page has no type to match them, and
|
||||
/// that the layers look no worse without on every page they lie on, are not drawn by the app that
|
||||
/// wrote the file (which paints every shown text object; it keeps the type of what was turned to
|
||||
/// outlines): they are taken out. How many. `reflow`: the type is all laid out again from the text
|
||||
/// document (see [`compare`]).
|
||||
pub(super) fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document, reflow: bool) -> usize {
|
||||
let mut found = vec![];
|
||||
unmark(&mut doc.layers, true, true, &mut found);
|
||||
let rect = frame.rect;
|
||||
if found.is_empty() || found.len() > MAX_WEIGHED {
|
||||
return 0;
|
||||
}
|
||||
let with = view.difference(doc, rect, reflow).weighed(reflow);
|
||||
// Taking a text object out changes only what is drawn in its box. When the art would still
|
||||
// differ from the page by more than `compare` allows with every candidate gone, the layers
|
||||
// aren't used whatever is pruned: don't draw them again for each one (#758).
|
||||
let pages = view.regions(frame.rect);
|
||||
// Taking a text object out changes only what is drawn in its box. When the art on a page would
|
||||
// still differ from it by more than `compare` allows with every candidate gone, pruning doesn't
|
||||
// make that page pass: the objects on it aren't drawn again one by one (#758).
|
||||
let margin = OUTLINE_REACH + 1.0;
|
||||
let boxes: f64 = found.iter().filter_map(|(_, b)| *b).map(|b| b.inflate(margin, margin).intersect(rect).area().max(0.0)).sum();
|
||||
if with - boxes / rect.area().max(1e-9) > MAX_DIFFERENCE {
|
||||
return 0;
|
||||
}
|
||||
let hopeless: Vec<bool> = view
|
||||
.differences(doc, frame.rect, reflow)
|
||||
.iter()
|
||||
.zip(&pages)
|
||||
.map(|(with, rect)| {
|
||||
let boxes: f64 = found.iter().filter_map(|(_, b)| *b).map(|b| b.inflate(margin, margin).intersect(*rect).area().max(0.0)).sum();
|
||||
with.weighed(reflow) - boxes / rect.area().max(1e-9) > MAX_DIFFERENCE
|
||||
})
|
||||
.collect();
|
||||
// Weighed by colour, not only lightness: a text object's red stroke over a dark red one is no
|
||||
// less there for being about as light (#1102).
|
||||
let mut with = view.colour_difference(doc, rect);
|
||||
// less there for being about as light (#1102). The view keeps each page's drawing and share: a
|
||||
// removal has only the pages in `on` drawn again.
|
||||
let mut removed = 0;
|
||||
for (id, bounds) in found {
|
||||
// A removal before it can have let its area type flow differently.
|
||||
let bounds = if removed > 0 { doc.node(id).and_then(|n| n.visual_bounds()) } else { bounds };
|
||||
if bounds.is_none_or(|b| b.intersect(rect).is_zero_area()) {
|
||||
let Some(b) = bounds else { continue };
|
||||
let mut lies = pages_touched(b, &pages, 0.0);
|
||||
if lies.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// The pages its strokes and effects can reach past its box are drawn again too.
|
||||
let mut on = pages_touched(b, &pages, margin);
|
||||
// Area type that flows around it can reach other pages: they are all weighed.
|
||||
if has_wraps(doc) {
|
||||
(lies, on) = ((0..pages.len()).collect(), (0..pages.len()).collect());
|
||||
}
|
||||
if lies.iter().any(|i| hopeless.get(*i).copied().unwrap_or(true)) {
|
||||
continue;
|
||||
}
|
||||
let mut without = doc.clone();
|
||||
@@ -1347,11 +1404,12 @@ pub(super) fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Doc
|
||||
}
|
||||
// Area type that wrapped around the text object flows without it.
|
||||
relayout_wrapped(&mut without);
|
||||
let d = view.colour_difference(&without, rect);
|
||||
if d - with <= UNSEEN_MARGIN {
|
||||
let shares = view.weigh_without(doc, &without, &on);
|
||||
if shares.len() == on.len() && shares.iter().all(|(_, before, after)| after - before <= UNSEEN_MARGIN) {
|
||||
take_out(&mut doc.layers, id, false);
|
||||
relayout_wrapped(doc);
|
||||
with = d;
|
||||
// Only the pages in `on` can draw differently (every page when type wraps).
|
||||
view.adopt(doc);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
@@ -1364,6 +1422,42 @@ mod candidate_tests {
|
||||
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject};
|
||||
|
||||
/// The pages compared stop where their drawings would take more than the pixels allowed.
|
||||
#[test]
|
||||
fn the_pages_compared_stop_at_the_pixel_bound() {
|
||||
let pages: Vec<Rect> = (0..1000).map(|i| Rect::new(f64::from(i) * 700.0, 0.0, f64::from(i) * 700.0 + 612.0, 792.0)).collect();
|
||||
let doc = Document::new(612.0, 792.0);
|
||||
let boards: Vec<vectorcraft_doc::Artboard> = pages
|
||||
.iter()
|
||||
.map(|page| {
|
||||
let mut board = doc.artboards[0].clone();
|
||||
board.rect = *page;
|
||||
board
|
||||
})
|
||||
.collect();
|
||||
let more = more_pages(Some(&boards), &pages, 1);
|
||||
let each = pixels_compared(pages[0], 1);
|
||||
assert_eq!(more.len() + 1, (MAX_PIXELS_COMPARED / each) as usize);
|
||||
assert!(more.len() >= 64, "{}", more.len());
|
||||
assert_eq!(more_pages(Some(&boards[..3]), &pages[..3], 1).len(), 2);
|
||||
// Pages drawn with 4 × 4 samples a pixel take 16 times the pixels.
|
||||
assert_eq!(more_pages(Some(&boards), &pages, 4).len() + 1, (MAX_PIXELS_COMPARED / (each * 16)) as usize);
|
||||
// Tiny pages stop at the page count.
|
||||
let tiny: Vec<Rect> = (0..1000).map(|i| Rect::new(f64::from(i) * 2.0, 0.0, f64::from(i) * 2.0 + 1.0, 1.0)).collect();
|
||||
let tiny_boards: Vec<vectorcraft_doc::Artboard> =
|
||||
boards.iter().zip(&tiny).map(|(b, r)| vectorcraft_doc::Artboard { rect: *r, ..b.clone() }).collect();
|
||||
assert_eq!(more_pages(Some(&tiny_boards), &tiny, 1).len() + 1, MAX_PAGES_COMPARED);
|
||||
}
|
||||
|
||||
/// The pages a box touches with a margin round it include those it only comes near.
|
||||
#[test]
|
||||
fn the_pages_within_reach_of_a_box_count_as_touched() {
|
||||
let pages = [Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(110.0, 0.0, 210.0, 100.0)];
|
||||
let near_the_second = Rect::new(60.0, 40.0, 106.0, 60.0);
|
||||
assert_eq!(pages_touched(near_the_second, &pages, 0.0), vec![0]);
|
||||
assert_eq!(pages_touched(near_the_second, &pages, OUTLINE_REACH + 1.0), vec![0, 1]);
|
||||
}
|
||||
|
||||
/// Layers read in the units of a page scaled up to points take their wrap offsets with their art,
|
||||
/// within ±1000 points.
|
||||
#[test]
|
||||
@@ -1504,7 +1598,7 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), 0, image).unwrap();
|
||||
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let kinds = replace_unread(
|
||||
&mut doc,
|
||||
&frame,
|
||||
@@ -1546,8 +1640,14 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), page.layers[0].children().unwrap().len(), art).unwrap();
|
||||
}
|
||||
let (first, second) = (Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(100.0, 0.0, 200.0, 100.0));
|
||||
let frame =
|
||||
Frame { page: &page, page_rect: first, rect: first, whole: false, pages: vec![(first, Affine::IDENTITY), (second, Affine::IDENTITY)] };
|
||||
let frame = Frame {
|
||||
page: &page,
|
||||
page_rect: first,
|
||||
rect: first,
|
||||
whole: false,
|
||||
pages: vec![(first, Affine::IDENTITY), (second, Affine::IDENTITY)],
|
||||
more: vec![],
|
||||
};
|
||||
let result = replace_unread(&mut doc, &frame, &[(id, vec!["placed files".into()])], &[], &BTreeSet::new(), &mut BTreeSet::new());
|
||||
assert_eq!(result.unwrap_err(), "an unread object lies on more than one artboard");
|
||||
}
|
||||
@@ -1572,7 +1672,7 @@ mod candidate_tests {
|
||||
);
|
||||
page.insert(Some(page.layers[0].id), 0, art).unwrap();
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let result = replace_unread(&mut doc, &frame, &[(id, vec!["placed files".into()])], &[], &BTreeSet::new(), &mut BTreeSet::new());
|
||||
assert_eq!(result.unwrap_err(), "an unread object lies on an artboard that has no page");
|
||||
|
||||
@@ -1603,7 +1703,7 @@ mod candidate_tests {
|
||||
);
|
||||
page.insert(Some(page.layers[0].id), 0, art).unwrap();
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let result = replace_unread(
|
||||
&mut doc,
|
||||
&frame,
|
||||
@@ -1641,7 +1741,7 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), page.layers[0].children().unwrap().len(), art).unwrap();
|
||||
}
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let kinds = replace_unread(
|
||||
&mut doc,
|
||||
&frame,
|
||||
@@ -1682,7 +1782,7 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), page.layers[0].children().unwrap().len(), art).unwrap();
|
||||
}
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let result = replace_unread(&mut doc, &frame, &[(id, vec!["placed files".into()])], &[], &BTreeSet::new(), &mut BTreeSet::new());
|
||||
assert_eq!(result.unwrap_err(), "an unread object's appearance is not identifiable in the page art");
|
||||
}
|
||||
@@ -1712,7 +1812,7 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), page.layers[0].children().unwrap().len(), art).unwrap();
|
||||
}
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let unread = [(first, vec!["placed files".into()]), (second, vec!["placed files".into()])];
|
||||
let mut one_box_doc = doc.clone();
|
||||
let kinds = replace_unread(&mut doc, &frame, &unread, &[(second, Rect::new(50.0, 50.0, 60.0, 60.0))], &BTreeSet::new(), &mut BTreeSet::new())
|
||||
@@ -1756,7 +1856,7 @@ mod candidate_tests {
|
||||
page.insert(Some(page.layers[0].id), page.layers[0].children().unwrap().len(), art).unwrap();
|
||||
}
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)] };
|
||||
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
|
||||
let unread = [(id, vec!["placed files".into()])];
|
||||
let mut shown: Vec<Vec<Arc<Node>>> = nodes.iter().map(|n| vec![Arc::clone(n)]).collect();
|
||||
let taken = take_unread_type(&doc, Some(&frame), &unread, &[(id, Rect::new(50.0, 50.0, 60.0, 60.0))], &mut shown);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1580,6 +1580,197 @@ fn a_pdf_of_its_second_artboard_is_compared_on_that_artboard() {
|
||||
assert!((e - 190.0).abs() < 1e-6 && (f - 20.0).abs() < 1e-6, "on the second artboard: {:?}", texts[0].xf);
|
||||
}
|
||||
|
||||
/// Two 100-point artboards, the second 50 below the first (art space, y up), as the editing data
|
||||
/// lists them.
|
||||
const TWO_BOARDS: &str = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
|
||||
/// The PDF part of a file with [`TWO_BOARDS`]: a page for each artboard in a row, the first with a
|
||||
/// square at (10, 86), and the second with one at (`x`, `y`) of its own when given.
|
||||
fn two_pages(second: Option<(f64, f64)>) -> Document {
|
||||
let mut page = pdf_part(10.0, 86.0);
|
||||
let mut board = page.artboards[0].clone();
|
||||
(board.id, board.rect) = (2, vectorcraft_geom::Rect::new(120.0, 0.0, 220.0, 100.0));
|
||||
page.artboards.push(board);
|
||||
if let Some((x, y)) = second {
|
||||
let other = pdf_part(120.0 + x, y);
|
||||
let square = page.reid(&other.layers[0].children().unwrap()[0]);
|
||||
let layer = page.layers[0].id;
|
||||
page.insert(Some(layer), 1, square).unwrap();
|
||||
}
|
||||
page
|
||||
}
|
||||
|
||||
/// The layers are compared with every page: art that they draw on the second artboard and its page
|
||||
/// doesn't sends the file to its PDF part, as it would on the first.
|
||||
#[test]
|
||||
fn art_only_the_layers_draw_on_the_second_artboard_makes_the_file_come_in_as_its_pdf_part() {
|
||||
// A 60-point square on the second artboard, 20 from its left and 20 down.
|
||||
let big = "0 0 1 0 k\n20 -130 m\n80 -130 L\n80 -70 L\n20 -70 L\nf";
|
||||
let art = native(&(TWO_BOARDS.to_string() + &layer("Live", true, &square(10, 10)) + &layer("Second", true, big)));
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), two_pages(None), vec![], false, false, 1.0);
|
||||
assert!(!read, "{w:?}");
|
||||
assert_ne!(names(&d), ["Live", "Second"], "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("differs from the page's")), "{w:?}");
|
||||
// The same square on the first artboard, which its page doesn't draw either.
|
||||
let big = "0 0 1 0 k\n20 20 m\n80 20 L\n80 80 L\n20 80 L\nf";
|
||||
let art = native(&(TWO_BOARDS.to_string() + &layer("Live", true, &square(10, 10)) + &layer("Second", true, big)));
|
||||
let (_, w, read) = crate::layered_ai(&private(&art), two_pages(None), vec![], false, false, 1.0);
|
||||
assert!(!read && w.iter().any(|w| w.contains("differs from the page's")), "{w:?}");
|
||||
}
|
||||
|
||||
/// Art that the second page draws and the layers don't sends the file to its PDF part too.
|
||||
#[test]
|
||||
fn art_only_the_second_page_draws_makes_the_file_come_in_as_its_pdf_part() {
|
||||
let art = native(&(TWO_BOARDS.to_string() + &layer("Live", true, &square(10, 10))));
|
||||
// A 60-point square on the second page, 20 from its left and 20 down.
|
||||
let mut page = two_pages(None);
|
||||
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(140.0, 20.0, 200.0, 80.0));
|
||||
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(0.0, 0.0, 0.0, 1.0));
|
||||
let appearance = vectorcraft_doc::Appearance {
|
||||
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint))],
|
||||
..Default::default()
|
||||
};
|
||||
let node = Node::path(page.alloc_id(), path, appearance);
|
||||
let layer_id = page.layers[0].id;
|
||||
page.insert(Some(layer_id), 1, node).unwrap();
|
||||
let (_, w, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
|
||||
assert!(!read, "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("differs from the page's")), "{w:?}");
|
||||
}
|
||||
|
||||
/// A PDF saved with some of the artboards, not starting with the first, can hold any of the
|
||||
/// others after it: its later pages aren't compared with the artboards that follow the first.
|
||||
#[test]
|
||||
fn the_later_pages_of_a_pdf_of_some_artboards_are_not_compared() {
|
||||
// Four 100-point artboards side by side, 50 apart (art space, y up), a 4-point square on the
|
||||
// first, second and fourth, and a 60-point square on the third.
|
||||
let board = |x0: i32| {
|
||||
format!(
|
||||
"%_/Dictionary :\n%_{x0} 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_{} 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n",
|
||||
x0 + 100
|
||||
)
|
||||
};
|
||||
let boards = format!(
|
||||
"%_/Document :\n%_/Dictionary :\n%_/Array :\n{}{}{}{}%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n",
|
||||
board(0),
|
||||
board(150),
|
||||
board(300),
|
||||
board(450)
|
||||
);
|
||||
let big = "0 0 1 0 k\n320 20 m\n380 20 L\n380 80 L\n320 80 L\nf";
|
||||
let art = native(&(boards + &layer("Art", true, &(square(10, 10) + "\n" + &square(180, 50) + "\n" + big + "\n" + &square(460, 30)))));
|
||||
// The PDF part: a page for the second artboard, then one for the fourth.
|
||||
let mut pdf = pdf_part(30.0, 46.0);
|
||||
let mut second = pdf.artboards[0].clone();
|
||||
(second.id, second.rect) = (2, vectorcraft_geom::Rect::new(120.0, 0.0, 220.0, 100.0));
|
||||
pdf.artboards.push(second);
|
||||
let other = pdf_part(130.0, 66.0);
|
||||
let fourth = pdf.reid(&other.layers[0].children().unwrap()[0]);
|
||||
let layer_id = pdf.layers[0].id;
|
||||
pdf.insert(Some(layer_id), 1, fourth).unwrap();
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), pdf, vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert_eq!(d.artboards.len(), 4, "{w:?}");
|
||||
}
|
||||
|
||||
/// Layers that draw each page's art on its artboard come in.
|
||||
#[test]
|
||||
fn layers_that_match_every_page_come_in() {
|
||||
// A square on the second artboard, 50 from its left and 46 down, which its page draws too.
|
||||
let second = "0 0 1 0 k\n50 -100 m\n54 -100 L\n54 -96 L\n50 -96 L\nf";
|
||||
let art = native(&(TWO_BOARDS.to_string() + &layer("Live", true, &square(10, 10)) + &layer("Second", true, second)));
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), two_pages(Some((50.0, 46.0))), vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert_eq!(names(&d), ["Live", "Second"], "{w:?}");
|
||||
assert!(w.is_empty(), "{w:?}");
|
||||
}
|
||||
|
||||
/// Layers that draw each page's art come in from [`TWO_BOARDS`] changed to `boards`.
|
||||
fn two_boards_come_in(boards: String) {
|
||||
let second = "0 0 1 0 k\n50 -100 m\n54 -100 L\n54 -96 L\n50 -96 L\nf";
|
||||
let art = native(&(boards + &layer("Live", true, &square(10, 10)) + &layer("Second", true, second)));
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), two_pages(Some((50.0, 46.0))), vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert_eq!(names(&d), ["Live", "Second"], "{w:?}");
|
||||
}
|
||||
|
||||
/// A first artboard up to half a point larger than its page is compared on an area of the page's
|
||||
/// size.
|
||||
#[test]
|
||||
fn a_first_artboard_a_little_wider_than_its_page_is_compared_on_the_pages_size() {
|
||||
two_boards_come_in(TWO_BOARDS.replace("%_100 0 /", "%_100.3 0 /"));
|
||||
}
|
||||
|
||||
/// A later artboard up to half a point smaller than its page is compared on an area of the page's
|
||||
/// size.
|
||||
#[test]
|
||||
fn a_second_artboard_a_little_shorter_than_its_page_is_compared_on_the_pages_size() {
|
||||
two_boards_come_in(TWO_BOARDS.replace("%_100 -150", "%_100 -149.7"));
|
||||
}
|
||||
|
||||
/// A text object the second page doesn't draw is left out even when the first page differs by more
|
||||
/// than the comparison allows.
|
||||
#[test]
|
||||
fn an_unseen_text_object_on_the_second_page_is_weighed_while_the_first_page_differs() {
|
||||
let doc = text_document(&[("Ghost\r", 0, (8200.0, 8330.0), (0.0, 0.0), 0.0)]);
|
||||
let big = "0 0 1 0 k\n20 20 m\n80 20 L\n80 80 L\n20 80 L\nf";
|
||||
let art = native_with_text(&(TWO_BOARDS.to_string() + &layer("Art", true, &(square(10, 10) + "\n" + big + "\n" + &text_object(0)))), &doc);
|
||||
// The first page draws the 60-point square in black.
|
||||
let mut page = two_pages(None);
|
||||
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(20.0, 20.0, 80.0, 80.0));
|
||||
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::cmyk(0.0, 0.0, 0.0, 1.0));
|
||||
let appearance = vectorcraft_doc::Appearance {
|
||||
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint))],
|
||||
..Default::default()
|
||||
};
|
||||
let node = Node::path(page.alloc_id(), path, appearance);
|
||||
let layer_id = page.layers[0].id;
|
||||
page.insert(Some(layer_id), 1, node).unwrap();
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
let mut texts = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
texts.push(t.plain_text());
|
||||
}
|
||||
});
|
||||
assert!(texts.is_empty(), "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("doesn't draw")), "{w:?}");
|
||||
}
|
||||
|
||||
/// A text object on the second artboard that its page doesn't draw is weighed as one on the first
|
||||
/// is: it is taken out, unless its layer doesn't print.
|
||||
#[test]
|
||||
fn a_text_object_the_second_page_doesnt_draw_is_left_out_unless_its_layer_doesnt_print() {
|
||||
// "Ghost" 38.5 down the second artboard (150 below the first's anchor in the canvas).
|
||||
let doc = text_document(&[("Ghost\r", 0, (8200.0, 8330.0), (0.0, 0.0), 0.0)]);
|
||||
let all_text = |d: &Document| {
|
||||
let mut all = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
all.push(t.plain_text());
|
||||
}
|
||||
});
|
||||
all
|
||||
};
|
||||
let art = native_with_text(&(TWO_BOARDS.to_string() + &layer("Art", true, &(square(10, 10) + "\n" + &text_object(0)))), &doc);
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), two_pages(None), vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert!(all_text(&d).is_empty(), "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("doesn't draw")), "{w:?}");
|
||||
// On a layer that shows and doesn't print, it stays.
|
||||
let tips = layer("Tips", true, &text_object(0)).replace("1 1 1 1 0 0 1 0 79", "1 1 1 0 0 0 1 0 79");
|
||||
let art = native_with_text(&(TWO_BOARDS.to_string() + &layer("Art", true, &square(10, 10)) + &tips), &doc);
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), two_pages(None), vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert_eq!(names(&d), ["Art", "Tips"], "{w:?}");
|
||||
assert_eq!(all_text(&d), ["Ghost"], "{w:?}");
|
||||
assert!(!w.iter().any(|w| w.contains("doesn't draw")), "{w:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_file_that_cant_be_read_through_its_editing_data_keeps_its_pdf_part() {
|
||||
let pdf = pdf_part(10.0, 86.0);
|
||||
|
||||
+8
-3
@@ -772,9 +772,14 @@ layer, even where the page doesn't draw it. A file whose editing data can't be d
|
||||
pattern fills, placed files or anything else the reader doesn't read on a layer that shows, or whose layers look
|
||||
different from its page, opens as before: an EPS as its printed page, a `.ai` file from its PDF content (where plain
|
||||
groups open ungrouped), with a warning saying why. Layers look different when they draw an object the page doesn't (or
|
||||
miss one it draws), and, for an EPS whose page is the box of its art, when they print art outside it. A file whose
|
||||
editing data is for a page of another size opens the same way, and its warning gives both sizes in points, with the
|
||||
scale of the editing data's page to the file's page (1:10 for a tenth of its size) when their sides are in proportion.
|
||||
miss one it draws), and, for an EPS whose page is the box of its art, when they print art outside it. When the PDF
|
||||
content of a `.ai` has a page for each artboard, the layers are compared with each page on its artboard, until the
|
||||
pages compared take 8,388,608 pixels at the comparison's scale (about 67 letter-size pages, or 4 when the PDF content
|
||||
is only images) or number 128; when it has pages for some of the artboards, or its pages are cropped to the art, they
|
||||
are compared with the first page only. An artboard, or the page of an EPS's editing data, whose sides are within half
|
||||
a point of its page's is compared on an area of the page's size. A file whose editing data is for a page of another
|
||||
size opens the same way, and its warning gives both sizes in points, with the scale of the editing data's page to the
|
||||
file's page (1:10 for a tenth of its size) when their sides are in proportion.
|
||||
With `textAs: "outlines"` a `.ai` file whose type shows opens from its PDF content, which has the type's outlines. A PDF
|
||||
saved with Illustrator's editing data holds it in `AIPDFPrivateData` streams and opens from it as a `.ai` file does.
|
||||
|
||||
|
||||
@@ -1148,6 +1148,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A `.ai` opened from its editing data opens its Objects on Path groups live, with their path, their objects in order along it, Pivot and Rotate, and their art where the file draws it; a group whose spacing has been dragged, or with options an Objects on Path group doesn't hold, opens as a plain group (`crates/eps/src/import/ai/tests.rs`).
|
||||
In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482).
|
||||
When the PDF content of a `.ai` has a page for each artboard, the layers are compared with every page on its artboard (until the pages compared take 8,388,608 pixels at the comparison's scale, about 67 letter-size pages, or number 128): art that only the layers or only a later page draws is handled as art that differs on the first page is, and a text object that a later page doesn't draw is left out unless its layer doesn't print (`crates/eps/src/import/tests_native.rs`).
|
||||
When the editing data of a `.ai` or EPS is for a page of another size than the file's, the warning that its layers weren't read gives both sizes in points, and the scale of the editing data's page to the file's page (1:10 for a tenth of its size) when their sides are in proportion (#2034; `crates/eps/src/import/tests_native.rs`).
|
||||
A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards or lies on an artboard that has no page, when no page art lies inside its box, when that page art is inside a page group with a clip, mask, transparency or appearance, when the object holds a frame of threaded type, when the page art uses a pattern, image or swatch that the page lacks or that differs from the editing data's, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
|
||||
Reference in New Issue
Block a user