From d7e79448d7a5d247acc4895047cc98d6926deeb4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 17 Oct 2026 14:13:04 -0700 Subject: [PATCH] Legacy .ai export: warns when an image in art it flattens or expands can't be decoded Saving a version 7 .ai expands raster effects into images and flattens images with see-through pixels, opacity or a blending mode, and both draw the art with the renderer. An image the renderer couldn't decode drew nothing, so it was missing from the result, and the reply carried no note. A semi-transparent image cut short was removed without one. The save now compares decode_failures() before and after it writes the artboards, as Expand Appearance and the raster exports do, and adds "an image couldn't be decoded and is missing from the picture" to its warnings once. Docs: docs/mcp.md (aiVersion "7"). --- crates/engine/src/cmd/fileio/legacy_ai.rs | 10 +++++- crates/engine/src/tests_legacy_ai.rs | 42 +++++++++++++++++++++++ docs/mcp.md | 4 ++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/fileio/legacy_ai.rs b/crates/engine/src/cmd/fileio/legacy_ai.rs index 5d1df1493..8dbd5ffde 100644 --- a/crates/engine/src/cmd/fileio/legacy_ai.rs +++ b/crates/engine/src/cmd/fileio/legacy_ai.rs @@ -15,7 +15,9 @@ //! 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. +//! 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 +//! then carry the raster exports' note once. //! //! The format can't hide objects, only layers (`hiddenObjects`): with `layer` (the default) each //! layer's hidden objects go to a hidden layer of their own just above it, with `omit` they are @@ -104,6 +106,7 @@ pub(super) fn encode(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result enc.warnings.push(SEPARATE.to_string()); } let created = vectorcraft_doc::metadata::now_unix(); + let failed = vectorcraft_render::decode_failures(); for b in boards { let one = super::save::artboard_doc(doc, b); let (full, placed) = crate::cmd::place::document::full_documents(&one); @@ -127,6 +130,11 @@ pub(super) fn encode(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result } enc.files.push((Some(b), out.bytes)); } + // Expanding raster effects and flattening images draw the art, and an image the renderer + // couldn't decode drew nothing there. + if let Some(w) = super::super::rasterfx::undecoded_warning(failed) { + note(&mut enc.warnings, w); + } Ok(enc) } diff --git a/crates/engine/src/tests_legacy_ai.rs b/crates/engine/src/tests_legacy_ai.rs index e38e0f2fa..aa6f82f08 100644 --- a/crates/engine/src/tests_legacy_ai.rs +++ b/crates/engine/src/tests_legacy_ai.rs @@ -3,6 +3,7 @@ use serde_json::{Value, json}; +use super::tests_menucmds::place_undecodable_png; use super::*; use crate::tests_links::Folder; @@ -262,6 +263,47 @@ fn a_transparent_image_is_flattened_with_the_art_under_it_which_stays() { assert_eq!(colour_at(page, 10.0, 20.0), [255, 0, 0], "the square outside the image"); } +/// How many of reply `r`'s warnings say that an image couldn't be decoded, as raster exports do. +fn undecoded_notes(r: &Value) -> usize { + warnings(r).iter().filter(|w| *w == "an image couldn't be decoded and is missing from the picture").count() +} + +#[test] +fn a_flattened_image_that_cant_be_decoded_is_noted_once() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap(); + s.execute("artboard.new", &json!({"x": 0, "y": 0, "width": 200, "height": 100, "name": "B"})).unwrap(); + let cut = place_undecodable_png(&mut s); + s.execute("transparency.set", &json!({"ids": [cut.0], "opacity": 50})).unwrap(); + // Both artboards flatten it. + let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7"})).unwrap(); + assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{r}"); + assert!(warnings(&r).iter().any(|w| w.contains("flattened with the art under them")), "{r}"); + assert_eq!(undecoded_notes(&r), 1, "{r}"); +} + +#[test] +fn an_image_that_cant_be_decoded_with_an_expanded_effect_is_noted() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap(); + let cut = place_undecodable_png(&mut s); + s.execute("effect.apply", &json!({"effect": "blur.gaussian", "params": {"radius": 3}, "ids": [cut.0]})).unwrap(); + // Left out rather than flattened: only expanding the blur draws it. + let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7", "transparentImages": "omit"})).unwrap(); + assert_eq!(undecoded_notes(&r), 1, "{r}"); +} + +#[test] +fn images_that_decode_give_no_decode_note() { + let mut s = shadow_over_square(); + s.execute("effect.apply", &json!({"effect": "blur.gaussian", "params": {"radius": 3}})).unwrap(); // the blue image + for mode in ["flatten", "omit"] { + let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7", "transparentImages": mode})).unwrap(); + assert!(warnings(&r).iter().any(|w| w.contains("transparent images")), "{r}"); + assert_eq!(undecoded_notes(&r), 0, "{r}"); + } +} + #[test] fn transparent_images_can_be_left_out_and_opacity_makes_one_transparent() { let mut s = shadow_over_square(); diff --git a/docs/mcp.md b/docs/mcp.md index fa29dba2b..e7342acab 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -690,7 +690,9 @@ PostScript form, which keeps layers, groups, compound paths and clipping groups of `range` (default all; `{name}-{artboard}.ai` when several, listed in the result's `files`), with `transparentImages: "flatten"` (default) or `"omit"` for images with see-through pixels, opacity or a blending mode, `hiddenObjects: "layer"` (default: a hidden layer of each layer's hidden objects), `"omit"` or `"visible"`, and -`outlineText: true` to write type as outlines instead of text objects. From the CLI: `vectorcraft-cli convert in.svg +`outlineText: true` to write type as outlines instead of text objects. When an image in art that is flattened or whose +raster effects are expanded can't be decoded, the result does not show it, and `warnings` carries "an image couldn't +be decoded and is missing from the picture" once. From the CLI: `vectorcraft-cli convert in.svg out.ai --ai-version 7 [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] [--outline-text] [--range R]`.