mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
203 lines
11 KiB
Rust
203 lines
11 KiB
Rust
//! 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). A file in the
|
||
//! legacy format (a `.ai` in PostScript form: versions 3 to 8, and the apps that still write it)
|
||
//! is that copy itself: its program has the layers, and the prolog it names defines the operators
|
||
//! for printing (other apps name it without including it, so their files can't be run).
|
||
//!
|
||
//! The copy 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`). When the text document has the type of every text object that shows,
|
||
//! all the type is made from it, each text object in its own place, and the page's type is only
|
||
//! compared with it. Otherwise the type it has is still made from it, and the page's type that
|
||
//! lies elsewhere goes into the text objects it can't make, by where the text document puts their
|
||
//! lines or else in painting order. A text object whose type neither has (a frame of a kind it
|
||
//! doesn't read, on a hidden layer or object) is 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::borrow::Cow;
|
||
use std::collections::BTreeSet;
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::BlendMode;
|
||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, Scaling, TextKind, Unit};
|
||
use vectorcraft_geom::{Affine, PathData, Point, Rect, Vec2};
|
||
|
||
use super::Imported;
|
||
use super::ai::{self, Structure, slot_of};
|
||
use super::ate::{Story, Texts};
|
||
|
||
mod build;
|
||
mod compare;
|
||
mod layers;
|
||
mod plan;
|
||
mod slots;
|
||
|
||
#[cfg(test)]
|
||
use build::{Frame, MADE, prune_unseen};
|
||
#[cfg(test)]
|
||
use compare::{
|
||
MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, Shares, compare, compared, comparison_render, comparison_samples, differ, differ_in_colour,
|
||
};
|
||
pub use layers::{ai_alone, layered_ai};
|
||
pub(super) use layers::{layered, program_alone};
|
||
pub use slots::is_loss;
|
||
#[cfg(test)]
|
||
use slots::join_pieces;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use vectorcraft_doc::{CharStyle, TextObject};
|
||
|
||
/// `doc` with a square ring round (20, 20)–(80, 80) (or at `at`), stroked `color` `width` wide,
|
||
/// named `name`.
|
||
fn ring(doc: &mut Document, name: Option<&str>, color: vectorcraft_color::Color, width: f64, at: Rect) {
|
||
let stroke = vectorcraft_color::Paint::solid(color);
|
||
let mut n = Node::path(
|
||
doc.alloc_id(),
|
||
vectorcraft_geom::shapes::rectangle(at),
|
||
vectorcraft_doc::Appearance::basic(vectorcraft_color::Paint::None, stroke, width),
|
||
);
|
||
n.name = name.map(str::to_string);
|
||
let layer = doc.layers[0].id;
|
||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||
}
|
||
|
||
/// A text object's red stroke drawn over a dark red one of about the same lightness is drawn
|
||
/// by the page: it isn't taken out as unseen (#1102). An object the page doesn't draw still is.
|
||
#[test]
|
||
fn a_stroke_over_one_as_light_is_not_taken_for_unseen() {
|
||
let (dark, red) = (vectorcraft_color::Color::rgb(0.55, 0.0, 0.0), vectorcraft_color::Color::rgb(1.0, 0.0, 0.0));
|
||
let square = Rect::new(20.0, 20.0, 80.0, 80.0);
|
||
let mut page = Document::new(100.0, 100.0);
|
||
ring(&mut page, None, dark, 12.0, square);
|
||
ring(&mut page, None, red, 6.0, square);
|
||
let mut layers = Document::new(100.0, 100.0);
|
||
ring(&mut layers, Some(MADE), dark, 12.0, square);
|
||
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 };
|
||
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();
|
||
assert_eq!(widths, [12.0, 6.0]);
|
||
// Taken by lightness alone, the red and the dark red are alike.
|
||
let px = |c: vectorcraft_color::Color| {
|
||
let [r, g, b] = c.to_rgb().map(|v| (v * 255.0).round() as u8);
|
||
[r, g, b, 255]
|
||
};
|
||
assert!(!differ(&px(dark), &px(red)) && differ_in_colour(&px(dark), &px(red)));
|
||
}
|
||
|
||
/// The share of a rectangle's pixels in the areas, however many areas there are: none for one
|
||
/// that only shares their edge, lies off the drawing or isn't a number.
|
||
#[test]
|
||
fn the_share_of_a_rectangle_in_the_areas_counts_its_pixels() {
|
||
let in_areas = |areas: &[Rect], w: usize, h: usize| {
|
||
let marks: Vec<bool> = (0..w * h)
|
||
.map(|k| {
|
||
let (x, y) = ((k % w) as f64 + 0.5, (k / w) as f64 + 0.5);
|
||
areas.iter().any(|a| a.contains(Point::new(x, y)))
|
||
})
|
||
.collect();
|
||
let shares = Shares::of(&marks, w, h);
|
||
move |r: &Rect| shares.share(r)
|
||
};
|
||
let share = in_areas(&[Rect::new(10.0, 10.0, 20.0, 20.0), Rect::new(30.0, 0.0, 31.0, 1.0)], 40, 40);
|
||
assert_eq!((share(&Rect::new(12.0, 12.0, 22.0, 18.0)), share(&Rect::new(10.0, 10.0, 30.0, 20.0))), (0.8, 0.5));
|
||
assert_eq!((share(&Rect::new(15.0, 15.0, 30.0, 30.0)), share(&Rect::new(30.2, 0.2, 30.8, 0.8))), (25.0 / 225.0, 1.0));
|
||
assert_eq!(
|
||
[share(&Rect::new(20.0, 20.0, 30.0, 30.0)), share(&Rect::new(0.0, 0.0, 5.0, 5.0)), share(&Rect::new(50.0, 50.0, 60.0, 60.0))],
|
||
[0.0; 3]
|
||
);
|
||
assert_eq!(share(&Rect::new(f64::NAN, 0.0, 39.0, 39.0)), 0.0);
|
||
let many: Vec<Rect> = (0..5000).map(|i| Rect::new(f64::from(i % 70), 50.0, f64::from(i % 70) + 1.0, 51.0)).collect();
|
||
let share = in_areas(&many, 80, 80);
|
||
assert_eq!((share(&Rect::new(60.0, 50.0, 75.0, 51.0)), share(&Rect::new(70.0, 50.0, 75.0, 51.0))), (10.0 / 15.0, 0.0));
|
||
}
|
||
|
||
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"]);
|
||
}
|
||
/// A flattened page samples a fine stripe pattern once per comparison pixel; that
|
||
/// aliases to white although vector coverage is gray. Shared minification avoids the
|
||
/// false fallback, while genuinely missing art must still be rejected.
|
||
#[test]
|
||
fn raster_page_comparison_filters_minification_without_accepting_missing_art() {
|
||
use vectorcraft_color::{Color, Paint};
|
||
use vectorcraft_doc::{Appearance, ImageBlob, ImageObject};
|
||
let mut vector = Document::new(1000.0, 500.0);
|
||
let layer = vector.layers[0].id;
|
||
for i in 0..400 {
|
||
let x = f64::from(i) * 2.5 + 0.5;
|
||
let n = Node::path(
|
||
vector.alloc_id(),
|
||
vectorcraft_geom::shapes::rectangle(Rect::new(x, 0.0, x + 1.0, 500.0)),
|
||
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||
);
|
||
vector.insert(Some(layer), usize::MAX, n).unwrap();
|
||
}
|
||
let rect = vector.artboards[0].rect;
|
||
let source = vectorcraft_render::Renderer::new().render_region(&vector, rect, 1.6, true);
|
||
let mut png = Vec::new();
|
||
image::RgbaImage::from_raw(source.width, source.height, source.to_straight())
|
||
.unwrap()
|
||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||
.unwrap();
|
||
let blob = ImageBlob::new("image/png", png);
|
||
let key = blob.content_key();
|
||
let mut page = Document::new(1000.0, 500.0);
|
||
page.images.insert(key.clone(), blob);
|
||
let image = Node::new(
|
||
page.alloc_id(),
|
||
NodeKind::Image(Box::new(ImageObject {
|
||
key,
|
||
width: source.width,
|
||
height: source.height,
|
||
xf: Affine::scale(1.0 / 1.6),
|
||
link: None,
|
||
placement: Default::default(),
|
||
pixels: None,
|
||
})),
|
||
);
|
||
page.insert(Some(page.layers[0].id), usize::MAX, image).unwrap();
|
||
assert_eq!(comparison_samples(&page), 4);
|
||
assert_eq!(comparison_samples(&vector), 1, "ordinary vector pages retain their comparison");
|
||
let mut renderer = vectorcraft_render::Renderer::new();
|
||
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 };
|
||
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");
|
||
}
|
||
}
|