//! 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 = 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 = (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 = (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 { 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 = 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"); } }