mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Merge pull request #2409 from dotCooCoo/feat/live-rasterize-effect
Effects: Effect › Rasterize draws an object as pixels while its paths stay editable
This commit is contained in:
@@ -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, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
|
||||
"{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, warning? (an image in the art couldn't be decoded, as object.rasterize says)}",
|
||||
can_expand,
|
||||
expand_appearance
|
||||
),
|
||||
@@ -426,9 +426,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() {
|
||||
|
||||
@@ -446,21 +446,51 @@ fn render_png(
|
||||
Ok((img.to_png().map_err(EngineError::Other)?, img.width, img.height, warning))
|
||||
}
|
||||
|
||||
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)>,
|
||||
/// The note that an image in the art couldn't be decoded ([`render_png`]).
|
||||
warning: Option<&'static str>,
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -470,50 +500,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, warning) = render_png(&tmp, region, scale, &raster.render_options(RasterFormat::Png), look.color_model)?;
|
||||
let tmp = isolated_doc(doc, nodes);
|
||||
let (png, width, height, warning) = 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, warning })
|
||||
}
|
||||
|
||||
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, warning) = (image.width, image.height, image.warning);
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -289,11 +291,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;
|
||||
@@ -307,10 +313,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).
|
||||
@@ -409,7 +442,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,
|
||||
}
|
||||
|
||||
@@ -418,7 +452,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(_))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,8 +553,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))
|
||||
}
|
||||
|
||||
@@ -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::rastersettings::MAX_ADD_AROUND;
|
||||
@@ -199,6 +200,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);
|
||||
|
||||
Reference in New Issue
Block a user