Flatten transparent image pattern tiles in legacy .ai v7 export

Fixes #3666

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-24 02:06:40 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 2e572c7805
commit 94759cbe71
3 changed files with 224 additions and 9 deletions
+33 -6
View File
@@ -15,8 +15,13 @@
//! blending mode written opaque is a box hiding the art under it (version 7 images have no alpha
//! channel, §8.1). With `flatten` (the default) each becomes an opaque image of itself and the art
//! under it, rendered from the document as it is, clipped to the area where it changes what shows;
//! the art under it stays as it was. With `omit` they are left out. An image that can't be decoded
//! is missing from the images that flattening and expanding raster effects draw, and the warnings
//! the art under it stays as it was. An image in a group with opacity or a blending mode is
//! flattened the same way, and a pattern fill that paints a transparent image (see-through
//! pixels, or opacity or a blending mode on the image, the fill, the shape or a group it is in) is
//! expanded into its tiles first so its images flatten too. With `omit` the images with
//! see-through pixels, opacity or a blending mode of their own are left out, and pattern fills stay
//! as the writer draws them (their tiles opaque over white). An image that can't be decoded is
//! missing from the images that flattening and expanding raster effects draw, and the warnings
//! then carry the raster exports' note once.
//!
//! The format can't hide objects, only layers (`hiddenObjects`): with `layer` (the default) each
@@ -117,6 +122,9 @@ pub(super) fn encode(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result
let (full, placed) = crate::cmd::place::document::full_documents(&one);
let mut d = full.into_owned();
expand_appearances(&mut d);
if mode == SeeThrough::Flatten {
expand_image_patterns(&mut d)?;
}
let images = see_through_images(&d, mode);
if !images.is_empty() {
note(&mut enc.warnings, if mode == SeeThrough::Flatten { FLATTENED } else { OMITTED });
@@ -153,17 +161,30 @@ fn expand_appearances(d: &mut Document) {
}
}
/// Expand every layer's pattern fills that paint a transparent image into their tiles, so those
/// images flatten like any other transparent image. The writer would otherwise draw them opaque
/// over white.
fn expand_image_patterns(d: &mut Document) -> Result<()> {
let keys = super::super::flatten::see_through_images(d);
let layers: Vec<NodeId> = d.layers.iter().map(|l| l.id).collect();
for id in layers {
super::super::expand::expand_transparent_image_patterns(d, id, &keys)?;
}
Ok(())
}
/// The images of `d` a legacy `.ai` can't hold, in paint order with their places in it (as
/// [`Document::walk`] counts): those with see-through pixels, opacity or a blending mode. Only
/// visible ones flatten (a hidden one, which the hidden objects option may write, is written over
/// white); all are left out with `omit`.
/// [`Document::walk`] counts). These are the images with see-through pixels, and the images
/// that have opacity or a blending mode of their own or, with `flatten`, of a group they are in.
/// With `flatten` only visible images are returned, because a hidden one (which the hidden objects
/// option may write) is written over white. With `omit` all of them are returned.
fn see_through_images(d: &Document, mode: SeeThrough) -> Vec<(usize, NodeId)> {
let keys = super::super::flatten::see_through_images(d);
let mut out = vec![];
let mut at = 0;
d.walk(|n| {
if let NodeKind::Image(im) = &n.kind
&& (keys.contains(&im.key) || n.opacity < 1.0 || n.blend != BlendMode::Normal)
&& (keys.contains(&im.key) || n.opacity < 1.0 || n.blend != BlendMode::Normal || (mode == SeeThrough::Flatten && transparent(d, n.id)))
{
out.push((at, n.id));
}
@@ -175,6 +196,12 @@ fn see_through_images(d: &Document, mode: SeeThrough) -> Vec<(usize, NodeId)> {
out
}
/// Whether node `id` or a group it is in has opacity or a blending mode. [`Document::ancestry`]
/// lists `id` itself last, so its own opacity and blending mode count too.
fn transparent(d: &Document, id: NodeId) -> bool {
d.ancestry(id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).any(|n| n.opacity < 1.0 || n.blend != BlendMode::Normal)
}
/// Replace each of `images` (from [`see_through_images`]) with an opaque image of what shows
/// where it is, clipped to where it changes that; one that changes nothing is left out. Each is
/// rendered from `d` as it was, so images over one another flatten as they look.