Fix #4036: a pattern defined only in a library's editing data keeps its swatch

A .ai swatch library without layers comes in as its PDF part with the
swatches of its editing data's palette. A pattern swatch was kept only
when the PDF part defined its pattern, and a pattern library's PDF part
usually defines none, so its pattern swatches were dropped.

ai::palette now gives pattern swatches for the patterns the editing
data defines too, and returns the definitions the PDF part lacks that a
palette swatch paints with, with the images their art shows. The
document gets them with new object ids.
This commit is contained in:
dotCooCoo
2026-10-29 10:10:58 +07:00
committed by bflatastic
parent 754051c26a
commit 48602ade23
5 changed files with 71 additions and 9 deletions
+1 -1
View File
@@ -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)]
+1 -1
View File
@@ -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};
+32 -5
View File
@@ -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<Swatch>,
pub groups: Vec<vectorcraft_color::swatch::SwatchGroup>,
/// The patterns the editing data defines that the given patterns lack and a swatch paints with.
pub patterns: Vec<PatternDef>,
/// The image blobs the art of `patterns` shows, by key.
pub images: std::collections::BTreeMap<String, ImageBlob>,
}
/// 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<Swatch>, Vec<vectorcraft_color::swatch::SwatchGroup>)> {
/// [`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<PaletteSwatches> {
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<PatternDef> = 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
+11 -2
View File
@@ -89,9 +89,18 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, 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();
+26
View File
@@ -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]