vectorcraft/crates/eps/src/import/native.rs

1062 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The layers of an Illustrator EPS or `.ai`.
//!
//! Besides the page it prints, such a file carries the app's own copy of the art: an EPS after its
//! `%%EOF`, a `.ai` in its `AIPrivateData` streams (see `ai` for the containers). It has what the
//! page doesn't: the layers with their names and options, the groups, compound paths and clipping
//! groups, the objects' names, the hidden objects and layers, the art outside the artboards and
//! every artboard. `ai` reads that structure into a document, with the colours, gradients,
//! transparency and images of the art.
//!
//! Text objects are slots in that document: their characters, fonts and places are in the file's
//! text document (see `ate`). Type that shows comes from the page, which draws it, into the slot
//! whose text is where the page has it. Type that doesn't show (a hidden layer or object, or off
//! the page) is made from the text document; where that can't be read (area type, type on a path)
//! it's left out, with a warning.
//!
//! What a layer that shows has that `ai` doesn't read makes the file come in as its page, as it did
//! before; on a layer that doesn't show it is left out of that layer, with a warning. The page is
//! also the safety net: when the layers' art looks different from it (one draws an object the
//! other doesn't, or much of it differs; an EPS whose page is its art's box has nothing printed
//! outside it), the page is used.
//!
//! Where a file doesn't match what this module expects, it is left alone and its page is imported.
use std::collections::BTreeSet;
use std::sync::Arc;
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
use super::Imported;
use super::ai::{self, Structure, slot_of};
use super::ate::{Story, Texts};
/// The notes that say the import left out something the file has (see [`is_loss`]).
const TEXT_LEFT_OUT: &str = "text objects on hidden layers or hidden objects couldn't be read, so they are left out";
/// The same for a file without a page to take shown type from.
const TYPE_LEFT_OUT: &str = "text objects of kinds VectorCraft doesn't read from the file's text document yet are left out";
const ART_LEFT_OUT: &str = "hidden layers have art this can't read";
const LAYERS_UNREAD: &str = "the file's layers weren't read";
/// Does this import note say that the file has something the document doesn't (hidden text, art or
/// layers that could not be read)? Writing the document over the file would lose it for good.
pub fn is_loss(note: &str) -> bool {
note.ends_with(TEXT_LEFT_OUT)
|| note.ends_with(TYPE_LEFT_OUT)
|| [ART_LEFT_OUT, LAYERS_UNREAD, super::graphics::TOO_MUCH, super::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
/// to each other (more than this and the layers aren't used).
const MAX_SLOTS: usize = 5000;
const MAX_PIECES: usize = 2000;
/// Most bytes of text in the text objects of the layers (a story named twice counts twice).
const MAX_TEXT: usize = 16 << 20;
/// Most pairs of a text object and a piece of the page's type compared.
const MAX_PAIRS: usize = 4_000_000;
/// `names` for a note: the first few, and how many more.
fn few(names: BTreeSet<String>) -> String {
const SHOWN: usize = 5;
let more = names.len().saturating_sub(SHOWN);
let mut list: Vec<String> = names.into_iter().take(SHOWN).collect();
if more > 0 {
list.push(format!("{more} more"));
}
list.join(", ")
}
/// A text slot of the layers.
struct SlotInfo {
story: Option<u32>,
/// Which of the story's frames it is.
frame: usize,
/// It, its layer and the objects round it show.
shown: bool,
}
/// The text slots of the layers and objects, in order.
fn collect_slots(nodes: &[Arc<Node>], shown: bool, out: &mut Vec<SlotInfo>) {
for n in nodes {
let shown = shown && n.visible;
if let Some(story) = slot_of(n.name.as_deref()) {
out.push(SlotInfo { story, frame: ai::slot_frame(n.name.as_deref()), shown });
} else if let Some(c) = n.children().filter(|_| !matches!(n.kind, NodeKind::Compound { .. })) {
collect_slots(c, shown, out);
}
}
}
/// How far from a text object's own extent the strokes drawn around it reach (points).
const OUTLINE_REACH: f64 = 6.0;
/// Does `n` look like part of a text object's outline: a path with a stroke, inside `around`?
fn outlines(n: &Node, around: Rect) -> bool {
matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })
&& n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if s.visible && s.width > 0.0))
&& n.visual_bounds().is_some_and(|b| {
let a = around.inflate(OUTLINE_REACH, OUTLINE_REACH);
b.x0 >= a.x0 && b.y0 >= a.y0 && b.x1 <= a.x1 && b.y1 <= a.y1
})
}
/// How far apart (points) the baselines of two text nodes may be for them to be on one line.
const SAME_LINE: f64 = 0.5;
/// Where `n`'s baseline lies across the direction it runs in (points), if it is text.
fn baseline(n: &Node) -> Option<f64> {
let NodeKind::Text(t) = &n.kind else { return None };
let [a, b, _, _, e, f] = t.xf.as_coeffs();
let len = a.hypot(b);
(len > 1e-9).then(|| (a * f - b * e) / len)
}
/// The text objects the page paints, each as the run of nodes that make it up (its text, once for
/// each fill and stroke it has, with the paths of its strokes), in painting order: those that
/// show in `shown`, those of hidden layers and objects (a PDF has them) in `hidden`.
fn text_objects(nodes: &[Arc<Node>], on: bool, shown: &mut Vec<Vec<Arc<Node>>>, hidden: &mut Vec<Vec<Arc<Node>>>) {
let is_text = |n: &Node| matches!(n.kind, NodeKind::Text(_));
let (mut i, mut free) = (0, 0);
while let Some(n) = nodes.get(i) {
if !is_text(n) {
if let Some(c) = n.children().filter(|_| !matches!(n.kind, NodeKind::Compound { .. })) {
text_objects(c, on && n.visible, shown, hidden);
free = i + 1;
}
i += 1;
continue;
}
let mut bounds = n.visual_bounds().unwrap_or_default();
let (mut start, mut end) = (i, i);
while start > free
&& let Some(p) = nodes.get(start - 1)
&& outlines(p, bounds)
{
start -= 1;
bounds = bounds.union(p.visual_bounds().unwrap_or_default());
}
while let Some(next) = nodes.get(end + 1)
&& let Some(b) = next.visual_bounds()
&& (is_text(next)
&& !b.intersect(bounds.inflate(OUTLINE_REACH, OUTLINE_REACH)).is_zero_area()
&& baseline(next)
.zip(nodes.get(start..=end).unwrap_or_default().iter().find_map(|m| baseline(m)))
.is_some_and(|(x, y)| (x - y).abs() <= SAME_LINE)
|| outlines(next, bounds))
{
end += 1;
bounds = bounds.union(b);
}
let object = nodes.get(start..=end).unwrap_or_default().to_vec();
let into = if on && n.visible { &mut *shown } else { &mut *hidden };
into.push(object);
i = end + 1;
free = i;
}
}
/// `n` and what it holds with ids of `doc`.
fn reid(doc: &mut Document, n: &Arc<Node>) -> Arc<Node> {
let mut n = n.as_ref().clone();
n.id = doc.alloc_id();
if let Some(children) = n.children_mut() {
for c in children.iter_mut() {
*c = reid(doc, c);
}
}
Arc::new(n)
}
/// The page's text objects given to each of `slots` text slots: in painting order, and when there
/// are more of one than of the other (a label may be a few text objects, or one with several
/// strokes), each goes where the share of the order it is at falls.
fn assign(objects: &[Vec<Arc<Node>>], slots: usize) -> Vec<Vec<&[Arc<Node>]>> {
let mut given: Vec<Vec<&[Arc<Node>]>> = vec![vec![]; slots];
for (j, o) in objects.iter().enumerate() {
if let Some(at) = given.get_mut(((2 * j + 1) * slots / (2 * objects.len())).min(slots.saturating_sub(1))) {
at.push(o);
}
}
given
}
/// What goes into a text slot.
enum Content {
/// Text objects (with the strokes round them) of the page.
Page(Vec<Arc<Node>>),
/// A text object made from the file's text document, where it lies on the document, whether
/// the page has type where it is, and the page's type of no text object that is nearest it
/// (kept after it).
Made(Box<Node>, Option<Rect>, bool, Vec<Arc<Node>>),
/// Nothing the file lets this read.
Empty,
/// Its type is on the page with another text object's; there is nothing to put here.
Merged,
}
/// Replace the text slots in `nodes` with what `content` has for each, in order (a slot given none is
/// left out): the page's type takes its place as it is, made type keeps the slot's name. `empty`
/// counts those left out.
fn fill_slots(
doc: &mut Document,
nodes: &mut Vec<Arc<Node>>,
content: &mut std::vec::IntoIter<Content>,
names: &std::collections::HashMap<NodeId, String>,
empty: &mut usize,
made: &mut Vec<(u32, usize, NodeId)>,
) {
let mut i = 0;
while i < nodes.len() {
let Some(n) = nodes.get(i) else { break };
if let Some(story) = slot_of(n.name.as_deref()) {
let visible = n.visible;
let name = names.get(&n.id);
let frame = ai::slot_frame(n.name.as_deref());
let replacement = match content.next() {
Some(Content::Page(parts)) if !parts.is_empty() => {
// The page's type (and the strokes round it) take the slot's place as they are.
let parts: Vec<Arc<Node>> = parts
.iter()
.map(|p| {
let mut p = reid(doc, p);
if !visible {
Arc::make_mut(&mut p).visible = false;
}
if let Some(name) = name.filter(|_| matches!(p.kind, NodeKind::Text(_))) {
Arc::make_mut(&mut p).name = Some(name.clone());
}
p
})
.collect();
let n = parts.len();
nodes.splice(i..=i, parts);
i += n;
continue;
}
Some(Content::Made(mut text, _, seen, extra)) => {
text.id = doc.alloc_id();
if let Some(story) = story {
made.push((story, frame, text.id));
}
text.visible = visible;
// Type the page shows is kept; other made type is weighed against the page.
text.name = if seen { name.cloned() } else { Some(format!("{MADE}{}", name.map_or("", String::as_str))) };
let mut parts = vec![Arc::new(*text)];
parts.extend(extra.iter().map(|p| reid(doc, p)));
let n = parts.len();
nodes.splice(i..=i, parts);
i += n;
continue;
}
Some(Content::Merged) => {
nodes.remove(i);
continue;
}
_ => None,
};
match replacement {
Some(node) => {
if let Some(slot) = nodes.get_mut(i) {
*slot = Arc::new(node);
}
i += 1;
}
None => {
*empty += 1;
nodes.remove(i);
}
}
} else {
if n.children().is_some()
&& !matches!(n.kind, NodeKind::Compound { .. })
&& let Some(c) = nodes.get_mut(i).map(Arc::make_mut).and_then(Node::children_mut)
{
fill_slots(doc, c, content, names, empty, made);
}
i += 1;
}
}
}
/// How far, in points, the page's type may be from where the file's text document puts it for the
/// two to be the same type.
const SAME_PLACE: f64 = 0.75;
/// Its letters and digits, which is what is compared of type (the page may spell a mark another way).
fn letters(s: &str) -> String {
s.chars().filter(char::is_ascii_alphanumeric).collect()
}
/// Does `n` hold type that is where `story` lays one of its lines out: the line, or a piece of it
/// (the page splits a line where its kerning or its styles change), on that line's baseline and
/// after its start? How far from where the line starts it is, if so.
fn same_type(n: &Node, lines: &[(String, Point)]) -> Option<f64> {
let NodeKind::Text(t) = &n.kind else { return None };
let have = letters(&t.plain_text());
let [a, b, _, _, e, f] = t.xf.as_coeffs();
let len = a.hypot(b);
if have.is_empty() || len < 1e-9 {
return None;
}
let (ux, uy) = (a / len, b / len);
// About how far a character of it advances, to say how far along a line a piece may be.
let advance = n.visual_bounds().map_or(0.0, |r| r.width().max(r.height())) / have.len() as f64;
lines
.iter()
.filter_map(|(text, at)| {
let line = letters(text);
let skipped = line.find(&have)?;
if skipped == 0 {
return Some(((at.x - e).powi(2) + (at.y - f).powi(2)).sqrt()).filter(|d| *d <= SAME_PLACE);
}
let (dx, dy) = (e - at.x, f - at.y);
let (along, across) = (dx * ux + dy * uy, (dx * uy - dy * ux).abs());
((have.len() > 1 || skipped + 1 == line.len())
&& across <= SAME_PLACE
&& along >= -SAME_PLACE
&& along <= line.len() as f64 * advance * 2.0 + 4.0)
.then_some(across + 0.001 * along)
})
.min_by(f64::total_cmp)
}
/// The page draws a line in pieces where its kerning or styles change ("swe", "ep"), and the file
/// has it as one line of one text object: the pieces that on one baseline add up to a line of
/// `lines` become one text (each keeping its own style, the line's spaces between them).
fn join_pieces(parts: &mut Vec<Arc<Node>>, lines: &[(String, Point)]) {
let text_of = |n: &Arc<Node>| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None };
let texts: Vec<usize> = parts.iter().enumerate().filter(|(_, n)| matches!(n.kind, NodeKind::Text(_))).map(|(i, _)| i).collect();
// A page with this many pieces of type in one place isn't one of lines of a story.
if texts.len() > MAX_PIECES {
return;
}
let mut taken = vec![false; parts.len()];
// The first piece of each line to join, the others, and the spaces to put after each piece.
let mut joins: Vec<(usize, Vec<usize>, Vec<String>)> = vec![];
for &i in &texts {
if taken.get(i).copied().unwrap_or(true) {
continue;
}
let Some(line) = parts.get(i).and_then(|n| baseline(n)) else { continue };
let group: Vec<usize> = texts
.iter()
.copied()
.filter(|j| {
!taken.get(*j).copied().unwrap_or(true) && parts.get(*j).and_then(|n| baseline(n)).is_some_and(|b| (b - line).abs() <= SAME_LINE)
})
.collect();
for j in &group {
if let Some(t) = taken.get_mut(*j) {
*t = true;
}
}
let pieces: Vec<String> = group.iter().filter_map(|j| parts.get(*j).and_then(text_of)).collect();
let places: Vec<(f64, f64)> = group
.iter()
.filter_map(|j| parts.get(*j))
.filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some((t.xf.as_coeffs()[4], t.xf.as_coeffs()[5])) } else { None })
.collect();
let distinct = places.iter().enumerate().all(|(a, p)| places.iter().skip(a + 1).all(|q| (p.0 - q.0).hypot(p.1 - q.1) > 0.01));
let joined: String = pieces.iter().map(|p| letters(p)).collect();
if group.len() < 2 || !distinct || joined.is_empty() {
continue;
}
let Some((whole, _)) = lines.iter().find(|(text, _)| letters(text) == joined) else { continue };
// Where the line has spaces after a piece's last character that the page doesn't draw.
let line_chars: Vec<char> = whole.chars().collect();
let mut at = 0;
let mut gaps = vec![];
for (k, piece) in pieces.iter().enumerate() {
for _ in piece.chars().filter(|c| !c.is_whitespace()) {
while line_chars.get(at).is_some_and(|c| c.is_whitespace()) {
at += 1;
}
at += 1;
}
let space: String = line_chars.iter().skip(at).take_while(|c| c.is_whitespace()).collect();
let next = pieces.get(k + 1);
let drawn = piece.ends_with(char::is_whitespace) || next.is_none_or(|n| n.starts_with(char::is_whitespace));
gaps.push(if drawn { String::new() } else { space });
}
if at <= line_chars.len() {
joins.push((group[0], group[1..].to_vec(), gaps));
}
}
let mut gone: BTreeSet<usize> = BTreeSet::new();
for (first, rest, gaps) in &joins {
let mut runs: Vec<Vec<vectorcraft_doc::TextRun>> = [first]
.into_iter()
.chain(rest)
.filter_map(|j| parts.get(*j))
.filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.runs.clone()) } else { None })
.collect();
for (r, gap) in runs.iter_mut().zip(gaps) {
if let Some(last) = r.last_mut() {
last.text.push_str(gap);
}
}
if let Some(node) = parts.get_mut(*first).map(Arc::make_mut)
&& let NodeKind::Text(t) = &mut node.kind
{
t.runs = runs.into_iter().flatten().collect();
// The layout cached for the first piece is not this text's.
t.cached_bounds = None;
t.cached_baselines.clear();
}
gone.extend(rest);
}
// The pieces that went into a text, removed once the joins are done: removing as each is made
// would move the places of the pieces that the others still name.
let mut keep = 0;
parts.retain(|_| {
keep += 1;
!gone.contains(&(keep - 1))
});
}
/// Do the regions of made type cover most of `object`?
fn covered(object: &[Arc<Node>], content: &[Content]) -> bool {
let Some(r) = extent(object) else { return false };
let inside: f64 =
content.iter().filter_map(|c| if let Content::Made(_, Some(region), ..) = c { Some(region.intersect(r).area()) } else { None }).sum();
inside >= 0.5 * r.area().max(1e-9)
}
/// The box round `parts`.
fn extent(parts: &[Arc<Node>]) -> Option<Rect> {
parts.iter().filter_map(|n| n.visual_bounds()).reduce(|a, b| a.union(b))
}
/// What goes into each of `infos`' slots: the page's type that the file's text document puts where the
/// page has it, else type made from the text document, else (for a file whose text document can't say)
/// the page's type in painting order.
fn plan(
infos: &[SlotInfo],
stories: &[Option<Arc<Story>>],
template: Option<(f64, f64)>,
to_doc: Affine,
pages: [&[Vec<Arc<Node>>]; 2],
warn: &mut Vec<String>,
) -> Vec<Content> {
let [shown, hidden] = pages;
let lines: Vec<Option<Vec<(String, Point)>>> = stories
.iter()
.map(|s| {
let (s, t) = (s.as_ref()?, template?);
Some(s.line_starts(t).into_iter().map(|(text, (x, y))| (text, to_doc * Point::new(x, y))).collect())
})
.collect();
// The page's objects that are where a shown slot's lines are, nearest first.
let mut pairs: Vec<(f64, usize, usize)> = vec![];
for (k, info) in infos.iter().enumerate() {
let Some(l) = lines.get(k).and_then(Option::as_ref).filter(|_| info.shown) else { continue };
for (j, object) in shown.iter().enumerate() {
if let Some(d) = object.iter().filter_map(|n| same_type(n, l)).min_by(f64::total_cmp) {
pairs.push((d, k, j));
}
}
}
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut owner: Vec<Option<usize>> = vec![None; shown.len()];
for (_, k, j) in pairs {
if let Some(o) = owner.get_mut(j).filter(|o| o.is_none()) {
*o = Some(k);
}
}
// Type the text document makes, shown or not: the page's pieces of it aren't needed.
let mut content: Vec<Content> = infos.iter().map(|_| Content::Empty).collect();
if let Some(t) = template {
for (k, info) in infos.iter().enumerate() {
let Some(Some(story)) = stories.get(k) else { continue };
let Some(node) = story.node(NodeId(0), info.frame, t, to_doc) else { continue };
let region = story.region(t, to_doc);
if let Some(c) = content.get_mut(k) {
*c = Content::Made(Box::new(node), region.filter(|_| info.shown), false, vec![]);
}
}
}
// The file's text document is trusted where it agrees with the page about something (its lines
// start where the page's type does, or the page has type where it lays type out), or where the
// page has no type to disagree with.
let in_region = |object: &[Arc<Node>]| covered(object, &content);
let trusted = owner.iter().any(Option::is_some) || shown.is_empty() || shown.iter().any(|o| in_region(o));
if !trusted {
content.iter_mut().for_each(|c| *c = Content::Empty);
}
let mut spare: Vec<Vec<Arc<Node>>> = vec![];
for (j, object) in shown.iter().enumerate() {
match owner.get(j).copied().flatten().filter(|_| trusted) {
Some(k) => match content.get_mut(k) {
Some(Content::Made(_, _, seen, _)) => *seen = true,
Some(Content::Page(p)) => p.extend(object.iter().cloned()),
Some(c) => *c = Content::Page(object.clone()),
None => {}
},
// What lies where made type is, is that type as the page draws it.
None if covered(object, &content) => {
let r = extent(object).unwrap_or(Rect::ZERO);
for c in &mut content {
if let Content::Made(_, Some(region), seen, _) = c
&& !region.intersect(r).is_zero_area()
{
*seen = true;
}
}
}
None => spare.push(object.clone()),
}
}
for (k, c) in content.iter_mut().enumerate() {
if let (Content::Page(parts), Some(Some(l))) = (c, lines.get(k)) {
join_pieces(parts, l);
}
}
// What is left of the page goes by painting order into the shown slots the file can't say.
let loose: Vec<usize> =
infos.iter().enumerate().filter(|(k, i)| i.shown && matches!(content.get(*k), Some(Content::Empty))).map(|(k, _)| k).collect();
if loose.is_empty() {
// Type of the page that no text object says it owns goes with the nearest text object that has
// type of the page; with none, it is left out.
let mut left_out = false;
for object in &spare {
let (centre, near) = (extent(object).map(|r| r.center()), |c: &Content| match c {
Content::Page(parts) => extent(parts).map(|r| r.center()),
Content::Made(_, region, ..) => region.map(|r| r.center()),
_ => None,
});
let nearest = content
.iter_mut()
.filter_map(|c| near(c).zip(centre).map(|(p, q)| ((p.x - q.x).hypot(p.y - q.y), c)))
.min_by(|a, b| a.0.total_cmp(&b.0));
match nearest {
Some((_, Content::Page(parts) | Content::Made(_, _, _, parts))) => parts.extend(object.iter().cloned()),
_ => left_out = true,
}
}
if !spare.is_empty() {
warn.push(
if left_out {
"some of the page's type has no text object in the layers, so it is left out"
} else {
"some of the page's type has no text object of its own in the layers, so it is with another's"
}
.into(),
);
}
} else {
for (slot, parts) in loose.iter().zip(assign(&spare, loose.len())) {
if let Some(c) = content.get_mut(*slot) {
// A text object the page shows as part of another's has nothing to hold of its own.
*c = if parts.is_empty() && !spare.is_empty() {
Content::Merged
} else {
Content::Page(parts.into_iter().flat_map(|p| p.iter().cloned()).collect())
};
}
}
if !spare.is_empty() && spare.len() != loose.len() {
warn.push("the text is on the layers in the order the page paints it, so a text object may be in another group than it was".into());
}
}
// The same for the slots that don't show.
let unread: Vec<usize> =
infos.iter().enumerate().filter(|(k, i)| !i.shown && matches!(content.get(*k), Some(Content::Empty))).map(|(k, _)| k).collect();
for (slot, parts) in unread.iter().zip(assign(hidden, unread.len())) {
if let Some(c) = content.get_mut(*slot) {
*c = Content::Page(parts.into_iter().flat_map(|p| p.iter().cloned()).collect());
}
}
content
}
/// `doc` with its layers that don't print hidden, when it has any.
fn as_printed(doc: &Document) -> Option<Document> {
fn hide(nodes: &mut [Arc<Node>]) -> bool {
let mut any = false;
for n in nodes {
if matches!(n.kind, NodeKind::Layer { printable: false, .. }) && n.visible {
Arc::make_mut(n).visible = false;
any = true;
} else if matches!(n.kind, NodeKind::Layer { .. })
&& let Some(c) = Arc::make_mut(n).children_mut()
{
any |= hide(c);
}
}
any
}
let mut d = doc.clone();
hide(&mut d.layers).then_some(d)
}
/// A premultiplied pixel seen on white.
fn on_white(p: &[u8; 4]) -> [u32; 3] {
let paper = 255 - u32::from(p[3]).min(255);
[0, 1, 2].map(|i| (u32::from(p[i]) + paper).min(255))
}
/// The lightness (0 to 255, Rec. 601 weights) of a pixel seen on white.
fn lightness([r, g, b]: [u32; 3]) -> u32 {
(r * 299 + g * 587 + b * 114) / 1000
}
/// How far (0 to 255) a pixel seen on white is from white: how much ink it has.
fn ink([r, g, b]: [u32; 3]) -> u32 {
255 - r.min(g).min(b)
}
/// How far apart two pixels' lightness, or their ink where one is white, may be for them to look
/// alike.
const SAME_PIXEL: u32 = 48;
/// The most ink a pixel seen as white has.
const WHITE: u32 = 8;
/// Do two pixels look different: in lightness, or one inked (as a light yellow is) where the other
/// is white?
fn differ(p: &[u8; 4], q: &[u8; 4]) -> bool {
let (p, q) = (on_white(p), on_white(q));
let (a, b) = (ink(p), ink(q));
lightness(p).abs_diff(lightness(q)) > SAME_PIXEL || (a.min(b) <= WHITE && a.abs_diff(b) > SAME_PIXEL)
}
/// How much two documents draw differently: the share (0 to 1) of the pixels that differ, and of
/// those whose neighbours all differ too, away from type (where one draws an object that the other
/// doesn't, not the edges of one drawn a little differently, or type laid out a little
/// differently: a line the page sets in pieces is laid out afresh as one).
#[derive(Clone, Copy)]
struct Difference {
any: f64,
solid: f64,
}
/// The scale the comparisons draw a region at: about 400 pixels along its longer side.
fn compare_scale(r: Rect) -> f64 {
(400.0 / r.width().max(r.height()).max(1.0)).min(4.0)
}
/// The page drawn once, to weigh the layers' art against it: [`Self::difference`] draws the layers
/// with a renderer of their own, which keeps their decoded images between draws (the page's images
/// can share their keys and not their pixels, so the two don't share one).
struct PageView {
page: vectorcraft_render::Rendered,
/// Where the page's own type is, in its pixels.
page_type: Vec<Rect>,
layers: vectorcraft_render::Renderer,
scale: f64,
}
impl PageView {
fn new(frame: &Frame<'_>) -> Self {
let ra = frame.page_rect;
let scale = compare_scale(ra);
// Drawn on nothing and then seen on white: blending modes have no page to blend with, as in
// the app that wrote the file.
let page = vectorcraft_render::Renderer::new().render_region(frame.page, ra, scale, false);
let mut page_type = vec![];
type_areas(&frame.page.layers, ra, scale, &mut page_type);
Self { page, page_type, layers: vectorcraft_render::Renderer::new(), scale }
}
/// How `b` on `rb` (a rectangle the page's size) draws differently from the page ([`compared`]).
fn difference(&mut self, b: &Document, rb: Rect) -> Difference {
// The page doesn't print what isn't printed.
let printed = as_printed(b);
let b = printed.as_ref().unwrap_or(b);
let y = self.layers.render_region(b, rb, self.scale, false);
let mut areas = self.page_type.clone();
type_areas(&b.layers, rb, self.scale, &mut areas);
compared(&self.page, &y, &areas)
}
}
/// How `a` on `ra` and `b` on `rb` (rectangles of the same size) draw differently ([`compared`]),
/// for a region other than the page's ([`PageView`] draws that one once).
fn difference(a: &Document, ra: Rect, b: &Document, rb: Rect) -> Difference {
let printed = as_printed(b);
let b = printed.as_ref().unwrap_or(b);
let scale = compare_scale(ra);
// A renderer each: the two documents' images can share their keys and not their pixels.
let x = vectorcraft_render::Renderer::new().render_region(a, ra, scale, false);
let y = vectorcraft_render::Renderer::new().render_region(b, rb, scale, false);
let mut areas = vec![];
type_areas(&a.layers, ra, scale, &mut areas);
type_areas(&b.layers, rb, scale, &mut areas);
compared(&x, &y, &areas)
}
/// How two drawings of the same size differ, each drawn on nothing and seen on white, away from
/// the type in `areas` (pixels). Pixels are compared by lightness, and by whether they are inked
/// 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.
fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect]) -> Difference {
if x.width != y.width || x.height != y.height {
return Difference { any: 1.0, solid: 1.0 };
}
let differs: Vec<bool> = x.pixels.as_chunks::<4>().0.iter().zip(y.pixels.as_chunks::<4>().0).map(|(p, q)| differ(p, q)).collect();
let (w, h) = (x.width as usize, x.height as usize);
let at = |i: usize, j: usize| j.checked_mul(w).and_then(|row| row.checked_add(i)).and_then(|k| differs.get(k)).copied().unwrap_or(false);
let typed = in_areas(areas, w, h);
// 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 area = (w as f64 * h as f64).max(1.0);
Difference { any: differs.iter().filter(|d| **d).count() as f64 / area, solid: solid as f64 / area }
}
/// Where the type that shows in `nodes` is, in pixels of `region` drawn at `scale` (with a margin
/// for the strokes round it).
fn type_areas(nodes: &[Arc<Node>], region: Rect, scale: f64, out: &mut Vec<Rect>) {
for n in nodes.iter().filter(|n| n.visible) {
if let NodeKind::Text(_) = n.kind
&& let Some(b) = n.visual_bounds()
{
let px =
|r: Rect| Rect::new((r.x0 - region.x0) * scale, (r.y0 - region.y0) * scale, (r.x1 - region.x0) * scale, (r.y1 - region.y0) * scale);
out.push(px(b.inflate(OUTLINE_REACH, OUTLINE_REACH)));
} else if let Some(c) = n.children() {
type_areas(c, region, scale, out);
}
}
}
/// Which of `w` × `h` pixels (row by row) lie in one of `areas`.
fn in_areas(areas: &[Rect], w: usize, h: usize) -> Vec<bool> {
// Each area adds one inside it: corners marked, then summed along the rows and down the columns.
let stride = w + 1;
let mut count = vec![0i64; stride * (h + 1)];
let clamp = |v: f64, max: usize| if v.is_finite() { v.clamp(0.0, max as f64) as usize } else { 0 };
for r in areas {
let (x0, y0, x1, y1) = (clamp(r.x0.floor(), w), clamp(r.y0.floor(), h), clamp(r.x1.ceil(), w), clamp(r.y1.ceil(), h));
for (x, y, d) in [(x0, y0, 1), (x1, y0, -1), (x0, y1, -1), (x1, y1, 1)] {
if let Some(c) = count.get_mut(y * stride + x) {
*c += d;
}
}
}
for row in count.chunks_mut(stride) {
let mut run = 0;
for c in row {
run += *c;
*c = run;
}
}
let mut column = vec![0i64; stride];
for row in count.chunks_mut(stride) {
for (c, sum) in row.iter_mut().zip(column.iter_mut()) {
*sum += *c;
*c = *sum;
}
}
(0..h).flat_map(|j| (0..w).map(move |i| (i, j))).map(|(i, j)| count.get(j * stride + i).is_some_and(|c| *c > 0)).collect()
}
/// More of the page than this differing between the layers' art and the page's own, and the
/// layers aren't used.
const MAX_DIFFERENCE: f64 = 0.05;
/// More than this and the import says so.
const NOTABLE_DIFFERENCE: f64 = 0.002;
/// More of the page than this where one draws an object that the other doesn't (see
/// [`Difference`]), and the layers aren't used: about a centimetre square on a letter page.
const MAX_SOLID_DIFFERENCE: f64 = 0.001;
/// Which box of the editing copy is the page.
#[derive(Clone, Copy)]
enum Page {
/// An EPS's page is its art's bounding box.
Art,
/// A `.ai` is read on its (first and only) artboard.
Artboard,
}
/// The page (`visible`, the file read as its page) and the place of its area in the layers'
/// document.
struct Frame<'a> {
page: &'a Document,
/// The page's area in `page`.
page_rect: Rect,
/// The same area in the layers' document.
rect: Rect,
/// The page holds all the art that prints (an EPS's page is its art's box, unless it was
/// saved as its artboard): the layers print nothing outside it.
whole: bool,
}
/// The document of the editing copy `data`, with the text (and strokes round it) of `visible`, the
/// file read as its page, put in its text objects → the document and its notes. Without a page
/// (a `.ai` saved without its PDF part), its type is made from the file's text document alone and
/// nothing is compared. `outlined`: the page's type was read as outlines, so it has none to put in
/// the text objects.
fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool) -> Result<(Document, Vec<String>), String> {
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, warnings } = ai::read(data)?;
let (mut shown, mut hidden) = (vec![], vec![]);
let mut frame = None;
if let Some(visible) = visible {
let art_box = match page {
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
};
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
if (art_box.width() - page_rect.width()).abs() > 0.5 || (art_box.height() - page_rect.height()).abs() > 0.5 {
return Err("its editing data is for a different page".into());
}
// The page's area in the layers' document, and the page's type moved there.
let rect = to_doc.transform_rect_bbox(art_box);
let shift = Affine::translate(Vec2::new(rect.x0 - page_rect.x0, rect.y0 - page_rect.y0));
for l in &visible.layers {
if let Some(c) = l.children() {
text_objects(c, l.visible, &mut shown, &mut hidden);
}
}
for object in shown.iter_mut().chain(hidden.iter_mut()) {
for n in object.iter_mut() {
Arc::make_mut(n).transform(shift, false);
}
}
// 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));
let whole = matches!(page, Page::Art) && !artboard_page;
frame = Some(Frame { page: visible, page_rect, rect, whole });
}
let mut infos = vec![];
collect_slots(&doc.layers, true, &mut infos);
// Type asked for as outlines is the page's: the text objects that show can't be filled.
if outlined && infos.iter().any(|i| i.shown) {
return Err("its type is to come in as outlines, which only its page has".into());
}
if infos.iter().all(|i| !i.shown) && !shown.is_empty() {
return Err("its page has text, and its layers have no text objects to put it in".into());
}
// The page's type is compared with every text object of the layers: that has its limits.
if infos.len() > MAX_SLOTS || infos.len().saturating_mul(shown.len()) > MAX_PAIRS {
return Err("it has too many text objects".into());
}
let texts = Texts::read(data);
// Each story is read once, however many text objects name it.
let mut read: std::collections::BTreeMap<u32, Option<Arc<Story>>> = Default::default();
let stories: Vec<Option<Arc<Story>>> = infos
.iter()
.map(|i| {
let n = i.story?;
read.entry(n).or_insert_with(|| texts.as_ref().and_then(|t| t.story(n as usize)).map(Arc::new)).clone()
})
.collect();
if stories.iter().flatten().map(|s| s.len()).sum::<usize>() > MAX_TEXT {
return Err("it has too much text".into());
}
let template = template.map(|t| ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0));
let mut notes = warnings;
let content = plan(&infos, &stories, template, to_doc, [&shown, &hidden], &mut notes);
let mut empty = 0;
let mut layers = std::mem::take(&mut doc.layers);
let mut made = vec![];
fill_slots(&mut doc, &mut layers, &mut content.into_iter(), &slot_names, &mut empty, &mut made);
doc.layers = layers;
// A story in several frames is threaded type: its frames in order (the first holds the story).
made.sort();
for thread in made.chunk_by(|a, b| a.0 == b.0) {
if thread.len() > 1 {
doc.text_threads.push(thread.iter().map(|(_, _, id)| *id).collect());
}
}
if empty > 0 {
notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT }));
}
if !hidden_unread.is_empty() {
notes.push(format!("{ART_LEFT_OUT} ({}), left out of them", few(hidden_unread)));
}
match frame {
Some(frame) => {
let mut view = PageView::new(&frame);
let unseen = prune_unseen(&frame, &mut view, &mut doc);
if unseen > 0 {
notes.push(format!("{unseen} {UNSEEN_TEXT}"));
}
notes.extend(compare(&frame, &mut view, &doc)?);
}
None => unmark(&mut doc.layers, true, &mut vec![]),
}
Ok((doc, notes))
}
/// Marks (in a node's name, until [`prune_unseen`] has looked) the text objects made from the file's
/// text document.
const MADE: &str = "\u{1}made";
/// How many such objects are weighed against the page (each takes a render of the page's size).
const MAX_WEIGHED: usize = 12;
/// The text objects the file keeps that the page doesn't draw.
const UNSEEN_TEXT: &str = "text objects that the file keeps but its page doesn't draw (what is left of type turned to outlines) are left out";
/// How much more of the page (0 to 1) the layers may differ without a text object and still look no worse.
const UNSEEN_MARGIN: f64 = 5e-5;
/// Clear the marks in `nodes` and say which marked objects show, by id.
fn unmark(nodes: &mut [Arc<Node>], shown: bool, found: &mut Vec<(NodeId, Option<Rect>)>) {
for n in nodes {
let shown = shown && n.visible;
if let Some(name) = n.name.as_deref().and_then(|m| m.strip_prefix(MADE)) {
if shown {
found.push((n.id, n.visual_bounds()));
}
let name = (!name.is_empty()).then(|| name.to_string());
Arc::make_mut(n).name = name;
} else if !matches!(n.kind, NodeKind::Compound { .. })
&& let Some(c) = Arc::make_mut(n).children_mut()
{
unmark(c, shown, found);
}
}
}
/// Take object `id` out of `nodes` (`hide`: leave it, hidden); whether it was there.
fn take_out(nodes: &mut Vec<Arc<Node>>, id: NodeId, hide: bool) -> bool {
if let Some(i) = nodes.iter().position(|n| n.id == id) {
if hide {
if let Some(n) = nodes.get_mut(i) {
Arc::make_mut(n).visible = false;
}
} else {
nodes.remove(i);
}
return true;
}
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 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.
fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document) -> usize {
let mut found = vec![];
unmark(&mut doc.layers, true, &mut found);
let rect = frame.rect;
if found.is_empty() || found.len() > MAX_WEIGHED {
return 0;
}
let mut with = view.difference(doc, rect).any;
let mut removed = 0;
for (id, bounds) in found {
if bounds.is_none_or(|b| b.intersect(rect).is_zero_area()) {
continue;
}
let mut without = doc.clone();
if !take_out(&mut without.layers, id, true) {
continue;
}
let d = view.difference(&without, rect).any;
if d - with <= UNSEEN_MARGIN {
take_out(&mut doc.layers, id, false);
with = d;
removed += 1;
}
}
removed
}
/// How far (points) the art the layers print may reach past a page that holds it all.
const PAST_THE_PAGE: f64 = 1.0;
/// Do the layers' art and the page's own look alike (where the page holds all the art that prints,
/// also round it)? An error says they don't; `Ok` has the note to give when they differ a little.
fn compare(frame: &Frame<'_>, view: &mut PageView, layered: &Document) -> Result<Option<String>, String> {
let mut diff = view.difference(layered, frame.rect);
if frame.whole
&& let Some(art) = vectorcraft_render::encode::art_bounds(as_printed(layered).as_ref().unwrap_or(layered))
.filter(|a| !frame.rect.inflate(PAST_THE_PAGE, PAST_THE_PAGE).contains_rect(*a))
{
// The page and the art the layers print beyond it, each in its own document's place.
let all = frame.rect.union(art);
let (dx, dy) = (frame.page_rect.x0 - frame.rect.x0, frame.page_rect.y0 - frame.rect.y0);
let round = difference(frame.page, Rect::new(all.x0 + dx, all.y0 + dy, all.x1 + dx, all.y1 + dy), layered, all);
diff = Difference { any: diff.any.max(round.any), solid: diff.solid.max(round.solid) };
}
if diff.any > MAX_DIFFERENCE || diff.solid > MAX_SOLID_DIFFERENCE {
return Err(format!("the art on them differs from the page's in {:.1}% of it", diff.any * 100.0));
}
Ok((diff.any > NOTABLE_DIFFERENCE).then(|| {
format!(
"the art on the layers differs from the file's own page in about {:.1}% of it (brush strokes, live effects and the like that the layers' data doesn't describe)",
diff.any * 100.0
)
}))
}
/// The EPS `visible` (read as its page) read through its editing copy instead: its layers (the
/// hidden ones too), groups, compound paths, clipping groups and artboards, with the text and the
/// strokes around it of the page. When the file has no editing copy, `visible` as it was; when it
/// has one that can't be used, `visible` with a warning that says why.
pub(super) fn layered(ps: &[u8], visible: Imported) -> Imported {
let Some(data) = ai::eps_data(ps) else { return visible };
let result = data.and_then(|data| build(&data, Some(&visible.document), Page::Art, false));
match result {
Ok((document, mut warnings)) => {
for w in &visible.warnings {
if !warnings.contains(w) {
warnings.push(w.clone());
}
}
Imported { document, warnings, preview: false }
}
Err(why) => {
let mut v = visible;
v.warnings.push(format!("{LAYERS_UNREAD} ({why}): it comes in as its page, in one layer"));
v
}
}
}
/// The `.ai` `visible` (read as its PDF part) read through its editing copy (`private`, the joined
/// `AIPrivateData` streams) instead, as [`layered`] reads an EPS: with the layers and objects of the
/// file, its artboards, and the art that lies outside them, which its PDF part doesn't have.
/// `warnings` are the notes of reading the PDF part; the notes returned are those, and the layers'.
/// `outlined`: the PDF part's type was read as outlines (a file whose type shows then comes in as
/// its PDF part).
pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outlined: bool) -> (Document, Vec<String>) {
let Ok(data) = ai::decode_private(private) else { return (visible, warnings) };
if !data.windows(15).any(|w| w == b"%AI5_BeginLayer") {
return (visible, warnings);
}
match build(&data, Some(&visible), Page::Artboard, outlined) {
Ok((done, notes)) => {
let mut all = warnings;
all.extend(notes.into_iter().filter(|n| !all.contains(n)).collect::<Vec<_>>());
(done, all)
}
Err(why) => {
let mut all = warnings;
all.push(format!("{LAYERS_UNREAD} from its editing data ({why}): they come from its PDF part"));
(visible, all)
}
}
}
/// A `.ai` saved without its PDF part (its pages show only a placeholder) from its editing copy
/// alone (`private`, the joined `AIPrivateData` streams) → the document and the import's notes.
pub fn ai_alone(private: &[u8]) -> Result<(Document, Vec<String>), String> {
let data = ai::decode_private(private)?;
build(&data, None, Page::Artboard, false)
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_doc::{CharStyle, TextObject};
fn piece(text: &str, x: f64, y: f64) -> Arc<Node> {
Arc::new(Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(x, y), text, CharStyle::default())))))
}
/// Lines that the page draws in pieces, the pieces of one between those of the other, are each
/// joined, and only their own pieces go.
#[test]
fn interleaved_lines_are_joined_without_taking_each_others_pieces() {
let mut parts = vec![piece("He", 0.0, 0.0), piece("ab", 0.0, -20.0), piece("cd", 16.0, -20.0), piece("llo", 20.0, 0.0)];
let lines = [("Hello".to_string(), Point::new(0.0, 0.0)), ("abcd".to_string(), Point::new(0.0, -20.0))];
join_pieces(&mut parts, &lines);
let texts: Vec<String> = parts.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None }).collect();
assert_eq!(texts, ["Hello", "abcd"]);
}
}