Export: EPS, legacy .ai, EMF/WMF and DXF give one note for images that can't be decoded

EPS, EMF, WMF and DXF exports gave each image they left out because it
couldn't be decoded its own warning, "image '<key>' could not be
decoded and was left out", which names the internal key the document
stores the image under. A version 7 .ai said "an image that can't be
decoded or is too large was left out".

The four writers now give the note raster exports, PDF and the legacy
.ai flattening give, "an image couldn't be decoded and is missing from
the picture", once per export. In a version 7 .ai it is one line with
the note for flattened art. The other image notes in the same
functions no longer name the key: EPS's note for an image too large to
write, and DXF's notes for an image with no pixels (its rows in 23
catalogs are reworded) and one that could not be written. Legacy .ai
gives a separate note for an image that is empty or too large.

Docs: docs/mcp.md (EPS, DXF export, EMF and WMF, aiVersion "7").
This commit is contained in:
dotCooCoo
2026-10-22 06:13:32 +07:00
committed by bflatastic
parent 3bb0f07a4b
commit 4e4723699d
34 changed files with 158 additions and 40 deletions
+18
View File
@@ -125,6 +125,24 @@ fn placed_images_come_back_as_linked_files() {
let _ = std::fs::remove_dir_all(dir);
}
/// Two placed images whose pixel data can't be decoded are left out of the drawing and get no
/// linked file, and `warnings` holds the note raster exports give, once, without naming either
/// image's key.
#[test]
fn images_that_cant_be_decoded_give_the_raster_note_once() {
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
let mut s = session(200.0, 100.0, 1);
crate::tests_menucmds::place_undecodable_png(&mut s);
crate::tests_menucmds::place_undecodable_png_sized(&mut s, 32, 16);
let keys: Vec<String> = s.doc().unwrap().doc.images.keys().cloned().collect();
assert_eq!(keys.len(), 2);
let r = s.execute("document.export", &json!({"format": "dxf"})).unwrap();
let w: Vec<&str> = r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
assert_eq!(w.iter().filter(|w| **w == NOTE).count(), 1, "{w:?}");
assert!(!w.iter().any(|w| keys.iter().any(|k| w.contains(k.as_str()))), "{w:?}");
assert!(entities(&text(&r)).is_empty() && r.get("linked").is_none(), "{r}");
}
#[test]
fn bad_dxf_options_are_refused() {
let mut s = two_boards();
+17
View File
@@ -156,6 +156,23 @@ fn placed_images_are_written_and_previews_are_black_and_white_on_request() {
assert!(l3.contains("/FlateDecode filter >> image"));
}
/// Two placed images whose pixel data can't be decoded are left out of the EPS, and `warnings`
/// holds the note raster exports give, once, without naming either image's key.
#[test]
fn images_that_cant_be_decoded_give_the_raster_note_once() {
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
let mut s = session(200.0, 100.0, 1);
crate::tests_menucmds::place_undecodable_png(&mut s);
crate::tests_menucmds::place_undecodable_png_sized(&mut s, 32, 16);
let keys: Vec<String> = s.doc().unwrap().doc.images.keys().cloned().collect();
assert_eq!(keys.len(), 2);
let r = s.execute("document.exportEps", &json!({"previewFormat": "none"})).unwrap();
let w = warnings(&r);
assert_eq!(w.iter().filter(|w| *w == NOTE).count(), 1, "{w:?}");
assert!(!w.iter().any(|w| keys.iter().any(|k| w.contains(k.as_str()))), "{w:?}");
assert!(!postscript(&b64(&r)).contains("filter >> image"), "no image is written");
}
#[test]
fn bad_eps_options_are_refused_and_formats_list_eps() {
let mut s = session(100.0, 100.0, 1);
@@ -95,6 +95,24 @@ fn raster_effects_are_written_as_images() {
assert!(kinds.contains(&114), "the shadow is an image with transparency");
}
/// Two placed images whose pixel data can't be decoded are left out of an EMF or WMF, and
/// `warnings` holds the note raster exports give, once, without naming either image's key.
#[test]
fn images_that_cant_be_decoded_give_the_raster_note_once() {
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
let mut s = session(200.0, 100.0, 1);
crate::tests_menucmds::place_undecodable_png(&mut s);
crate::tests_menucmds::place_undecodable_png_sized(&mut s, 32, 16);
let keys: Vec<String> = s.doc().unwrap().doc.images.keys().cloned().collect();
assert_eq!(keys.len(), 2);
for format in ["emf", "wmf"] {
let r = s.execute("document.export", &json!({"format": format})).unwrap();
let w: Vec<&str> = r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
assert_eq!(w.iter().filter(|w| **w == NOTE).count(), 1, "{format}: {w:?}");
assert!(!w.iter().any(|w| keys.iter().any(|k| w.contains(k.as_str()))), "{format}: {w:?}");
}
}
#[test]
fn metafiles_open_and_place() {
let mut s = two_boards();
+24 -1
View File
@@ -3,7 +3,7 @@
use serde_json::{Value, json};
use super::tests_menucmds::place_undecodable_png;
use super::tests_menucmds::{place_undecodable_png, place_undecodable_png_sized};
use super::*;
use crate::tests_links::Folder;
@@ -362,6 +362,29 @@ fn a_flattened_image_that_cant_be_decoded_is_noted_once() {
assert_eq!(undecoded_notes(&r), 1, "{r}");
}
/// Images that can't be decoded and are written as they are get the same note as one that is
/// flattened. All of them give one note, and the note names no image's key.
#[test]
fn images_written_or_flattened_that_cant_be_decoded_are_noted_once() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
place_undecodable_png(&mut s);
place_undecodable_png_sized(&mut s, 48, 24);
let keys: Vec<String> = s.doc().unwrap().doc.images.keys().cloned().collect();
assert_eq!(keys.len(), 2);
let decode_notes = |r: &Value| warnings(r).iter().filter(|w| w.contains("decoded")).count();
let named = |r: &Value| warnings(r).iter().any(|w| keys.iter().any(|k| w.contains(k.as_str())));
let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7"})).unwrap();
assert_eq!((undecoded_notes(&r), decode_notes(&r), named(&r)), (1, 1, false), "{r}");
// A third image, at 50% opacity, is flattened. The engine's note and the writer's note come out
// as one warning.
let cut = place_undecodable_png_sized(&mut s, 32, 16);
s.execute("transparency.set", &json!({"ids": [cut.0], "opacity": 50})).unwrap();
let r = s.execute("file.saveCopy", &json!({"format": "ai", "aiVersion": "7"})).unwrap();
assert!(warnings(&r).iter().any(|w| w.contains("flattened with the art under them")), "{r}");
assert_eq!((undecoded_notes(&r), decode_notes(&r), named(&r)), (1, 1, false), "{r}");
}
#[test]
fn an_image_that_cant_be_decoded_with_an_expanded_effect_is_noted() {
let mut s = Session::new();
+7 -1
View File
@@ -41,8 +41,14 @@ fn sel(s: &mut Session, ids: &[NodeId]) {
/// A placed 64 × 32 px PNG whose header reads (so it places at its size) but whose pixel data is
/// cut short: the renderer can't decode it and draws nothing.
pub(crate) fn place_undecodable_png(s: &mut Session) -> NodeId {
place_undecodable_png_sized(s, 64, 32)
}
/// Places a PNG like [`place_undecodable_png`] does, at `w` × `h` px. PNGs of different sizes are
/// different images, each stored under its own key.
pub(crate) fn place_undecodable_png_sized(s: &mut Session, w: u32, h: u32) -> NodeId {
let mut png = Vec::new();
image::RgbaImage::from_pixel(64, 32, image::Rgba([200, 30, 30, 255]))
image::RgbaImage::from_pixel(w, h, image::Rgba([200, 30, 30, 255]))
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.unwrap();
let idat = png.windows(4).position(|w| w == b"IDAT").unwrap();