diff --git a/crates/engine/src/cmd/addons/image.rs b/crates/engine/src/cmd/addons/image.rs index f95caf422e..898c28c707 100644 --- a/crates/engine/src/cmd/addons/image.rs +++ b/crates/engine/src/cmd/addons/image.rs @@ -24,7 +24,7 @@ use vectorcraft_geom::Affine; use super::super::fileio::{MAX_RASTER_ALLOC, ppi}; use super::super::menucmds::has_image; -use super::super::paint::{PROCESS_ON_TRANSPARENCY, gray_colorized_with_process}; +use super::super::paint::{PROCESS_ON_TRANSPARENCY, colorized_with_process}; use super::super::{CommandSpec, Result, Session, bad, checked_ids_param, cmd, hex_with_alpha, number_or, switch_or, text_param}; /// Remove Background's command. @@ -173,10 +173,11 @@ fn remove_background(s: &mut Session, p: &Value) -> Result { fn changes(s: &Session, p: &Value, settings: &Settings, mut kept: Option<&mut DecodedImages>) -> Result> { let images = images(s, p, C)?; // The result has an alpha channel, which a grayscale image colorized with a process color - // can't take. + // can't take. The pixels that are cleared are checked, the image's own, not the ones a color + // mask shows. let st = s.doc()?; - for (id, ..) in &images { - if st.doc.node(*id).is_some_and(|n| gray_colorized_with_process(&st.doc, n).is_some()) { + for (id, _, blob) in &images { + if blob.gray_source().is_some() && st.doc.node(*id).is_some_and(|n| colorized_with_process(&st.doc, n)) { return Err(bad(C, PROCESS_ON_TRANSPARENCY)); } } diff --git a/crates/engine/src/cmd/addons/image/mask/tests.rs b/crates/engine/src/cmd/addons/image/mask/tests.rs index fb978235d9..9faef84edb 100644 --- a/crates/engine/src/cmd/addons/image/mask/tests.rs +++ b/crates/engine/src/cmd/addons/image/mask/tests.rs @@ -461,6 +461,32 @@ fn a_gray_image_whose_earlier_mask_shows_color_pixels_is_refused() { assert_eq!((doc(&s), undo_len(&s)), (&before, steps)); } +/// Remove Background clears the image's own pixels: a grayscale image colorized with a process +/// color is refused even when an earlier mask, as an opened file can hold, shows color pixels. +#[test] +fn remove_background_refuses_a_gray_image_whose_earlier_mask_shows_color_pixels() { + let (mut s, id) = open(&gray_framed()); + s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); + let rgb = ImageBlob::png(framed()); + let key = rgb.content_key(); + s.edit("Mask", |d, _| { + d.images.insert(key.clone(), rgb.clone()); + let from = image_of(d, id).key; + let mask = ColorMask { key: key.clone(), from, ..mask_of_settings() }; + d.node_mut(id).unwrap().extras_mut().color_mask = Some(Box::new(mask)); + Ok(()) + }) + .unwrap(); + let d = doc(&s); + let n = d.node(id).unwrap(); + let NodeKind::Image(im) = &n.kind else { panic!("an image") }; + assert_eq!(d.shown_image(n, im).key, key, "the image shows the color pixels"); + let (before, steps) = (doc(&s).clone(), undo_len(&s)); + let e = s.execute(super::super::REMOVE_BACKGROUND, &hide_white()).unwrap_err().to_string(); + assert!(e.contains("a process color can't colorize a grayscale image with transparency"), "{e}"); + assert_eq!((doc(&s), undo_len(&s)), (&before, steps)); +} + /// A mask that hides nothing leaves the image's own pixels without transparency, so a grayscale /// image colorized with a process color takes it. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 524af3a661..bb6cd3b7d9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10296,10 +10296,12 @@ profile and resolution). PDF's color key masking hides chosen colors of an image EPS, the raster formats, DXF, EMF, WMF, the legacy .ai, the .ai editing data, copy and paste, and placing a document show the masked pixels. A linked image stays linked. An output that refers to linked files instead of embedding them (SVG with `images: "link"`) writes the masked pixels in place of the link. -- A grayscale image's masked pixels have an alpha channel. A colorize color on a masked grayscale image follows the rule - for grayscale images with transparency (a spot color, black or white), and Color Mask refuses a mask that hides - pixels of a grayscale image colorized with a process color. Each image is checked on the pixels its new mask shows, - whatever other images in the document hold. +- A grayscale image's masked pixels have an alpha channel when the mask hides pixels. A colorize color on such an image + follows the rule for grayscale images with transparency (a spot color, black or white), and Color Mask refuses a mask + that hides pixels of a grayscale image colorized with a process color. A mask that hides no pixels shows the image's + own pixels, so a grayscale image with such a mask takes a process color. Each image is checked on the pixels its new + mask shows, whatever other images in the document hold. Remove Background checks the image's own pixels, which it + clears. - A click anywhere in the image's frame selects it, hidden pixels included, as it does for an image with clear pixels. The Eyedropper, Object Mosaic, Create Gradient Mesh, Crop Image's trim and Image Trace read the image's own pixels, hidden colors included. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8bd6b79020..1d7f8603ef 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2047,7 +2047,7 @@ A VectorCraft addition, so no area score moves. Object › Plug-ins › Image To ## Image Tools add-on: Color Mask (2026-11-12) -A VectorCraft addition, so no area score moves. Object › Plug-ins › Image Tools › Color Mask… (`addon.image.colorMask`) hides colors of the selected images while their pixels stay as they are. It matches colors as Remove Background does (edge, corner or picked colors, Contiguous, Match, Tolerance and Softness, VectorCraft's own values). The mask's settings and the masked pixels are kept with the image object and saved with the document. The canvas (clip groups and white-shape opacity masks included), printing and separations, PDF (PDF/X-1a and PDF/X-3 flatten the hidden pixels), SVG, EPS, PNG, DXF, EMF, WMF, the legacy .ai, the .ai editing data, copy and paste, and placing a document show the masked pixels. A linked image stays linked, and outputs that would refer to its file write the masked pixels instead. A masked grayscale image takes only a spot, black or white colorize color. The dialog opens on the image's mask settings. Remove Color Mask takes the mask off, and Apply Color Mask makes it part of the image's pixels. Each is one undo step, and none is recorded when nothing changes. When the image's own pixels are replaced (a link updated or read again on open, the image cropped or recolored), the mask is made again for the new pixels. A click anywhere in the image's frame selects it. Tests: `cmd::addons::image::mask::tests` in `vectorcraft-engine` (hidden colors drawn, pixels kept, edit, report, remove and apply, the outputs listed, save and open, copy and paste, placing a document, a linked image, cropped and replaced pixels, colorize colors, refusals), `color_mask::tests` and `placed_document::tests` in `vectorcraft-doc`, the separations and CMYK soft-mask tests in `vectorcraft-pdf`, and `addons::image::tests::color_mask_previews_keeps_its_settings_and_comes_off` in `vectorcraft-ui-egui`. Commands: `docs/mcp.md` › Image Tools add-on › Color Mask. +A VectorCraft addition, so no area score moves. Object › Plug-ins › Image Tools › Color Mask… (`addon.image.colorMask`) hides colors of the selected images while their pixels stay as they are. It matches colors as Remove Background does (edge, corner or picked colors, Contiguous, Match, Tolerance and Softness, VectorCraft's own values). The mask's settings and the masked pixels are kept with the image object and saved with the document. The canvas (clip groups and white-shape opacity masks included), printing and separations, PDF (PDF/X-1a and PDF/X-3 flatten the hidden pixels), SVG, EPS, PNG, DXF, EMF, WMF, the legacy .ai, the .ai editing data, copy and paste, and placing a document show the masked pixels. A linked image stays linked, and outputs that would refer to its file write the masked pixels instead. A masked grayscale image whose mask hides pixels takes only a spot, black or white colorize color; one whose mask hides no pixels takes a process color too. The dialog opens on the image's mask settings. Remove Color Mask takes the mask off, and Apply Color Mask makes it part of the image's pixels. Each is one undo step, and none is recorded when nothing changes. When the image's own pixels are replaced (a link updated or read again on open, the image cropped or recolored), the mask is made again for the new pixels. A click anywhere in the image's frame selects it. Tests: `cmd::addons::image::mask::tests` in `vectorcraft-engine` (hidden colors drawn, pixels kept, edit, report, remove and apply, the outputs listed, save and open, copy and paste, placing a document, a linked image, cropped and replaced pixels, colorize colors, refusals), `color_mask::tests` and `placed_document::tests` in `vectorcraft-doc`, the separations and CMYK soft-mask tests in `vectorcraft-pdf`, and `addons::image::tests::color_mask_previews_keeps_its_settings_and_comes_off` in `vectorcraft-ui-egui`. Commands: `docs/mcp.md` › Image Tools add-on › Color Mask. ## Color Effects add-on: Find Nearest Swatch (2026-11-01)