Rasterize, Flatten Transparency: commands that replace art with a rendering warn about an undecodable image

Object > Rasterize, Crop Image, Flatten Transparency, Expand
Appearance of raster effects and Vector Halftone draw the art with the
renderer and replace it with the result. When the art held an image
that could not be decoded, the renderer drew nothing for it, and the
result lacked the image with no message. A raster export of the same
art warns that "an image couldn't be decoded and is missing from the
picture".

These five commands now compare decode_failures before and after they
draw, as raster exports do, and their replies carry that text as
`warning`. They still make their result in one undo step. The app
shows a command's `warning` in the status bar: after a menu command,
after OK in a dialog with a preview, and after a tool runs a command
(Crop Image's Apply).

Docs: docs/mcp.md (Flatten Transparency, Expand Appearance, Document
Raster Effects Settings, Vector halftone).
This commit is contained in:
dotCooCoo
2026-10-17 16:38:42 +07:00
committed by bflatastic
parent 8ee5f96da5
commit 133fd73feb
16 changed files with 215 additions and 23 deletions
+8 -2
View File
@@ -59,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Expand Appearance",
["Object"],
None,
"{ids?|id?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (strokes outlined, brushed strokes their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it), type with effects or fills of its own is outlined; Crop Marks become a group of the object and its marks; a group's or layer's own fills and strokes become objects among its members, which are expanded too. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids}",
"{ids?|id?: [..], target?} turn each object's appearance into objects: every fill and stroke becomes an object of its own (strokes outlined, brushed strokes their brush art) grouped in paint order under the object's id and transparency, geometry effects are baked, raster effects become an embedded image (shadows and outer glows below the art; blur, feather and inner glow replace it), type with effects or fills of its own is outlined; Crop Marks become a group of the object and its marks; a group's or layer's own fills and strokes become objects among its members, which are expanded too. Hidden fills, strokes and effects are dropped. Enabled when a selected or targeted object's appearance isn't basic (`ids` then pick which objects) → {ids, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
can_expand,
expand_appearance
),
@@ -551,6 +551,7 @@ pub(crate) fn expand_node(d: &mut Document, id: NodeId, brushes: &[vectorcraft_b
fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
let roots = appearance_targets(s, p)?;
let failed = vectorcraft_render::decode_failures();
let ids = s.edit("Expand Appearance", |d, _| {
let brushes = vectorcraft_brush::library(d);
let mut out = vec![];
@@ -562,7 +563,12 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(out)
})?;
Ok(json!({ "ids": ids_json(&ids) }))
let mut out = json!({ "ids": ids_json(&ids) });
// Raster effects become images rendered from the art.
if let Some(w) = rasterfx::undecoded_warning(failed) {
out["warning"] = json!(w);
}
Ok(out)
}
#[cfg(test)]
+4 -2
View File
@@ -417,8 +417,10 @@ fn encode_all_of(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
vectorcraft_doc::cancel::check()?;
// An image the renderer couldn't decode draws nothing: say so rather than write a picture
// that silently lacks it.
if f.raster && vectorcraft_render::decode_failures() > failed {
enc.warnings.push("an image couldn't be decoded and is missing from the picture".into());
if f.raster
&& let Some(w) = crate::cmd::rasterfx::undecoded_warning(failed)
{
enc.warnings.push(w.into());
}
// Formats that draw type: missing fonts come out in the fallback font (PDF and SVG say so
// themselves: SVG only when it outlines or embeds fonts).
+8 -2
View File
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Flatten Transparency…",
["Object"],
None,
"{preset?: \"high\"|\"medium\"|\"low\" or a saved preset's name (see flattener.presets.list; default medium), balance?: 0..100 (raster/vector balance; 0 rasterizes everything), lineArtPpi?: 1..2400, gradientPpi?: 1..2400 (areas only gradients and meshes reach), textToOutlines?, strokesToOutlines?, clipComplexRegions? (clip images to the region outlines, else rectangles), antiAlias?, preserveAlpha? (composite over nothing instead of white), preserveOverprints? (areas showing one paint keep its colour and overprint; false clears overprints), options?: {the same keys}, ids?|id?} overlapping transparent objects become one group of flat-colour regions (CMYK colours composited ink by ink in CMYK documents), plus an image where gradients, patterns, images, masks or raster effects reach; objects without transparency stay → {ids, rasterized: images made, vector: regions made, options}",
"{preset?: \"high\"|\"medium\"|\"low\" or a saved preset's name (see flattener.presets.list; default medium), balance?: 0..100 (raster/vector balance; 0 rasterizes everything), lineArtPpi?: 1..2400, gradientPpi?: 1..2400 (areas only gradients and meshes reach), textToOutlines?, strokesToOutlines?, clipComplexRegions? (clip images to the region outlines, else rectangles), antiAlias?, preserveAlpha? (composite over nothing instead of white), preserveOverprints? (areas showing one paint keep its colour and overprint; false clears overprints), options?: {the same keys}, ids?|id?} overlapping transparent objects become one group of flat-colour regions (CMYK colours composited ink by ink in CMYK documents), plus an image where gradients, patterns, images, masks or raster effects reach; objects without transparency stay → {ids, rasterized: images made, vector: regions made, options, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
has_doc,
flatten
),
@@ -259,13 +259,19 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
return Err(EngineError::Other("Flatten Transparency: select objects to flatten".into()));
}
let src = s.doc()?.doc.clone();
let failed = vectorcraft_render::decode_failures();
let plan = plan(&src, &roots, &o, true)?;
let warning = super::rasterfx::undecoded_warning(failed);
let options = serde_json::to_value(&o).unwrap_or_default();
if plan.flat.is_empty() && !plan.kept.iter().any(|id| src.node(*id).is_some_and(|n| kept_work(n, &o))) {
return Ok(json!({ "ids": roots.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": 0, "vector": 0, "options": options }));
}
let (ids, vector, rasterized) = s.edit("Flatten Transparency", |d, sel| apply(d, sel, plan, &o))?;
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": rasterized, "vector": vector, "options": options }))
let mut out = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": rasterized, "vector": vector, "options": options });
if let Some(w) = warning {
out["warning"] = json!(w);
}
Ok(out)
}
// ---------- presets ----------
+8 -2
View File
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Vector Halftone…",
["Object"],
None,
"{shape?: \"circle\"|\"ellipse\"|\"square\"|\"diamond\"|\"line\" (circle), frequency?: lines per inch 1–300 (20), angle?: deg (45; CMYK: the black screen's, cyan −30°, magenta +30°, yellow −45° from it), mode?: \"mono\"|\"cmyk\" (mono), color?: mono dot colour (\"#000000\"), invert?: bool (dots for light tones), clip?: true (clip the dots to the art's outline), keepOriginal?: false} the selection as vector halftone dots sized by its tone, a group of one path per screen (CMYK screens multiply), as one undo step → {id, dots}",
"{shape?: \"circle\"|\"ellipse\"|\"square\"|\"diamond\"|\"line\" (circle), frequency?: lines per inch 1–300 (20), angle?: deg (45; CMYK: the black screen's, cyan −30°, magenta +30°, yellow −45° from it), mode?: \"mono\"|\"cmyk\" (mono), color?: mono dot colour (\"#000000\"), invert?: bool (dots for light tones), clip?: true (clip the dots to the art's outline), keepOriginal?: false} the selection as vector halftone dots sized by its tone, a group of one path per screen (CMYK screens multiply), as one undo step → {id, dots, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
has_selection,
halftone
)]
@@ -185,11 +185,13 @@ fn halftone(s: &mut Session, p: &Value) -> Result<Value> {
let mut r = vectorcraft_render::Renderer::new();
let (w, h) = vectorcraft_render::region_pixels(region, scale);
let (w, h) = (w as usize, h as usize);
let failed = vectorcraft_render::decode_failures();
let px = if mono {
r.render_region(&tmp, region, scale, true).pixels
} else {
r.render_region_inks(&tmp, region, scale, &vectorcraft_render::RenderOptions { skip_templates: true, ..Default::default() })
};
let warning = super::rasterfx::undecoded_warning(failed);
if px.len() < w * h * 4 {
return Err(EngineError::Other("the art could not be sampled".into()));
}
@@ -241,7 +243,11 @@ fn halftone(s: &mut Session, p: &Value) -> Result<Value> {
sel.set([gid]);
Ok(gid)
})?;
Ok(json!({ "id": id.0, "dots": dots }))
let mut out = json!({ "id": id.0, "dots": dots });
if let Some(w) = warning {
out["warning"] = json!(w);
}
Ok(out)
}
/// The dots of one screen over `bounds` as one path, with their count.
+21 -9
View File
@@ -94,7 +94,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Rasterize…",
["Object"],
None,
"{ppi?, background?: transparent|white|black|\"#rrggbb\", antiAlias?: none|art|type (text snapped to pixels)|bool, padding?|addAround?: pt (0–1000), colorModel?: \"rgb\"|\"cmyk\"|\"grayscale\"|\"bitmap\", clippingMask?: bool (a clip group with the art's outline)} replace the selection with an embedded PNG image; each defaults to document.rasterEffectsSettings → {id (the image, or its clip group), width, height}",
"{ppi?, background?: transparent|white|black|\"#rrggbb\", antiAlias?: none|art|type (text snapped to pixels)|bool, padding?|addAround?: pt (0–1000), colorModel?: \"rgb\"|\"cmyk\"|\"grayscale\"|\"bitmap\", clippingMask?: bool (a clip group with the art's outline)} replace the selection with an embedded PNG image; each defaults to document.rasterEffectsSettings → {id (the image, or its clip group), width, height, warning?: \"an image couldn't be decoded and is missing from the picture\" (as raster exports say)}",
has_selection,
rasterize
),
@@ -112,7 +112,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Crop Image",
["Object"],
None,
"{rect?: [x, y, width, height] | trim?: true (to the pixels that aren't fully transparent, pixel for pixel; an image with none to cut is left as it is, trimmed: false)} crop the selected image (default: to the artboard it sits on) → {id, width, height, trimmed? (with trim)}",
"{rect?: [x, y, width, height] | trim?: true (to the pixels that aren't fully transparent, pixel for pixel; an image with none to cut is left as it is, trimmed: false)} crop the selected image (default: to the artboard it sits on) → {id, width, height, trimmed? (with trim), warning? (without trim: the image couldn't be decoded, as object.rasterize says)}",
has_image,
crop_image
),
@@ -422,21 +422,24 @@ pub(crate) fn unique_key(d: &vectorcraft_doc::Document, stem: &str) -> String {
}
}
/// `doc` rendered over `region` with `opts` as a PNG in colour model `color`.
/// `doc` rendered over `region` with `opts` as a PNG in colour model `color`, with the note that an
/// image in it couldn't be decoded ([`super::rasterfx::undecoded_warning`]).
fn render_png(
doc: &vectorcraft_doc::Document,
region: Rect,
scale: f64,
opts: &vectorcraft_render::RenderOptions,
color: RasterColorModel,
) -> Result<(Vec<u8>, u32, u32)> {
) -> Result<(Vec<u8>, u32, u32, Option<&'static str>)> {
if region.width() * region.height() * scale * scale > MAX_PIXELS || region.width() * scale > 65535.0 || region.height() * scale > 65535.0 {
return Err(EngineError::Other("image would be too large; lower the resolution".into()));
}
let mut r = vectorcraft_render::Renderer::new();
let failed = vectorcraft_render::decode_failures();
let mut img = r.render_region_with(doc, region, scale, opts);
let warning = super::rasterfx::undecoded_warning(failed);
RasterEffectsSettings { color_model: color, ..Default::default() }.finish_pixels(&mut img.pixels);
Ok((img.to_png().map_err(EngineError::Other)?, img.width, img.height))
Ok((img.to_png().map_err(EngineError::Other)?, img.width, img.height, warning))
}
fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
@@ -493,7 +496,7 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
.flatten()
.map(|(bp, rule)| (PathData::from_bezpath(&bp), rule));
let tmp = isolated_doc(&st.doc, nodes);
let (png, w, h) = render_png(&tmp, region, scale, &raster.render_options(RasterFormat::Png), look.color_model)?;
let (png, w, h, warning) = render_png(&tmp, region, scale, &raster.render_options(RasterFormat::Png), look.color_model)?;
if roots.is_empty() {
return Err(bad(C, "nothing selected"));
}
@@ -511,7 +514,11 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
sel.set([id]);
Ok(id)
})?;
Ok(json!({ "id": id.0, "width": w, "height": h }))
let mut out = json!({ "id": id.0, "width": w, "height": h });
if let Some(w) = warning {
out["warning"] = json!(w);
}
Ok(out)
}
fn has_image(s: &Session) -> std::result::Result<(), String> {
@@ -648,7 +655,8 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
bare.visible = true;
let tmp = isolated_doc(&st.doc, vec![bare]);
let region = Rect::new(r.x0, r.y0, r.x0 + (r.width() * scale).round().max(1.0) / scale, r.y0 + (r.height() * scale).round().max(1.0) / scale);
let (png, w, h) = render_png(&tmp, region, scale, &RasterExportOptions::default().render_options(RasterFormat::Png), RasterColorModel::Document)?;
let (png, w, h, warning) =
render_png(&tmp, region, scale, &RasterExportOptions::default().render_options(RasterFormat::Png), RasterColorModel::Document)?;
s.edit("Crop Image", |d, _| {
let key = unique_key(d, "crop");
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
@@ -664,7 +672,11 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(())
})?;
Ok(json!({ "id": id.0, "width": w, "height": h }))
let mut out = json!({ "id": id.0, "width": w, "height": h });
if let Some(w) = warning {
out["warning"] = json!(w);
}
Ok(out)
}
/// Crop Image's trim: the image cut to the box of its pixels that aren't fully transparent, the
+6
View File
@@ -159,6 +159,12 @@ fn render(doc: &Document, nodes: Vec<Node>, region: Rect, scale: f64, anti_alias
r.render_region_with(&tmp, region, scale, &RenderOptions { skip_templates: true, anti_alias, supersample: true, ..Default::default() })
}
/// The note for pixels drawn since [`vectorcraft_render::decode_failures`] stood at `failed`, when
/// an image among them couldn't be decoded and so drew nothing; `None` otherwise.
pub(crate) fn undecoded_warning(failed: u64) -> Option<&'static str> {
(vectorcraft_render::decode_failures() > failed).then_some("an image couldn't be decoded and is missing from the picture")
}
/// Pixels per point of raster effects rendered as images (the document's raster effects
/// resolution).
pub(crate) fn effects_scale(doc: &Document) -> f64 {
+11
View File
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
use vectorcraft_color::BlendMode;
use vectorcraft_doc::{Node, NodeKind};
use super::tests_menucmds::{place_undecodable_png, warns_undecoded};
use super::*;
fn session() -> Session {
@@ -117,6 +118,16 @@ fn a_blur_turns_the_object_into_an_image() {
assert!(n.appearance.effects.is_empty());
}
#[test]
fn expanding_a_blur_says_when_an_image_could_not_be_decoded() {
let mut s = session();
let cut = place_undecodable_png(&mut s);
run(&mut s, "effect.apply", json!({"effect": "blur.gaussian", "params": {"radius": 3}, "ids": [cut.0]}));
let r = run(&mut s, "effect.expandAppearance", json!({"ids": [cut.0]}));
assert!(warns_undecoded(&r), "{r}");
assert!(matches!(node(&s, cut).kind, NodeKind::Image(_)));
}
#[test]
fn an_art_brush_becomes_a_group_of_its_art() {
let mut s = session();
+13
View File
@@ -7,6 +7,7 @@ use vectorcraft_doc::{AppearanceItem, Node, NodeKind};
use vectorcraft_geom::{BezPath, Point, Rect, Shape};
use vectorcraft_render::{Rendered, Renderer};
use super::tests_menucmds::{place_undecodable_png, warns_undecoded};
use super::*;
/// Colours here go through the process-wide colour settings, which the colour management tests
@@ -292,6 +293,18 @@ fn gradients_rasterize_and_flat_areas_stay_vector() {
}
}
#[test]
fn flatten_says_when_an_image_could_not_be_decoded() {
let _settings = settings_lock();
let mut s = session();
let cut = place_undecodable_png(&mut s).0;
let t = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#20c040");
run(&mut s, "transparency.set", json!({"ids": [t], "opacity": 50}));
let r = run(&mut s, "object.flattenTransparency", json!({"ids": [cut, t]}));
assert!(warns_undecoded(&r), "{r}");
assert_eq!(r["rasterized"], 1, "the image area is still flattened: {r}");
}
#[test]
fn flatten_is_one_undo_step() {
let _settings = settings_lock();
+14
View File
@@ -4,6 +4,7 @@ use serde_json::json;
use super::*;
use crate::tests_adjust::{fill_hex, node, red_rect, session};
use crate::tests_menucmds::{place_undecodable_png, warns_undecoded};
#[test]
fn halftone_turns_art_into_dots_by_tone() {
@@ -26,6 +27,19 @@ fn halftone_turns_art_into_dots_by_tone() {
assert!(b.x0 >= 19.9 && b.y0 >= 19.9 && b.x1 <= 80.1 && b.y1 <= 80.1, "{b:?}");
}
#[test]
fn halftone_says_when_an_image_could_not_be_decoded() {
for mode in ["mono", "cmyk"] {
let mut s = session();
let id = red_rect(&mut s);
s.execute("paint.setFill", &json!({"color": "#000000"})).unwrap();
let cut = place_undecodable_png(&mut s);
s.execute("select.set", &json!({"ids": [id.0, cut.0]})).unwrap();
let r = s.execute("object.vectorHalftone", &json!({"frequency": 18, "mode": mode})).unwrap();
assert!(warns_undecoded(&r), "{mode}: {r}");
}
}
#[test]
fn halftone_options() {
let mut s = session();
+41
View File
@@ -38,6 +38,24 @@ fn sel(s: &mut Session, ids: &[NodeId]) {
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
}
/// 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 {
let mut png = Vec::new();
image::RgbaImage::from_pixel(64, 32, 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();
png.truncate(idat + 8);
let r = s.execute("file.place", &json!({"name": "cut.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
NodeId(r["ids"][0].as_u64().unwrap())
}
/// Whether reply `r` warns that an image couldn't be decoded, as raster exports do.
pub(crate) fn warns_undecoded(r: &Value) -> bool {
r["warning"].as_str() == Some("an image couldn't be decoded and is missing from the picture")
}
fn fill(s: &mut Session, id: NodeId, hex: &str) {
s.execute("paint.setFill", &json!({"ids": [id.0], "color": hex})).unwrap();
}
@@ -553,6 +571,29 @@ fn rasterize_replaces_selection_with_image() {
assert_eq!(selected(&s), vec![img]);
}
#[test]
fn rasterize_says_when_an_image_could_not_be_decoded() {
let mut s = session();
let a = rect(&mut s, 10.0, 20.0, 40.0, 30.0);
let cut = place_undecodable_png(&mut s);
sel(&mut s, &[a, cut]);
let r = s.execute("object.rasterize", &json!({"ppi": 72})).unwrap();
assert!(warns_undecoded(&r), "{r}");
// The image still replaces the selection.
assert!(matches!(node(&s, id_of(&r)).kind, NodeKind::Image(_)));
assert!(s.doc().unwrap().doc.node(a).is_none() && s.doc().unwrap().doc.node(cut).is_none());
}
#[test]
fn crop_image_says_when_the_image_could_not_be_decoded() {
let mut s = session();
let cut = place_undecodable_png(&mut s);
sel(&mut s, &[cut]);
let b = bounds(&s, cut);
let r = s.execute("object.cropImage", &json!({"rect": [b.x0, b.y0, b.width() / 2.0, b.height()]})).unwrap();
assert!(warns_undecoded(&r), "{r}");
}
#[test]
fn crop_image_to_rect() {
let mut s = session();
+5 -1
View File
@@ -543,7 +543,11 @@ impl Session {
self.cancel_interaction()?
}
Action::Exec(cmd, p) => {
self.execute(&cmd, &p)?;
// A command's warning (Crop Image's when the image couldn't be decoded) goes to the
// status bar too.
if let Some(w) = self.execute(&cmd, &p)?.get("warning").and_then(Value::as_str) {
ui.push(UiRequest::Status(w.to_string()));
}
}
Action::Dialog(k, p) => ui.push(UiRequest::Dialog(k, p)),
Action::SwitchTool(t) => ui.push(UiRequest::SwitchTool(t)),
+26
View File
@@ -308,6 +308,32 @@ mod tests {
assert!(!transparent_left(&app));
}
#[test]
fn ok_says_in_the_status_bar_when_an_image_could_not_be_decoded() {
const WARNING: &str = "an image couldn't be decoded and is missing from the picture";
// A PNG whose header reads (so it places at its size) but whose pixel data is cut short.
let mut png = Vec::new();
image::RgbaImage::from_pixel(64, 32, 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();
png.truncate(idat + 8);
for preview in [true, false] {
let mut app = scene();
let placed = app.run("file.place", json!({"name": "cut.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
// The image is half transparent itself, so Flatten Transparency flattens it wherever it was placed.
app.run("transparency.set", json!({"ids": placed["ids"], "opacity": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
app.run("ui.flattenTransparencyDialog", json!({})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(preview));
frame(&mut app);
assert_eq!(app.session.in_interaction(), preview);
app.status("");
super::super::confirm(&mut app).unwrap();
assert_eq!(app.ui.status, WARNING, "preview {preview}");
}
}
#[test]
fn save_preset_keeps_the_options_and_lists_them() {
let mut app = scene();
+11 -2
View File
@@ -585,11 +585,20 @@ pub(super) fn preview_result(
}
/// OK of a dialog using [`preview`]: keep the previewed `cmd` as one undo step (or run it when no
/// preview runs) and close. On an error the dialog stays open.
/// preview runs) and close; the command's warning shows in the status bar either way. On an error
/// the dialog stays open.
pub(super) fn commit_preview(app: &mut crate::VectorcraftApp, cmd: &str, params: Value) -> Result<Value, String> {
let r = if app.session.in_interaction() {
match app.session.preview(cmd, &params) {
Ok(_) => app.session.commit_interaction().map(|_| Value::Null).map_err(|e| e.to_string()),
Ok(v) => {
let r = app.session.commit_interaction().map(|_| Value::Null).map_err(|e| e.to_string());
if r.is_ok()
&& let Some(w) = v.get("warning").and_then(Value::as_str)
{
app.status(w.to_string());
}
r
}
Err(e) => Err(e.to_string()),
}
} else {
+5
View File
@@ -766,6 +766,11 @@ impl VectorcraftApp {
r.insert(0, font.to_string());
r.truncate(MAX_RECENT_FONTS);
}
// A command's warning (Rasterize's when an image couldn't be decoded) shows in the
// status bar, as a tool preview's does.
if let Some(w) = v.get("warning").and_then(Value::as_str) {
self.ui.status = w.to_string();
}
}
}
r
+24
View File
@@ -675,6 +675,30 @@ fn crop_image_shows_a_box_that_apply_crops_to() {
assert!(!crate::menus::enabled(&app, "ui.cropImage") && app.run("ui.cropImage", json!({})).is_err());
}
/// Crop Image's Apply and Object › Rasterize say in the status bar when the image couldn't be
/// decoded, as raster exports do.
#[test]
fn crop_and_rasterize_say_in_the_status_bar_when_an_image_could_not_be_decoded() {
const WARNING: &str = "an image couldn't be decoded and is missing from the picture";
// A PNG whose header reads (so it places at its size) but whose pixel data is cut short.
let mut cut = png(100, 50, 72.0);
let idat = cut.windows(4).position(|w| w == b"IDAT").unwrap();
cut.truncate(idat + 8);
let mut app = app();
app.run("file.place", json!({"name": "cut.png", "dataBase64": vectorcraft_format::base64_encode(&cut)})).unwrap();
let whole = selected_image(&app).geometric_bounds().unwrap();
app.run("ui.cropImage", json!({})).unwrap();
app.run("tool.setOption", json!({"key": "rect", "value": [whole.x0, whole.y0, whole.width() / 2.0, whole.height()]})).unwrap();
app.status("");
tool_key(&mut app, vectorcraft_tools::ToolKey::Enter);
assert_eq!(app.ui.status, WARNING, "Crop Image's Apply");
// Undo puts the image back as placed; then Rasterize runs on it.
app.run("edit.undo", json!({})).unwrap();
app.status("");
app.run("object.rasterize", json!({"ppi": 72})).unwrap();
assert_eq!(app.ui.status, WARNING, "Rasterize");
}
/// One headless frame of the whole window at `time` → how long the app asked to wait before the
/// next one.
fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64) -> std::time::Duration {
+10 -3
View File
@@ -1482,7 +1482,9 @@ opacity masks or raster effects reach (clipped to those regions with `clipComple
`preset` is `high`, `medium` (the default) or `low`; option keys (`balance` 0–100, `lineArtPpi`, `gradientPpi`,
`textToOutlines`, `strokesToOutlines`, `clipComplexRegions`, `antiAlias`, `preserveAlpha`, `preserveOverprints`)
override it, at the top level or in `options`. `balance: 0` rasterizes everything; lower balances rasterize groups
that split into many regions. The result reports `{ids, vector, rasterized, options}`.
that split into many regions. The result reports `{ids, vector, rasterized, options}`. When an image in the art
can't be decoded, the image that replaces the art does not show it, and the result also carries `warning`: "an image
couldn't be decoded and is missing from the picture", the note raster exports give.
```json
{"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"ids":[12,15],"preset":"high"}}}
@@ -1605,7 +1607,8 @@ document's raster effects resolution (shadows and outer glows under the art; a b
the object with the image). Type with a geometry effect (Outline Object included) or with fills and strokes of its own
is outlined; type that has no fills or strokes of its own and whose only effects are shadows or outer glows stays
type, with the image under it. A group's or layer's own fills and strokes become objects among its members, and its
members are expanded too.
members are expanded too. The result is `{ids}`; when an image in the art can't be decoded, the images made from
raster effects do not show it, and the result also carries the `warning` that Flatten Transparency gives.
```json
{"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{"ids":[12]}}}
@@ -2520,6 +2523,8 @@ in one undo step. PDF export renders the raster effects of every kind of object
images, symbol instances, live objects, and those in opacity masks, symbols and pattern tiles): shadows and outer glows
go in an image under the untouched vector art, other effects replace the object with its image, and a path whose
fills or strokes carry effects is written as one piece per fill and stroke, so only the affected ones become images.
When an image in the art can't be decoded, `object.rasterize` and `object.cropImage` (without `trim`) make their image
without it, and their results carry the `warning` that Flatten Transparency gives.
```json
{"name":"run_command","arguments":{"command":"document.rasterEffectsSettings","params":{"resolution":"high","background":"white","addAround":36}}}
@@ -3612,7 +3617,9 @@ effects keep their colours.
## Vector halftone
`object.vectorHalftone` turns the selection into vector dots, lines or shapes sized by its
tone (mono, or CMYK screens multiplied over each other), clipped to the art's outline by default:
tone (mono, or CMYK screens multiplied over each other), clipped to the art's outline by default. An image in the
art that can't be decoded is sampled as blank (no dots, or full dots with `invert`), and the result carries the
`warning` that Flatten Transparency gives:
```json
{"name":"run_command","arguments":{"command":"object.vectorHalftone","params":{"shape":"circle","frequency":25,"angle":45,"mode":"cmyk"}}}