mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
412 lines
21 KiB
Rust
412 lines
21 KiB
Rust
//! Effect → Document Raster Effects Settings (`document.rasterEffectsSettings`): the settings, the
|
||
//! 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;
|
||
use vectorcraft_doc::{Background, Document, NodeId, NodeKind, RasterColorModel, RasterEffectsSettings};
|
||
|
||
use super::*;
|
||
|
||
const C: &str = "document.rasterEffectsSettings";
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn id(v: &Value) -> NodeId {
|
||
NodeId(v["id"].as_u64().unwrap())
|
||
}
|
||
|
||
/// A red rectangle with a drop shadow.
|
||
fn shadowed(s: &mut Session) -> NodeId {
|
||
let r = id(&s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap());
|
||
s.execute("paint.setFill", &json!({"color": "#ff0000", "ids": [r.0]})).unwrap();
|
||
s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [r.0]})).unwrap();
|
||
r
|
||
}
|
||
|
||
/// The decoded pixels of the first raster effect image of `doc` flattened for output.
|
||
fn effect_image(doc: &Document) -> image::RgbaImage {
|
||
let flat = crate::flatten_raster_effects(doc).unwrap();
|
||
let kids = flat.layers[0].children().unwrap();
|
||
let NodeKind::Group { children, .. } = &kids[0].kind else { panic!("a shadow becomes a group") };
|
||
let NodeKind::Image(im) = &children[0].kind else { panic!("the image goes under the object") };
|
||
image::load_from_memory(&flat.images[&im.key].bytes).unwrap().to_rgba8()
|
||
}
|
||
|
||
#[test]
|
||
fn settings_report_change_in_one_step_and_reject_junk() {
|
||
let mut s = session();
|
||
let r = s.execute(C, &json!({})).unwrap();
|
||
assert_eq!(
|
||
r,
|
||
json!({"resolution": 72.0, "colorModel": "rgb", "background": "transparent", "antiAlias": true, "clippingMask": false, "addAround": 0.0, "preserveSpotColors": true})
|
||
);
|
||
let r = s
|
||
.execute(C, &json!({"resolution": "high", "colorModel": "grayscale", "background": "white", "antiAlias": false, "clippingMask": true, "addAround": 36}))
|
||
.unwrap();
|
||
assert_eq!((r["resolution"].as_f64(), r["colorModel"].as_str(), r["addAround"].as_f64()), (Some(300.0), Some("grayscale"), Some(36.0)));
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), 1);
|
||
// The report fed back is no change.
|
||
s.execute(C, &r).unwrap();
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), 1);
|
||
for bad in [
|
||
json!({"colorModel": "cmyk"}),
|
||
json!({"colorModel": "lab"}),
|
||
json!({"background": "pink"}),
|
||
json!({"antiAlias": "yes"}),
|
||
json!({"addAround": -1}),
|
||
json!({"addAround": 5000}),
|
||
json!({"resolution": 0}),
|
||
json!({"dpi": 300}),
|
||
] {
|
||
assert!(s.execute(C, &bad).is_err(), "{bad}");
|
||
}
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.raster_effects_ppi, &d.raster_effects), (72.0, &RasterEffectsSettings::default()));
|
||
// A CMYK document names its own mode.
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"colorMode": "cmyk"})).unwrap();
|
||
assert_eq!(s.execute(C, &json!({"colorModel": "cmyk"})).unwrap()["colorModel"], "cmyk");
|
||
}
|
||
|
||
#[test]
|
||
fn a_white_background_makes_effect_images_opaque() {
|
||
let mut s = session();
|
||
shadowed(&mut s);
|
||
let clear = effect_image(&s.doc().unwrap().doc);
|
||
assert_eq!(clear.get_pixel(0, 0)[3], 0, "transparent by default");
|
||
s.execute(C, &json!({"background": "white"})).unwrap();
|
||
let white = effect_image(&s.doc().unwrap().doc);
|
||
assert!(white.pixels().all(|p| p[3] == 255), "every pixel is opaque");
|
||
assert_eq!(white.get_pixel(0, 0).0, [255, 255, 255, 255]);
|
||
// With a clipping mask, the white stays under the art only.
|
||
s.execute(C, &json!({"clippingMask": true})).unwrap();
|
||
assert_eq!(effect_image(&s.doc().unwrap().doc).get_pixel(0, 0)[3], 0);
|
||
}
|
||
|
||
#[test]
|
||
fn add_around_grows_the_effect_image() {
|
||
let mut s = session();
|
||
shadowed(&mut s);
|
||
let before = effect_image(&s.doc().unwrap().doc);
|
||
s.execute(C, &json!({"addAround": 36})).unwrap();
|
||
let after = effect_image(&s.doc().unwrap().doc);
|
||
// 36 pt on every side at 72 ppi.
|
||
assert_eq!((after.width() - before.width(), after.height() - before.height()), (72, 72));
|
||
}
|
||
|
||
/// The transforms of the images in `doc` flattened for output.
|
||
fn image_transforms(doc: &Document) -> Vec<[f64; 6]> {
|
||
let mut xfs = vec![];
|
||
crate::flatten_raster_effects(doc).unwrap().walk(|n| {
|
||
if let NodeKind::Image(im) = &n.kind {
|
||
xfs.push(im.xf.as_coeffs());
|
||
}
|
||
});
|
||
xfs
|
||
}
|
||
|
||
#[test]
|
||
fn a_file_with_add_around_out_of_range_opens_with_it_in_range() {
|
||
let mut s = session();
|
||
shadowed(&mut s);
|
||
let mut d = (*s.doc().unwrap().doc).clone();
|
||
d.raster_effects.add_around = 1e300;
|
||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap();
|
||
assert_eq!(back.raster_effects.add_around, MAX_ADD_AROUND);
|
||
let xfs = image_transforms(&back);
|
||
assert!(!xfs.is_empty() && xfs.iter().flatten().all(|c| c.is_finite()), "{xfs:?}");
|
||
// 1,000 pt on every side at 72 ppi.
|
||
assert_eq!(effect_image(&back).width() - effect_image(&s.doc().unwrap().doc).width(), 2000);
|
||
d.raster_effects.add_around = -5.0;
|
||
assert_eq!(vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap().raster_effects.add_around, 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn art_too_large_for_f64_gets_no_effect_image() {
|
||
let mut s = session();
|
||
let r = shadowed(&mut s);
|
||
// Settings out of range in a document made without opening a file.
|
||
let mut d = (*s.doc().unwrap().doc).clone();
|
||
d.raster_effects.add_around = 1e300;
|
||
assert_eq!(image_transforms(&d), Vec::<[f64; 6]>::new());
|
||
let mut d = (*s.doc().unwrap().doc).clone();
|
||
let NodeKind::Path { path, live, .. } = &mut d.node_mut(r).unwrap().kind else { panic!("a path") };
|
||
(*path, *live) = (vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(-1e300, -1e300, 1e300, 1e300)), None);
|
||
assert_eq!(image_transforms(&d), Vec::<[f64; 6]>::new());
|
||
// Expand Appearance of it by id, with nothing selected, puts no image into the document (the
|
||
// rectangle at its own size gets one).
|
||
let normal = (*s.doc().unwrap().doc).clone();
|
||
let mut expanded_images = |d: Document| {
|
||
let st = s.doc_mut().unwrap();
|
||
(st.doc, st.selection) = (std::sync::Arc::new(d), Default::default());
|
||
s.execute("effect.expandAppearance", &json!({"ids": [r.0]})).unwrap();
|
||
let mut images = 0;
|
||
s.doc().unwrap().doc.walk(|n| images += usize::from(matches!(n.kind, NodeKind::Image(_))));
|
||
images
|
||
};
|
||
assert_eq!(expanded_images(normal), 1);
|
||
assert_eq!(expanded_images(d), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn the_color_model_and_anti_alias_reach_the_pixels() {
|
||
let mut s = session();
|
||
shadowed(&mut s);
|
||
s.execute(C, &json!({"colorModel": "bitmap", "antiAlias": false})).unwrap();
|
||
let img = effect_image(&s.doc().unwrap().doc);
|
||
assert!(img.pixels().filter(|p| p[3] > 0).all(|p| p[0] == p[1] && p[1] == p[2] && (p[0] == 0 || p[0] == 255)), "black and white");
|
||
// Anti-alias off: Rasterize leaves no partly covered pixel on a circle's edge.
|
||
let e = id(&s.execute("shape.ellipse", &json!({"x": 180, "y": 180, "width": 50, "height": 50})).unwrap());
|
||
s.execute("select.set", &json!({"ids": [e.0]})).unwrap();
|
||
let out = s.execute("object.rasterize", &json!({"colorModel": "rgb"})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
let NodeKind::Image(im) = &d.node(id(&out)).unwrap().kind else { panic!("an image") };
|
||
let img = image::load_from_memory(&d.images[&im.key].bytes).unwrap().to_rgba8();
|
||
assert!(img.pixels().all(|p| p[3] == 0 || p[3] == 255), "hard edges");
|
||
assert!(img.pixels().any(|p| p[3] == 0) && img.pixels().any(|p| p[3] == 255));
|
||
}
|
||
|
||
#[test]
|
||
fn rasterize_takes_the_document_settings_as_defaults() {
|
||
let mut s = session();
|
||
s.execute(C, &json!({"background": "white", "colorModel": "grayscale", "addAround": 10})).unwrap();
|
||
let r = id(&s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 40, "height": 40})).unwrap());
|
||
s.execute("paint.setFill", &json!({"color": "#ff0000", "ids": [r.0]})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"none": true, "ids": [r.0]})).unwrap();
|
||
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
|
||
let out = s.execute("object.rasterize", &json!({"ppi": 72})).unwrap();
|
||
assert_eq!((out["width"].as_u64(), out["height"].as_u64()), (Some(60), Some(60)), "10 pt around");
|
||
let d = &s.doc().unwrap().doc;
|
||
let NodeKind::Image(im) = &d.node(id(&out)).unwrap().kind else { panic!("an image") };
|
||
let img = image::load_from_memory(&d.images[&im.key].bytes).unwrap().to_rgba8();
|
||
assert_eq!(img.get_pixel(0, 0).0, [255, 255, 255, 255], "white around the art");
|
||
let p = img.get_pixel(30, 30).0;
|
||
assert!(p[0] == p[1] && p[1] == p[2] && p[0] < 128, "red in grayscale: {p:?}");
|
||
// Params win; a clipping mask makes a clip group with the art's outline.
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
|
||
let out = s.execute("object.rasterize", &json!({"ppi": 72, "padding": 0, "clippingMask": true, "background": "transparent"})).unwrap();
|
||
let d = &s.doc().unwrap().doc;
|
||
let NodeKind::Group { children, clip: true } = &d.node(id(&out)).unwrap().kind else { panic!("a clip group") };
|
||
let clip = children[0].geometric_bounds().unwrap();
|
||
assert!((clip.width() - 40.0).abs() < 1e-6 && matches!(children[1].kind, NodeKind::Image(_)));
|
||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [id(&out)]);
|
||
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);
|
||
d.raster_effects =
|
||
RasterEffectsSettings { color_model: RasterColorModel::Bitmap, background: Background::White, add_around: 12.0, ..Default::default() };
|
||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&d)).unwrap();
|
||
assert_eq!(back.raster_effects, d.raster_effects);
|
||
// Saved before the settings existed: only the resolution.
|
||
let old = Document::new(10.0, 10.0);
|
||
let bytes = vectorcraft_format::save_file(&old);
|
||
assert!(!String::from_utf8_lossy(&bytes).contains("raster_effects\""), "default settings aren't written");
|
||
let back = vectorcraft_format::load(&bytes).unwrap();
|
||
assert_eq!((back.raster_effects_ppi, back.raster_effects), (72.0, RasterEffectsSettings::default()));
|
||
}
|
||
|
||
/// Save the active document with the raster effects resolution `ppi` and open the file.
|
||
fn reopen_with_ppi(s: &mut Session, ppi: f64) {
|
||
let mut d = (*s.doc().unwrap().doc).clone();
|
||
d.raster_effects_ppi = ppi;
|
||
let data = vectorcraft_format::base64_encode(&vectorcraft_format::save_file(&d));
|
||
s.execute("document.open", &json!({"name": "ppi.vectorcraft", "dataBase64": data})).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn a_file_with_the_resolution_out_of_range_opens_with_it_in_range() {
|
||
for (saved, opened, pixels) in [(0.0, 1.0, 1), (-5.0, 1.0, 1), (5000.0, 2400.0, 334), (f64::MAX, 2400.0, 334)] {
|
||
let mut s = session();
|
||
let r = id(&s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 10, "height": 10})).unwrap());
|
||
s.execute("paint.setStroke", &json!({"none": true, "ids": [r.0]})).unwrap();
|
||
reopen_with_ppi(&mut s, saved);
|
||
assert_eq!(s.doc().unwrap().doc.raster_effects_ppi, opened, "saved as {saved}");
|
||
assert_eq!(s.execute(C, &json!({})).unwrap()["resolution"], opened);
|
||
assert_eq!(s.execute("document.info", &json!({})).unwrap()["document"]["rasterEffectsPpi"], opened);
|
||
// Rasterize without a ppi param renders the 10 pt square at it.
|
||
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
|
||
let out = s.execute("object.rasterize", &json!({})).unwrap();
|
||
assert_eq!((out["width"].as_u64(), out["height"].as_u64()), (Some(pixels), Some(pixels)), "saved as {saved}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_gradient_exports_at_the_resolution_the_file_opens_with() {
|
||
for saved in [0.0, -5.0] {
|
||
let mut s = session();
|
||
let r = id(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 720, "height": 720})).unwrap());
|
||
s.execute("paint.setFill", &json!({"gradient": {"kind": "linear"}, "ids": [r.0]})).unwrap();
|
||
reopen_with_ppi(&mut s, saved);
|
||
// EMF writes the gradient as an image at the raster effects resolution: 720 pt at 1 ppi.
|
||
let region = vectorcraft_geom::Rect::new(0.0, 0.0, 720.0, 720.0);
|
||
let out = vectorcraft_metafile::export(&s.doc().unwrap().doc, region, vectorcraft_metafile::Kind::Emf).unwrap();
|
||
let mut sizes = vec![];
|
||
vectorcraft_metafile::import(&out.bytes).unwrap().document.walk(|n| {
|
||
if let NodeKind::Image(im) = &n.kind {
|
||
sizes.push((im.width, im.height));
|
||
}
|
||
});
|
||
assert_eq!(sizes, [(10, 10)], "saved as {saved}");
|
||
}
|
||
}
|