mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
This commit is contained in:
@@ -421,8 +421,15 @@ fn import_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let img = fileio::raster_image(&data_param(p, C)?).map_err(|e| bad(C, e.to_string()))?;
|
||||
let (sx, sy) = pt_per_px(img.ppi);
|
||||
let (w, h) = (img.width, img.height);
|
||||
let image =
|
||||
ImageObject { key: img.key.clone(), width: w, height: h, xf: Affine::scale_non_uniform(sx, sy), link: None, placement: Default::default() };
|
||||
let image = ImageObject {
|
||||
key: img.key.clone(),
|
||||
width: w,
|
||||
height: h,
|
||||
xf: Affine::scale_non_uniform(sx, sy),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut clip = Clipboard { nodes: vec![Node::new(NodeId(1), NodeKind::Image(Box::new(image)))], ..Default::default() };
|
||||
clip.images.insert(img.key, img.blob);
|
||||
let count = s.load_clipboard(clip, p)?;
|
||||
|
||||
@@ -558,10 +558,12 @@ pub(crate) fn expand_node(d: &mut Document, id: NodeId, brushes: &[vectorcraft_b
|
||||
let mut listed = vec![];
|
||||
let m = expanded_node(d, n, brushes, &mut listed);
|
||||
out.extend(listed.into_iter().map(|(id, _)| id));
|
||||
if let Some(m) = m
|
||||
&& let Some(slot) = d.node_mut(id)
|
||||
{
|
||||
*slot = m;
|
||||
if let Some(mut m) = m {
|
||||
// Art made from type takes its emoji's pixels into the document.
|
||||
d.adopt_pixels(&mut m);
|
||||
if let Some(slot) = d.node_mut(id) {
|
||||
*slot = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1274,7 +1274,15 @@ impl Builder {
|
||||
let sy = img.height as f64 / f64::from(r.height.max(1));
|
||||
let xf = to_doc * affine(img.transform) * Affine::scale_non_uniform(sx, sy);
|
||||
self.doc.images.entry(r.key.clone()).or_insert(r.blob);
|
||||
Some(NodeKind::Image(Box::new(ImageObject { key: r.key, width: r.width, height: r.height, xf, link: None, placement: Default::default() })))
|
||||
Some(NodeKind::Image(Box::new(ImageObject {
|
||||
key: r.key,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,8 @@ fn flatten_images(d: &mut Document, images: &[(usize, NodeId)]) -> Result<()> {
|
||||
let png = flat.png;
|
||||
let key = super::super::menucmds::unique_key(d, "flattened");
|
||||
d.images.insert(key.clone(), ImageBlob::new("image/png", png));
|
||||
let image = ImageObject { key, width: flat.width, height: flat.height, xf: flat.xf, link: None, placement: Default::default() };
|
||||
let image =
|
||||
ImageObject { key, width: flat.width, height: flat.height, xf: flat.xf, link: None, placement: Default::default(), pixels: None };
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
|
||||
let mut group = super::super::rasterfx::clip_group(d, flat.clip, FillRule::NonZero, image);
|
||||
group.name = src.node(id).and_then(|n| n.name.clone());
|
||||
|
||||
@@ -658,8 +658,10 @@ pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
||||
let layer = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::scale_non_uniform(sx, sy);
|
||||
let mut n =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default() })));
|
||||
let mut n = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
n.name = Some(name.to_string());
|
||||
d.images.insert(key, blob);
|
||||
d.insert(Some(layer), 0, n).map_err(err)?;
|
||||
|
||||
@@ -248,6 +248,7 @@ fn a_raster_export_says_nothing_of_images_another_thread_could_not_decode() {
|
||||
xf: vectorcraft_geom::Affine::scale(20.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let (id, layer) = (broken.alloc_id(), broken.layers[0].id);
|
||||
broken.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
|
||||
@@ -1136,7 +1136,15 @@ fn raster_node(d: &mut Document, r: Raster) -> Node {
|
||||
d.images.insert(key.clone(), r.blob);
|
||||
let image = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: r.width, height: r.height, xf: r.xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
xf: r.xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let Some(path) = r.clip else { return image };
|
||||
super::rasterfx::clip_group(d, path, FillRule::NonZero, image)
|
||||
|
||||
@@ -1334,6 +1334,8 @@ fn expand_envelope(d: &mut Document, id: NodeId) -> Result<()> {
|
||||
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let mut g = vectorcraft_render::effects::expanded_live_group(Some(d), &n);
|
||||
fix_ids(d, &mut g);
|
||||
// Outlined type's emoji take their pixels into the document.
|
||||
d.adopt_pixels(&mut g);
|
||||
// Outlined type repeats its object's id on its pieces.
|
||||
vectorcraft_render::effects::fresh_ids(d, &mut g, &mut Default::default());
|
||||
*d.node_mut(id).ok_or(EngineError::NoNode(id))? = g;
|
||||
|
||||
@@ -469,7 +469,7 @@ impl Rasterized {
|
||||
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 image = ImageObject { key, width: self.width, height: self.height, xf: self.xf, link: None, placement: Default::default(), pixels: None };
|
||||
let node = Node::new(id, NodeKind::Image(Box::new(image)));
|
||||
match self.clip {
|
||||
Some((path, rule)) => super::rasterfx::clip_group(d, path, rule, node),
|
||||
@@ -721,6 +721,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
xf: Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
@@ -788,6 +789,7 @@ fn trim_image(s: &mut Session, id: NodeId, im: ImageObject) -> Result<Value> {
|
||||
xf: im.xf * Affine::translate((f64::from(x0), f64::from(y0))),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -385,6 +385,7 @@ fn build(d: &mut Document, l: Loaded, link: Option<LinkInfo>) -> Node {
|
||||
xf: Affine::scale_non_uniform(sx, sy),
|
||||
link,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ fn bitmap_document(cmd: &str, doc: &Document, set: &PrintSettings, rich_black: b
|
||||
let key = unique_key(&out, "print-bitmap");
|
||||
out.images.insert(key.clone(), ImageBlob::new("image/png", img.to_png().map_err(EngineError::Other)?));
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let image = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||||
let image = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default(), pixels: None };
|
||||
images.push(Arc::new(Node::new(out.alloc_id(), NodeKind::Image(Box::new(image)))));
|
||||
}
|
||||
let mut layer = Node::layer(out.alloc_id(), "Bitmap", LayerColor::Preset(0));
|
||||
|
||||
@@ -225,7 +225,15 @@ pub(crate) fn effect_image(out: &mut Document, whole: &Node, knockout: Option<&N
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let mut image = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
image.name = Some("Raster effect".into());
|
||||
Some(image)
|
||||
|
||||
@@ -92,6 +92,7 @@ mod tests {
|
||||
xf: Affine::scale(72.0 / ppi),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{AreaFit, Document, ImageObject, Justify, Node, NodeId, NodeKind, RubyAlign, RubyPosition, RubySpan, TextObject};
|
||||
use vectorcraft_doc::{AreaFit, Document, Justify, Node, NodeId, NodeKind, RubyAlign, RubyPosition, RubySpan, TextObject};
|
||||
use vectorcraft_geom::{Affine, PathData, Vec2};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
@@ -559,18 +559,10 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
children.push(Arc::new(Node::new(id, NodeKind::SymbolInstance { symbol: art.symbol.clone(), xf })));
|
||||
continue;
|
||||
}
|
||||
// Colour bitmap glyphs (emoji) become embedded images, in place; glyphs that show
|
||||
// the same image share its pixels, under a key of their own if another image has
|
||||
// taken theirs.
|
||||
if let Some((image, to_text)) = g.placed_image() {
|
||||
let mut key = format!("glyph-{}", image.blob.content_key());
|
||||
if d.images.get(&key).is_some_and(|b| b.bytes != image.blob.bytes) {
|
||||
key = super::menucmds::unique_key(d, "glyph");
|
||||
}
|
||||
d.images.entry(key.clone()).or_insert_with(|| image.blob.clone());
|
||||
let im =
|
||||
ImageObject { key, width: image.width, height: image.height, xf: t.xf * to_text, link: None, placement: Default::default() };
|
||||
children.push(Arc::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))));
|
||||
// Colour bitmap glyphs (emoji) become embedded images, in place, their pixels taken
|
||||
// into the document: glyphs that show the same image share them.
|
||||
if let Some(image) = g.placed_image_node(t.xf) {
|
||||
children.push(Arc::new(d.reid(&image)));
|
||||
continue;
|
||||
}
|
||||
// COLR glyphs become groups of their layers, in place, with ids of the document's.
|
||||
|
||||
@@ -42,6 +42,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
||||
xf: vectorcraft_geom::Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -1976,7 +1976,15 @@ fn add_image(s: &mut Session, r: &vectorcraft_trace::Raster, xf: Affine) -> Node
|
||||
0,
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: "img1".into(), width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "img1".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
),
|
||||
)?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -168,3 +168,63 @@ fn the_canvas_draws_svg_glyphs_as_their_art() {
|
||||
assert_eq!(img.pixel(14, 40), [255, 0, 0, 255], "its own red");
|
||||
assert_eq!(img.pixel(22, 40), [0, 0, 0, 255], "the type's black fill");
|
||||
}
|
||||
|
||||
/// Expand Appearance of type with an effect keeps its emoji as images, their pixels taken into
|
||||
/// the document; the PDF of the type before expanding draws them too.
|
||||
#[test]
|
||||
fn expand_appearance_keeps_the_emoji_of_type_with_effects() {
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, COLOUR_LARGE, COLOUR_PPEM, colour_bitmap_family, colour_bitmap_font, solid_png};
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let r = s
|
||||
.execute("text.create", &json!({"x": 10, "y": 50, "text": format!("{COLOUR_CHAR}F"), "size": 32, "font": colour_bitmap_family(false)}))
|
||||
.unwrap();
|
||||
let t = NodeId(r["id"].as_u64().unwrap());
|
||||
// Turned, not only moved: type that a Transform only moves stays type in exports.
|
||||
s.execute("effect.apply", &json!({"ids": [t.0], "effect": "distort.transform", "params": {"rotate": 10}})).unwrap();
|
||||
let pdf = vectorcraft_pdf::export_with_report(&s.doc().unwrap().doc, &vectorcraft_pdf::PdfOptions::uncompressed()).unwrap();
|
||||
assert!(pdf.warnings.is_empty(), "{:?}", pdf.warnings);
|
||||
assert!(pdf.bytes.windows(14).any(|w| w == b"/Subtype/Image"), "the emoji, in the PDF");
|
||||
let r = ids(&s.execute("effect.expandAppearance", &json!({})).unwrap());
|
||||
let mut images = vec![];
|
||||
for id in r {
|
||||
node(&s, id).walk(&mut |n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
images.push((**im).clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let glyph = vectorcraft_doc::ImageBlob::png(solid_png(COLOUR_PPEM.into(), COLOUR_LARGE));
|
||||
assert_eq!(images.len(), 1, "the emoji");
|
||||
assert!(images[0].pixels.is_none(), "the document holds them");
|
||||
assert_eq!(d.images[&images[0].key].bytes, glyph.bytes);
|
||||
vectorcraft_testkit::invariants::check_document(d).unwrap();
|
||||
}
|
||||
|
||||
/// Emoji type in a warp envelope: SVG export writes each emoji's pixels once for all the warp's
|
||||
/// pieces, and Object › Expand takes them into the document.
|
||||
#[test]
|
||||
fn an_envelope_shares_and_expands_the_emoji_of_its_type() {
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, colour_bitmap_family, colour_bitmap_font};
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let r = s
|
||||
.execute("text.create", &json!({"x": 10, "y": 50, "text": COLOUR_CHAR.to_string(), "size": 32, "font": colour_bitmap_family(false)}))
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r["id"]]})).unwrap();
|
||||
let e = NodeId(s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": 50})).unwrap()["id"].as_u64().unwrap());
|
||||
let svg = vectorcraft_svg::export_full(&s.doc().unwrap().doc, &Default::default(), None).svg;
|
||||
assert_eq!(svg.matches("data:image/png").count(), 1, "one copy of the pixels");
|
||||
s.execute("object.envelope.expand", &json!({})).unwrap();
|
||||
let mut images = vec![];
|
||||
node(&s, e).walk(&mut |n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
images.push((**im).clone());
|
||||
}
|
||||
});
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(images.len() > 1, "the warp's pieces");
|
||||
assert!(images.iter().all(|im| im.pixels.is_none() && d.images.contains_key(&im.key)), "the document holds them");
|
||||
vectorcraft_testkit::invariants::check_document(d).unwrap();
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ fn image_node(s: &mut Session) -> NodeId {
|
||||
xf: Default::default(),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -122,7 +122,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
||||
d.images.insert("pic".into(), vectorcraft_doc::ImageBlob::png(png));
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((100.0, 100.0)) * Affine::scale(10.0);
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default() };
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
||||
@@ -160,7 +160,7 @@ fn clicking_an_image_samples_its_pixel() {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((300.0, 0.0)) * vectorcraft_geom::Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: 2, height: 1, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "px".into(), width: 2, height: 1, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -186,7 +186,7 @@ fn cmyk_doc_with_image(px: &[[u8; 4]]) -> Session {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((300.0, 0.0)) * vectorcraft_geom::Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: w, height: 1, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "px".into(), width: w, height: 1, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -462,6 +462,7 @@ fn relative_paths_are_written_against_the_saved_file() {
|
||||
xf: Default::default(),
|
||||
link: Some(LinkInfo::new(format!("{root}{sep}art{sep}a.png"))),
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let rel = |dest: String| {
|
||||
|
||||
@@ -1103,7 +1103,7 @@ fn two_colour_image(s: &mut Session, clear_right: bool, linked: bool) -> NodeId
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((100.0, 50.0)) * vectorcraft_geom::Affine::scale(2.0);
|
||||
let link = linked.then(|| vectorcraft_doc::LinkInfo { path: "/tmp/pic.png".into(), ..Default::default() });
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 8, height: 4, xf, link, placement: Default::default() };
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 8, height: 4, xf, link, placement: Default::default(), pixels: None };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
||||
@@ -890,7 +890,7 @@ fn create_outlines_keeps_an_image_already_under_a_glyph_images_key() {
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let glyph = vectorcraft_doc::ImageBlob::png(solid_png(COLOUR_PPEM.into(), COLOUR_LARGE));
|
||||
let taken = format!("glyph-{}", glyph.content_key());
|
||||
let taken = glyph.content_key();
|
||||
let other = vectorcraft_doc::ImageBlob::png(solid_png(4, [0, 255, 0, 255]));
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).images.insert(taken.clone(), other.clone());
|
||||
s.execute("text.create", &json!({"x": 10, "y": 50, "text": COLOUR_CHAR.to_string(), "size": 32, "font": colour_bitmap_family(false)})).unwrap();
|
||||
|
||||
@@ -103,7 +103,15 @@ fn groups_layers_type_images_and_symbols_keep_their_raster_effects() {
|
||||
let xf = Affine::translate((40.0, 50.0)) * Affine::scale(2.5);
|
||||
let im = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key: "pic".into(), width: 40, height: 30, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "pic".into(),
|
||||
width: 40,
|
||||
height: 30,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
add(&mut d, with(im, shadow()));
|
||||
assert_written("image", &d);
|
||||
|
||||
Reference in New Issue
Block a user