diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index 19269a165..9d281c213 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -740,13 +740,14 @@ fn differ_in_colour(p: &[u8; 4], q: &[u8; 4]) -> bool { p.iter().zip(q).any(|(a, b)| a.abs_diff(b) > SAME_COLOUR) } -/// 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). +/// How much two documents draw differently: the share (0 to 1) of the pixels that differ, of those +/// that differ away from type, and of those whose neighbors 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, + art: f64, solid: f64, } @@ -866,19 +867,20 @@ fn difference(a: &Document, ra: Rect, b: &Document, rb: Rect) -> Difference { /// 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 == 0 || x.height == 0 || x.width != y.width || x.height != y.height { - return Difference { any: 1.0, solid: 1.0 }; + return Difference { any: 1.0, art: 1.0, solid: 1.0 }; } let differs: Vec = 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); + let art = differs.iter().zip(&typed).filter(|(d, t)| **d && !**t).count(); // A pixel on the region's edge has neighbours outside it, which don't count as differing. let solid = (1..h.saturating_sub(1)) .flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j))) .filter(|&(i, j)| !typed.get(j * w + i).copied().unwrap_or(true) && (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n)))) .count(); let 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 } + Difference { any: differs.iter().filter(|d| **d).count() as f64 / area, art: art 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 @@ -1243,7 +1245,9 @@ fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document) -> u 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. +/// also round it)? An error says they don't; `Ok` has the note to give when they differ a little: +/// with the share of the pixels that differ away from type when that is notable, otherwise with the +/// share of those that differ in the type's areas. fn compare(frame: &Frame<'_>, view: &mut PageView, layered: &Document) -> Result, String> { let mut diff = view.difference(layered, frame.rect); if frame.whole @@ -1254,15 +1258,23 @@ fn compare(frame: &Frame<'_>, view: &mut PageView, layered: &Document) -> Result 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) }; + diff = Difference { any: diff.any.max(round.any), art: diff.art.max(round.art), 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(|| { + if diff.any <= NOTABLE_DIFFERENCE { + return Ok(None); + } + Ok(Some(if diff.art > NOTABLE_DIFFERENCE { 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 + diff.art * 100.0 + ) + } else { + format!( + "the type on the layers differs from the file's own page in about {:.1}% of it (type laid out again from the file's text, or drawn in another font)", + (diff.any - diff.art) * 100.0 ) })) } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 5c8e7c6bf..1344fba28 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -907,6 +907,34 @@ fn an_object_the_page_doesnt_draw_makes_the_file_come_in_as_its_page() { assert!(r.warnings.iter().any(|w| w.contains("layers weren't read") && w.contains("differs from the page's")), "{:?}", r.warnings); } +/// The page sets "Hi there" in pieces, and the layers lay the line out again as one: the page and +/// the layers differ only where the type is. +fn page_in_pieces_and_layers(art: &str) -> Vec { + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 61.5 moveto (Hi) show 70 61.5 moveto (there) show"; + eps(page, &native_with_text(&layer("Art", true, &format!("{art}{}", text_object(0))), &doc)) +} + +#[test] +fn type_laid_out_again_is_noted_as_type_and_not_as_art_the_layers_dont_describe() { + let r = import(&page_in_pieces_and_layers("")).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.starts_with("the type on the layers differs from the file's own page")), "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("brush strokes")), "{:?}", r.warnings); +} + +#[test] +fn art_the_page_doesnt_draw_is_noted_with_its_own_share_and_not_the_types() { + // A bar of 0.5 × 80 points that the page doesn't draw, too thin to be refused as an object: 0.4% of + // the page, away from the type. + let bar = "0 0 0 1 k\n20 10 m\n20.5 10 L\n20.5 90 L\n20 90 L\nf\n"; + let r = import(&page_in_pieces_and_layers(bar)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let notes: Vec<&String> = r.warnings.iter().filter(|w| w.contains("differs from the file's own page")).collect(); + assert_eq!(notes.len(), 1, "{:?}", r.warnings); + assert!(notes[0].starts_with("the art on the layers differs from the file's own page in about 0.4% of it (brush strokes"), "{notes:?}"); +} + /// An EPS whose page is its art's box `[x0 y0 x1 y1]` (not its artboard, 0 0 100 100), drawing /// `page`, with the editing copy of `layers`. fn eps_of_art(art_box: [u32; 4], page: &str, layers: &str) -> Vec { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c93d6180c..acf95a0f2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -682,7 +682,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In a `.ai` or EPS opened from its editing data, type has its text object's opacity and blend mode, whether it is made from the file's text document or taken from the page. Where such type has transparency and comes in with other art (the rest of its look, or page type and strokes that go with it), a group of them has it. Where the page draws its type, or the strokes around it, with transparency that is neither its text object's nor its text object's combined with that of the objects around it, the page's type and strokes keep their own instead (`crates/eps/src/import/tests_native.rs`). In SVG export without Fewer `` Elements, each character of live type that justification or Mojikumi moves (the letter gaps of a Justify All line without word spaces, the gaps between the characters of a justified Japanese line, closed-up punctuation, the space between Japanese and Latin letters) starts its own positioned ``, and type scaled horizontally on a justified line is stretched to its own width (`crates/svg/tests/text_layout.rs`). A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304). - In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). + In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). Where the art of a `.ai` or EPS opened from its editing data differs a little from the file's page, the warning gives the share of the page that differs away from type. When that share is 0.2% or less, the warning names the type and gives the share of the page that differs where the type is (`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). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points. Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests_links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).