diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index eaa8d167a..b1a211249 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -33,7 +33,7 @@ use super::lex::find; use crate::ps; pub(crate) use frames::text_frames; -pub use read::{IMAGE_TOO_LARGE, Structure, artboards, palette, read, variables}; +pub use read::{IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, palette, read, variables}; pub(crate) use read::{slot_frame, slot_of}; #[cfg(test)] diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 96903f641..bba345254 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -74,7 +74,7 @@ mod ops; mod types; use art::preview_png; -pub use document::{VariableLibrary, artboards, palette, variables}; +pub use document::{PaletteSwatches, VariableLibrary, artboards, palette, variables}; pub(crate) use document::{slot_frame, slot_of}; pub use types::IMAGE_TOO_LARGE; use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, Place, Raster}; diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index 9fa54cf8e..6bec430d1 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -72,15 +72,42 @@ fn artboard_array(data: &[u8]) -> Option<&[u8]> { } } +/// The palette of a `.ai` file's editing data, as [`palette`] reads it. +pub struct PaletteSwatches { + pub swatches: Vec, + pub groups: Vec, + /// The patterns the editing data defines that the given patterns lack and a swatch paints with. + pub patterns: Vec, + /// The image blobs the art of `patterns` shows, by key. + pub images: std::collections::BTreeMap, +} + /// The swatches and color groups of the palette of the editing data `data`, for a file whose /// layers aren't read: the swatches its PDF part has are the defaults (#4036). They come as -/// [`read`] makes them, with pattern swatches only for the patterns in `patterns`; `None` when the -/// palette is empty or reading stops before it. -pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option<(Vec, Vec)> { +/// [`read`] makes them, with pattern swatches only for the patterns in `patterns` and those the +/// editing data defines; `None` when the palette is empty or reading stops before it. +pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option { let mut r = Reader::new(data); - // The palette and the gradients it names come before the art, so an error in the art leaves them read. + // The palette, the gradients and the patterns it names come before the art, so an error in the + // art leaves them read. let _ = r.run(); - r.palette_swatches(patterns) + let mut known = patterns.to_vec(); + let defined = std::mem::take(&mut r.doc.patterns); + known.extend(defined.iter().filter(|d| !patterns.iter().any(|p| p.name == d.name)).cloned()); + let (swatches, groups) = r.palette_swatches(&known)?; + let painted = |name: &str| { + swatches.iter().chain(groups.iter().flat_map(|g| &g.swatches)).any(|s| matches!(&s.paint, Paint::Pattern { pattern, .. } if pattern == name)) + }; + let added: Vec = defined.into_iter().filter(|d| !patterns.iter().any(|p| p.name == d.name) && painted(&d.name)).collect(); + let mut shown = Document::new(1.0, 1.0); + shown.patterns.clone_from(&added); + let mut images = std::collections::BTreeMap::new(); + shown.visit_images(|_, im| { + if let Some(blob) = r.doc.images.get(&im.key) { + images.insert(im.key.clone(), blob.clone()); + } + }); + Some(PaletteSwatches { swatches, groups, patterns: added, images }) } /// [`artboards`] as far as reading `data` from its start gets: those read before an error, and diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index 0582bf074..c4403cdcf 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -89,9 +89,18 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec, outl /// `doc` (a `.ai` read as its PDF part, in place of its layers) with the swatches and color groups /// of the palette of its editing data `data` in place of the default ones, as [`build`] gives a /// document read from the layers. The spot and pattern swatches of the PDF part's art that the -/// palette doesn't have follow the palette's swatches. Left as it is when the palette is empty. +/// palette doesn't have follow the palette's swatches. The patterns that the editing data defines and +/// the PDF part lacks come in for the palette's pattern swatches, with the images they show. Left as +/// it is when the palette is empty. fn use_palette(data: &[u8], doc: &mut Document) { - let Some((swatches, groups)) = ai::palette(data, &doc.patterns) else { return }; + let Some(ai::PaletteSwatches { swatches, groups, patterns, images }) = ai::palette(data, &doc.patterns) else { return }; + for (key, blob) in images { + doc.images.entry(key).or_insert(blob); + } + for mut p in patterns { + p.art = p.art.iter().map(|n| Arc::new(doc.reid(n))).collect(); + doc.patterns.push(p); + } let named = |name: &str| swatches.iter().chain(groups.iter().flat_map(|g| &g.swatches)).any(|s| s.name == name); let art: Vec<_> = doc.swatches.iter().filter(|s| (s.spot || matches!(s.paint, vectorcraft_color::Paint::Pattern { .. })) && !named(&s.name)).cloned().collect(); diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index fd3bf9a9b..7d804142c 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -290,6 +290,32 @@ fn an_ai_file_that_comes_in_as_its_pdf_part_keeps_the_swatches_of_its_palette() assert!(d.swatch("Red ink").is_some_and(|s| s.spot)); } +/// A `.ai` pattern library (a palette of pattern swatches and no layers) whose PDF part defines no +/// pattern gets the patterns its editing data defines with the palette's pattern swatches. +#[test] +fn an_ai_file_without_layers_keeps_the_patterns_of_its_palette() { + let dots = "%AI3_BeginPattern: (Dots)\n(Dots) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n"; + let palette = + "%AI5_BeginPalette\n0 0 Pb\n0 0 0 0 1 1 1 Xa\n(White)\nPc\n(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n(Dots)\nPc\nPB\n%AI5_EndPalette\n"; + let art = native(&format!("{dots}{palette}")); + let (d, warnings, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + assert!(!read, "{warnings:?}"); + let swatches: Vec<&str> = d.swatches.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(swatches, ["[None]", "White", "Dots"], "{warnings:?}"); + assert!(matches!(&d.swatch("Dots").map(|s| &s.paint), Some(vectorcraft_color::Paint::Pattern { pattern, .. }) if pattern == "Dots")); + assert!(d.pattern("Dots").is_some_and(|p| !p.art.is_empty())); + + // A pattern that shows an image brings the image's data with it. + let pic = "%AI3_BeginPattern: (Pic)\n(Pic) 0 0 4 4\n%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceGray XN\n[ 4 0 0 4 0 4 ] 1 1 0 Xh\n\ + [ 4 0 0 4 0 4 ] 0 0 1 1 1 1 8 1 0 0 1 0 4 4 0 0\n%%BeginData: 1\rXI\n\u{0}%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN\nE\n%AI3_EndPattern\n"; + let art = native(&format!("{pic}{}", palette.replace("(Dots)", "(Pic)"))); + let (d, warnings, _) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0); + let mut keys = vec![]; + d.visit_images(|_, im| keys.push(im.key.clone())); + assert_eq!(keys.len(), 1, "{warnings:?}"); + assert!(keys.iter().all(|k| d.images.contains_key(k)), "{keys:?}"); +} + /// An EPS whose editing data can't be used, a palette with no layers among others, comes in as its /// page with the swatches and color groups of that palette in place of the defaults. #[test]