Effects: Effect › Rasterize draws an object as pixels while its paths stay editable

The Effect menu had no Rasterize. Object › Rasterize replaces the
object with an image, so its paths can no longer be edited.

Effect › Rasterize… (`rasterize`) is a live effect with Object ›
Rasterize's options: color model, resolution (the document's raster
effects resolution by default), white or transparent background,
anti-aliasing, Create Clipping Mask and Add Around Object. The canvas
draws the object as square pixels over its bounds, and its paths, type
and other art stay as they are. The effects above it in the stack,
shadows included, are part of its image, and shadows below it are cast
by the image. When Rasterize is an object's only raster effect, Expand
Appearance and PDF and SVG export give the image Object › Rasterize
gives with the same options.

Object › Rasterize now measures the art as drawn, with its geometry
effects, brush art and raster effects, so a Transform effect that moves
the art no longer leaves the image empty. Its Create Clipping Mask
clips the image to the art as drawn. The canvas no longer panics
drawing a pixel effect such as Gaussian Blur on an object about 65,500
screen pixels wide: the raster of such an object is at most 32,768
pixels a side, and the object is drawn into it at a lower resolution.

Docs: docs/mcp.md, docs/effects-parity.md, docs/target-app-parity.md,
ROADMAP.
This commit is contained in:
dotCooCoo
2026-10-16 05:24:14 -07:00
parent 8bc6532928
commit fa46998006
39 changed files with 1707 additions and 118 deletions
+5 -4
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; an object whose only raster effect is its own Rasterize becomes the image object.rasterize makes with the effect's options), 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}",
can_expand,
expand_appearance
),
@@ -421,9 +421,10 @@ fn is_container(n: &Node) -> bool {
}
/// The raster effects of `n` (its own, its fills' and strokes') as an embedded image rendered at
/// the document's raster effects resolution, with whether they all paint below it (shadows and
/// outer glows: the image then holds just them). A group's or layer's image leaves out its
/// members' raster effects, which are expanded with them. `None` when it has none.
/// the document's raster effects resolution or a Rasterize effect's finer one, with whether they
/// all paint below it (shadows and outer glows: the image then holds just them). A group's or
/// layer's image leaves out its members' raster effects, which are expanded with them. `None`
/// when it has none.
fn raster_image(d: &mut Document, n: &Node) -> Option<(Node, bool)> {
let fx = rasterfx::raster_fx(n);
if fx.is_empty() {
+75 -32
View File
@@ -425,21 +425,49 @@ fn render_png(
Ok((img.to_png().map_err(EngineError::Other)?, img.width, img.height))
}
fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.rasterize";
let roots = selected_roots(s)?;
let st = s.doc()?;
let ppi = f64_or(p, "ppi", st.doc.raster_effects_ppi);
/// The image Object › Rasterize makes of some art, before it goes in a document
/// ([`rasterize_art`]).
pub(crate) struct Rasterized {
png: Vec<u8>,
width: u32,
height: u32,
/// Image pixels → document.
xf: Affine,
/// The art's outline, with Create Clipping Mask.
clip: Option<(PathData, vectorcraft_geom::FillRule)>,
}
impl Rasterized {
/// The image as a new object of `d` (its pixels in `d.images`), in a clip group with the art's
/// outline when it has one.
pub(crate) fn into_node(self, d: &mut vectorcraft_doc::Document) -> Node {
let key = unique_key(d, "raster");
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", self.png));
let id = d.alloc_id();
let image = ImageObject { key, width: self.width, height: self.height, xf: self.xf, link: None, placement: Default::default() };
let node = Node::new(id, NodeKind::Image(Box::new(image)));
match self.clip {
Some((path, rule)) => super::rasterfx::clip_group(d, path, rule, node),
None => node,
}
}
}
/// Object › Rasterize's image of `nodes` (drawn also where hidden) with the params `p` of command
/// `cmd`, each defaulting to `doc`'s raster effects settings: over the bounds of what the art draws
/// ([`vectorcraft_render::drawn_bounds`]) and the room around them, in whole pixels.
pub(crate) fn rasterize_art(doc: &vectorcraft_doc::Document, nodes: Vec<Node>, p: &Value, cmd: &str) -> Result<Rasterized> {
let ppi = f64_or(p, "ppi", doc.raster_effects_ppi);
if !(1.0..=2400.0).contains(&ppi) {
return Err(bad(C, "ppi must be between 1 and 2400"));
return Err(bad(cmd, "ppi must be between 1 and 2400"));
}
// The document's raster effects settings, changed by the params.
let mut look = st.doc.raster_effects.clone();
let mut look = doc.raster_effects.clone();
let mut background = (look.background == vectorcraft_doc::Background::White).then_some([255; 3]);
let mut anti_alias = if look.anti_alias { AntiAlias::Art } else { AntiAlias::None };
for (k, v) in p.as_object().into_iter().flatten().filter(|(_, v)| !v.is_null()) {
match k.as_str() {
"background" => background = fileio::background(v).map_err(|e| bad(C, e))?,
"background" => background = fileio::background(v).map_err(|e| bad(cmd, e))?,
"antiAlias" => {
anti_alias = match v {
Value::Bool(b) => {
@@ -449,50 +477,65 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
AntiAlias::None
}
}
v => fileio::anti_alias(v.as_str().unwrap_or_default()).map_err(|e| bad(C, e))?,
v => fileio::anti_alias(v.as_str().unwrap_or_default()).map_err(|e| bad(cmd, e))?,
}
}
"padding" | "addAround" => look.add_around = super::rasterfx::add_around(v, C, k)?,
"colorModel" => look.color_model = super::rasterfx::color_model(v, st.doc.color_mode, C)?,
"clippingMask" => look.clipping_mask = v.as_bool().ok_or_else(|| bad(C, "clippingMask must be true or false"))?,
"padding" | "addAround" => look.add_around = super::rasterfx::add_around(v, cmd, k)?,
"colorModel" => look.color_model = super::rasterfx::color_model(v, doc.color_mode, cmd)?,
"clippingMask" => look.clipping_mask = v.as_bool().ok_or_else(|| bad(cmd, "clippingMask must be true or false"))?,
_ => {}
}
}
let raster = RasterExportOptions { ppi, background, anti_alias, ..Default::default() };
let pad = look.add_around;
let b = st.doc.bounds_of(&roots, true).ok_or_else(|| bad(C, "selection has no bounds"))?.inflate(pad, pad);
let scale = ppi / 72.0;
// Snap to whole pixels.
let region = Rect::new(b.x0, b.y0, b.x0 + (b.width() * scale).ceil().max(1.0) / scale, b.y0 + (b.height() * scale).ceil().max(1.0) / scale);
let nodes: Vec<Node> = roots
.iter()
.filter_map(|id| st.doc.node(*id).cloned())
let nodes: Vec<Node> = nodes
.into_iter()
.map(|mut n| {
n.visible = true;
n
})
.collect();
// Create Clipping Mask: the image is clipped to the art's outline.
// What the art draws, its geometry effects applied.
let b = nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, vectorcraft_render::drawn_bounds(doc, n)));
let b = b.ok_or_else(|| bad(cmd, "selection has no bounds"))?.inflate(pad, pad);
let scale = ppi / 72.0;
// Snap to whole pixels.
let region = Rect::new(b.x0, b.y0, b.x0 + (b.width() * scale).ceil().max(1.0) / scale, b.y0 + (b.height() * scale).ceil().max(1.0) / scale);
// Create Clipping Mask: the image is clipped to the art's outline with its geometry effects
// applied, as on the canvas.
let clip = look
.clipping_mask
.then(|| vectorcraft_render::effects::clip_outline(&Node::group(NodeId(u64::MAX), nodes.iter().cloned().map(Arc::new).collect())))
.then(|| {
let outlines = nodes.iter().filter_map(|n| {
let (bp, rule) = vectorcraft_render::drawn_outline(doc, n)?;
let mut p = Node::path(n.id, PathData::from_bezpath(&bp), Appearance::default());
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
*r = rule;
}
Some(Arc::new(p))
});
vectorcraft_render::effects::clip_outline(&Node::group(NodeId(u64::MAX), outlines.collect()))
})
.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 tmp = isolated_doc(doc, nodes);
let (png, width, height) = render_png(&tmp, region, scale, &raster.render_options(RasterFormat::Png), look.color_model)?;
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
Ok(Rasterized { png, width, height, xf, clip })
}
fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.rasterize";
let roots = selected_roots(s)?;
let st = s.doc()?;
let nodes: Vec<Node> = roots.iter().filter_map(|id| st.doc.node(*id).cloned()).collect();
let image = rasterize_art(&st.doc, nodes, p, C)?;
let (w, h) = (image.width, image.height);
if roots.is_empty() {
return Err(bad(C, "nothing selected"));
}
let id = s.edit("Rasterize", |d, sel| {
let key = unique_key(d, "raster");
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
let id = d.alloc_id();
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
let mut node =
Node::new(id, NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })));
if let Some((path, rule)) = clip {
node = super::rasterfx::clip_group(d, path, rule, node);
}
let node = image.into_node(d);
let id = d.wrap(&roots, |_| node)?;
sel.set([id]);
Ok(id)
+46 -12
View File
@@ -6,11 +6,13 @@
//! model, background, anti-aliasing, room around the art). Shadows and outer glows only add the
//! effect: the image (with the object's own area knocked out) goes under the untouched vector
//! object. Effects that change the object itself (inner glow, feather, Gaussian blur) replace it
//! with the image. Expand Appearance makes its images with the same helpers.
//! with the image. An object whose only raster effect is a Rasterize of its own is replaced with
//! the image Object › Rasterize makes with that effect's options. Expand Appearance makes its
//! images with the same helpers.
//!
//! SVG has filters for shadows, glows, blurs and feathers but none for the Photoshop-style
//! effects (Radial Blur, Unsharp Mask…): SVG export turns only the objects carrying one of those
//! into images the same way ([`flatten_pixel_effects`]).
//! effects (Radial Blur, Unsharp Mask…) or Rasterize: SVG export turns only the objects carrying
//! one of those into images the same way ([`flatten_pixel_effects`]).
use std::sync::Arc;
@@ -273,11 +275,15 @@ fn below_images(d: &mut Document, n: &Node) -> Option<Vec<Node>> {
}
/// The raster effects of `n` (its own, its fills' and strokes') as an embedded image of `d`
/// rendered at the document's raster effects resolution, without `n`'s transparency (that stays
/// on the object). With `below` (they all paint below it: shadows and outer glows) the image holds
/// just them; with `members` too, a group's or layer's image leaves out its members' raster
/// effects (they get images of their own).
/// rendered at the document's raster effects resolution, or at a Rasterize effect's finer one (an
/// object whose only raster effect is its own Rasterize gets [`rasterize_effect_image`]'s image),
/// without `n`'s transparency (that stays on the object). With `below` (they all paint below it:
/// shadows and outer glows) the image holds just them; with `members` too, a group's or layer's
/// image leaves out its members' raster effects (they get images of their own).
pub(crate) fn raster_image(d: &mut Document, n: &Node, below: bool, members: bool) -> Option<Node> {
if !below && let Some(image) = rasterize_effect_image(d, n) {
return Some(image);
}
let mut whole = n.clone();
(whole.opacity, whole.blend) = (1.0, Default::default());
whole.extras_mut().mask = None;
@@ -291,10 +297,37 @@ pub(crate) fn raster_image(d: &mut Document, n: &Node, below: bool, members: boo
strip_raster(&mut b, false);
b
});
let scale = effects_scale(d);
// A Rasterize effect at a finer resolution than the document's is rendered at its own.
let scale = raster_fx(n)
.iter()
.filter_map(|fx| if let RasterFx::Rasterize(r) = fx { r.ppi } else { None })
.fold(effects_scale(d), |s, ppi| s.max(ppi / 72.0));
effect_image(d, &whole, bare.as_ref(), scale)
}
/// The image Object › Rasterize makes of `n` with the options of `n`'s Effect › Rasterize, when
/// that is its only visible raster effect and is its own (not a fill's or a stroke's): made of `n`
/// without that effect and without its transparency, which stays on the object. `None` otherwise,
/// or when the image can't be made.
fn rasterize_effect_image(d: &mut Document, n: &Node) -> Option<Node> {
let [RasterFx::Rasterize(r)] = raster_fx(n)[..] else { return None };
let at = n.appearance.effects.iter().position(|e| e.visible && e.id == effects::RASTERIZE)?;
let p = json!({
"ppi": r.ppi.unwrap_or(d.raster_effects_ppi),
"colorModel": r.color_model.id(),
"background": if r.white { "white" } else { "transparent" },
"antiAlias": if !r.anti_alias { "none" } else if r.hinted { "type" } else { "art" },
"clippingMask": r.clipping_mask,
"addAround": r.add_around,
});
let mut whole = n.clone();
whole.appearance.effects.remove(at);
(whole.opacity, whole.blend) = (1.0, Default::default());
whole.extras_mut().mask = None;
let image = super::menucmds::rasterize_art(d, vec![whole], &p, "object.rasterize").ok()?;
Some(image.into_node(d))
}
/// `v` (an object without its raster effects) replaced by `image` of the object with them: the
/// image takes its place with its id, name and transparency (a layer keeps the image as its only
/// member).
@@ -393,7 +426,8 @@ fn placed_symbol(d: &Document, n: &Node) -> Option<Node> {
enum Which {
/// Every raster effect (PDF).
All,
/// Objects with a Photoshop-style effect, which SVG can't draw (with all their raster effects).
/// Objects with a Photoshop-style effect or Rasterize, which SVG can't draw (with all their
/// raster effects).
Pixel,
}
@@ -402,7 +436,7 @@ fn imaged(n: &Node, which: Which) -> bool {
let fx = raster_fx(n);
match which {
Which::All => !fx.is_empty(),
Which::Pixel => fx.iter().any(|f| matches!(f, RasterFx::Pixel(_))),
Which::Pixel => fx.iter().any(|f| matches!(f, RasterFx::Pixel(_) | RasterFx::Rasterize(_))),
}
}
@@ -499,8 +533,8 @@ pub fn flatten_raster_effects(doc: &Document) -> Option<Document> {
}
/// A copy of `doc` in which the objects with Photoshop-style raster effects (Radial Blur, Unsharp
/// Mask…) are images as [`flatten_raster_effects`] makes them (SVG export: its filters draw the
/// other raster effects live), or `None` when there are none.
/// Mask…) or Rasterize are images as [`flatten_raster_effects`] makes them (SVG export: its
/// filters draw the other raster effects live), or `None` when there are none.
pub fn flatten_pixel_effects(doc: &Document) -> Option<Document> {
flatten(doc, &mut FlattenReport::new(Which::Pixel, doc))
}
+152 -1
View File
@@ -1,5 +1,6 @@
//! Effect → Document Raster Effects Settings (`document.rasterEffectsSettings`): the settings, the
//! raster effect images they make (PDF export, Expand Appearance) and the Rasterize defaults.
//! raster effect images they make (PDF export, Expand Appearance), the Rasterize defaults, and
//! Effect › Rasterize's image.
use serde_json::{Value, json};
use vectorcraft_doc::{Background, Document, NodeId, NodeKind, RasterColorModel, RasterEffectsSettings};
@@ -144,6 +145,156 @@ fn rasterize_takes_the_document_settings_as_defaults() {
assert!(s.execute("object.rasterize", &json!({"colorModel": "lab"})).is_err());
}
/// An image Object › Rasterize or Expand Appearance made, `n` or the image in its clip group:
/// (width, height, where its pixels lie, its PNG, the clipping path's geometry).
fn rasterized(d: &Document, n: &vectorcraft_doc::Node) -> (u32, u32, vectorcraft_geom::Affine, Vec<u8>, Option<vectorcraft_geom::PathData>) {
let (image, clip) = match &n.kind {
NodeKind::Group { children, clip: true } => (&*children[1], children[0].path_data().cloned()),
_ => (n, None),
};
let NodeKind::Image(im) = &image.kind else { panic!("an image: {:?}", image.kind_label()) };
(im.width, im.height, im.xf, d.images[&im.key].bytes.to_vec(), clip)
}
/// A rectangle of 40 × 30 pt with its default stroke and a red fill, selected.
fn red_rectangle(s: &mut Session) -> NodeId {
let r = id(&s.execute("shape.rectangle", &json!({"x": 50.5, "y": 50.25, "width": 40, "height": 30})).unwrap());
s.execute("paint.setFill", &json!({"color": "#ff0000", "ids": [r.0]})).unwrap();
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
r
}
#[test]
fn the_rasterize_effect_keeps_the_path_and_expands_to_the_image_object_rasterize_makes() {
let cases = [
(
json!({"useDocumentResolution": false, "resolution": 150, "background": "white", "colorModel": "grayscale", "antiAlias": "none", "addAround": 3}),
json!({"ppi": 150, "background": "white", "colorModel": "grayscale", "antiAlias": "none", "addAround": 3}),
),
// At the document's resolution (300 ppi here), clipped to the art's outline.
(
json!({"clippingMask": true, "antiAlias": "type"}),
json!({"ppi": 300, "clippingMask": true, "antiAlias": "type", "background": "transparent"}),
),
];
for (effect, object) in cases {
let mut s = session();
s.execute(C, &json!({"resolution": "high"})).unwrap();
let r = red_rectangle(&mut s);
let before = s.doc().unwrap().doc.node(r).unwrap().path_data().cloned();
s.execute("effect.apply", &json!({"effect": "rasterize", "params": effect, "ids": [r.0]})).unwrap();
let d = s.doc().unwrap().doc.clone();
let n = d.node(r).unwrap();
assert_eq!((n.path_data().cloned(), n.appearance.effects[0].id.as_str()), (before, "rasterize"), "the path stays as it was");
// Saved and read back.
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap();
assert_eq!(back.node(r).unwrap().appearance.effects, n.appearance.effects);
// Object › Rasterize of the same rectangle.
let mut o = session();
o.execute(C, &json!({"resolution": "high"})).unwrap();
red_rectangle(&mut o);
let made = id(&o.execute("object.rasterize", &object).unwrap());
let od = &o.doc().unwrap().doc;
let want = rasterized(od, od.node(made).unwrap());
// PDF export draws that image in its place.
let flat = crate::flatten_raster_effects(&d).unwrap();
assert_eq!(rasterized(&flat, &flat.layers[0].children().unwrap()[0]), want, "{effect}");
// So does Expand Appearance.
s.execute("effect.expandAppearance", &json!({"ids": [r.0]})).unwrap();
let d = &s.doc().unwrap().doc;
assert_eq!(rasterized(d, d.node(r).unwrap()), want, "{effect}");
}
}
#[test]
fn the_rasterize_effect_follows_edits_to_the_path() {
let mut s = session();
let r = red_rectangle(&mut s);
s.execute("effect.apply", &json!({"effect": "rasterize", "params": {"background": "white"}, "ids": [r.0]})).unwrap();
let width = |s: &Session| {
let flat = crate::flatten_raster_effects(&s.doc().unwrap().doc).unwrap();
rasterized(&flat, &flat.layers[0].children().unwrap()[0]).0
};
let before = width(&s);
// Direct Selection moves the right-hand anchors 20 pt: the image widens with the path.
s.execute("select.anchors", &json!({"id": r.0, "anchors": [[0, 1], [0, 2]]})).unwrap();
s.execute("path.moveAnchors", &json!({"dx": 20, "dy": 0})).unwrap();
let n = s.doc().unwrap().doc.node(r).unwrap().clone();
assert!(matches!(n.kind, NodeKind::Path { .. }) && n.appearance.effects.len() == 1);
assert_eq!(width(&s), before + 20, "72 ppi: a pixel a point");
}
#[test]
fn rasterize_measures_the_art_its_geometry_effects_draw() {
let mut s = session();
let r = red_rectangle(&mut s);
s.execute("effect.apply", &json!({"effect": "distort.transform", "params": {"moveH": 100}, "ids": [r.0]})).unwrap();
s.execute("effect.apply", &json!({"effect": "rasterize", "params": {"background": "white"}, "ids": [r.0]})).unwrap();
s.execute("effect.expandAppearance", &json!({"ids": [r.0]})).unwrap();
let d = &s.doc().unwrap().doc;
let (width, _, xf, ..) = rasterized(d, d.node(r).unwrap());
// The rectangle as the Transform effect draws it: 100 pt to the right, 41 pt wide with its stroke.
assert_eq!((xf.translation().x, width), (150.0, 41));
}
#[test]
fn the_clipping_mask_follows_the_art_its_geometry_effects_draw() {
let clip = |s: &Session, n: NodeId| match &s.doc().unwrap().doc.node(n).unwrap().kind {
NodeKind::Group { children, clip: true } => children[0].geometric_bounds(),
_ => None,
};
let moved = |s: &mut Session, n: NodeId| {
s.execute("effect.apply", &json!({"effect": "distort.transform", "params": {"moveH": 100}, "ids": [n.0]})).unwrap();
};
// Expand Appearance clips the image to the rectangle drawn 100 pt to the right, from 150.5 to
// 190.5 pt.
let mut s = session();
let r = red_rectangle(&mut s);
moved(&mut s, r);
s.execute("effect.apply", &json!({"effect": "rasterize", "params": {"clippingMask": true}, "ids": [r.0]})).unwrap();
s.execute("effect.expandAppearance", &json!({"ids": [r.0]})).unwrap();
let b = clip(&s, r).expect("a clip group");
assert!((b.x0 - 150.5).abs() < 1e-6 && (b.x1 - 190.5).abs() < 1e-6, "{b:?}");
// Object › Rasterize clips its image there too.
let mut s = session();
let r = red_rectangle(&mut s);
moved(&mut s, r);
let made = id(&s.execute("object.rasterize", &json!({"clippingMask": true})).unwrap());
let b = clip(&s, made).expect("a clip group");
assert!((b.x0 - 150.5).abs() < 1e-6 && (b.x1 - 190.5).abs() < 1e-6, "{b:?}");
// Type's image is clipped to its glyphs, 100 pt right of where they are without the effect.
let glyphs = |effect: bool| {
let mut s = session();
let t = id(&s.execute("text.create", &json!({"x": 50, "y": 100, "text": "H", "size": 40})).unwrap());
if effect {
moved(&mut s, t);
}
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
let made = id(&s.execute("object.rasterize", &json!({"clippingMask": true})).unwrap());
clip(&s, made).expect("a clip group")
};
let (plain, shifted) = (glyphs(false), glyphs(true));
assert!((shifted.x0 - plain.x0 - 100.0).abs() < 1e-6 && (shifted.x1 - plain.x1 - 100.0).abs() < 1e-6, "{plain:?} {shifted:?}");
}
#[test]
fn object_rasterize_takes_in_everything_an_effect_stack_paints() {
let mut s = session();
let r = red_rectangle(&mut s);
s.execute("effect.apply", &json!({"effect": "blur.gaussian", "params": {"radius": 4}, "ids": [r.0]})).unwrap();
let live = json!({"useDocumentResolution": false, "resolution": 72, "background": "white", "addAround": 10});
s.execute("effect.apply", &json!({"effect": "rasterize", "params": live, "ids": [r.0]})).unwrap();
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
let made = id(&s.execute("object.rasterize", &json!({"ppi": 72, "background": "transparent"})).unwrap());
let d = &s.doc().unwrap().doc;
let (width, height, xf, png, _) = rasterized(d, d.node(made).unwrap());
let img = image::load_from_memory(&png).unwrap().to_rgba8();
// 41 pt of rectangle and stroke, and 6 pt of blur and 10 pt of white on each side: white from
// the first column to the last.
assert_eq!((width, xf.translation().x), (73, 34.0));
assert_eq!((img.get_pixel(0, height / 2).0, img.get_pixel(width - 1, height / 2).0), ([255; 4], [255; 4]));
}
#[test]
fn raster_settings_round_trip_and_old_files_load() {
let mut d = Document::new(10.0, 10.0);