mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 20:34:18 +00:00
401 lines
20 KiB
Rust
401 lines
20 KiB
Rust
//! SVG Options through the engine: every option changes the output, artboards write one file
|
||
//! each, junk is rejected, and Save writes SVG with remembered options.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use super::*;
|
||
|
||
fn session(artboards: usize) -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": artboards})).unwrap();
|
||
s.execute("shape.rectangle", &json!({"x": 10.123456, "y": 10, "width": 50, "height": 40})).unwrap();
|
||
s
|
||
}
|
||
|
||
pub(super) fn svg(s: &mut Session, p: Value) -> String {
|
||
let mut p = p;
|
||
p["format"] = json!("svg");
|
||
s.execute("document.serialize", &p).unwrap_or_else(|e| panic!("{p}: {e}"))["text"].as_str().unwrap().to_string()
|
||
}
|
||
|
||
fn tmp_dir(tag: &str) -> std::path::PathBuf {
|
||
let d = std::env::temp_dir().join(format!("vc-svgopts-{tag}-{}", std::process::id()));
|
||
std::fs::create_dir_all(&d).unwrap();
|
||
d
|
||
}
|
||
|
||
/// A session whose document holds one embedded 2×2 PNG.
|
||
pub(super) fn image_session() -> Session {
|
||
let mut png = Vec::new();
|
||
image::RgbaImage::from_pixel(2, 2, image::Rgba([10, 200, 30, 255]))
|
||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||
.unwrap();
|
||
let mut s = Session::new();
|
||
s.execute("document.open", &json!({"name": "dot.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
|
||
s
|
||
}
|
||
|
||
#[test]
|
||
fn every_option_changes_the_output() {
|
||
let mut s = session(1);
|
||
let plain = svg(&mut s, json!({}));
|
||
assert!(plain.contains("fill=\"#") && plain.contains(" width=\"200\"") && plain.contains("id=\"Layer_1\""), "{plain}");
|
||
let changed = |s: &mut Session, p: Value, check: &dyn Fn(&str) -> bool| {
|
||
let out = svg(s, p.clone());
|
||
assert_ne!(out, plain, "{p} changed nothing");
|
||
assert!(check(&out), "{p}:\n{out}");
|
||
};
|
||
changed(&mut s, json!({"styling": "style"}), &|o| o.contains("style=\"fill:#"));
|
||
changed(&mut s, json!({"styling": "entities"}), &|o| o.contains("<!ENTITY st1 ") && o.contains("style=\"&st1;\""));
|
||
changed(&mut s, json!({"svg": {"styling": "css"}}), &|o| o.contains("<style>") && o.contains("class=\"cls-1\""));
|
||
changed(&mut s, json!({"objectIds": "minimal"}), &|o| !o.contains(" id=\""));
|
||
changed(&mut s, json!({"objectIds": "unique"}), &|o| o.contains(" id=\"u") && o.contains("-Layer_1\""));
|
||
changed(&mut s, json!({"decimals": 1}), &|o| o.contains("x=\"10.1\"") && !o.contains("10.123"));
|
||
changed(&mut s, json!({"decimals": 6}), &|o| o.contains("10.123456"));
|
||
changed(&mut s, json!({"minify": true}), &|o| !o.trim_end().contains('\n') && !o.contains("<?xml"));
|
||
changed(&mut s, json!({"responsive": true}), &|o| !o.contains(" width=\"200\"") && o.contains("viewBox=\"0 0 200 100\""));
|
||
changed(&mut s, json!({"useArtboards": false}), &|o| !o.contains("viewBox=\"0 0 200 100\""));
|
||
changed(&mut s, json!({"metadata": true}), &|o| o.contains("<metadata>") && o.contains("<dc:format>image/svg+xml</dc:format>"));
|
||
changed(&mut s, json!({"preserveEditing": true}), &|o| o.contains(vectorcraft_svg::EDITING_NS));
|
||
s.execute("text.create", &json!({"x": 20, "y": 80, "text": "Hi"})).unwrap();
|
||
let with_text = svg(&mut s, json!({}));
|
||
assert!(with_text.contains("<text"));
|
||
assert!(!svg(&mut s, json!({"outlineText": true})).contains("<text"), "Fonts → outlines");
|
||
// The nested object wins over the top level.
|
||
assert!(svg(&mut s, json!({"styling": "style", "svg": {"styling": "css"}})).contains("<style>"));
|
||
|
||
let mut s = image_session();
|
||
assert!(svg(&mut s, json!({})).contains("href=\"data:image/png;base64,"));
|
||
let r = s.execute("document.serialize", &json!({"format": "svg", "images": "link"})).unwrap();
|
||
let name = r["linked"][0]["name"].as_str().unwrap().to_string();
|
||
assert!(name.ends_with(".png") && r["text"].as_str().unwrap().contains(&format!("href=\"{name}\"")), "{r}");
|
||
}
|
||
|
||
#[test]
|
||
fn junk_options_are_rejected() {
|
||
let mut s = session(1);
|
||
for p in [
|
||
json!({"styling": "fancy"}),
|
||
json!({"objectIds": true}),
|
||
json!({"decimals": 0}),
|
||
json!({"decimals": 8}),
|
||
json!({"svg": {"bogus": 1}}),
|
||
json!({"svg": "css"}),
|
||
json!({"images": "inline"}),
|
||
json!({"useArtboards": "yes"}),
|
||
] {
|
||
let mut q = p.clone();
|
||
q["format"] = json!("svg");
|
||
assert!(s.execute("document.export", &q).is_err(), "{p} was accepted");
|
||
}
|
||
// Other formats ignore the SVG options.
|
||
assert!(s.execute("document.export", &json!({"format": "png", "styling": "fancy"})).is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn several_artboards_write_one_file_each() {
|
||
let mut s = session(3);
|
||
s.execute("artboard.setProps", &json!({"index": 0, "name": "Cover"})).unwrap();
|
||
let dir = tmp_dir("boards");
|
||
let path = dir.join("art.svg").to_string_lossy().to_string();
|
||
let r = s.execute("document.export", &json!({"path": path, "range": "all"})).unwrap();
|
||
let files: Vec<&str> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect();
|
||
let names: Vec<&str> = files.iter().map(|f| f.rsplit(['/', '\\']).next().unwrap()).collect();
|
||
assert_eq!(names, ["art-Cover.svg", "art-Artboard-2.svg", "art-Artboard-3.svg"]);
|
||
for f in &files {
|
||
assert!(std::fs::read_to_string(f).unwrap().contains("viewBox=\"0 0 200 100\""), "{f}");
|
||
}
|
||
let r = s.execute("document.export", &json!({"format": "svg", "svg": {"range": "2-3"}})).unwrap();
|
||
assert_eq!(r["files"].as_array().unwrap().len(), 2, "{r}");
|
||
let r = s.execute("document.serialize", &json!({"format": "svg", "range": "all"})).unwrap();
|
||
assert_eq!(r["files"].as_array().unwrap().len(), 3);
|
||
assert!(r["files"][2]["text"].as_str().unwrap().starts_with("<?xml"));
|
||
let _ = std::fs::remove_dir_all(dir);
|
||
}
|
||
|
||
#[test]
|
||
fn linked_images_are_written_next_to_the_svg() {
|
||
let mut s = image_session();
|
||
let dir = tmp_dir("linked");
|
||
let path = dir.join("pic.svg").to_string_lossy().to_string();
|
||
let r = s.execute("document.export", &json!({"path": path, "images": "link"})).unwrap();
|
||
let linked = r["linked"][0].as_str().unwrap();
|
||
assert!(image::load_from_memory(&std::fs::read(linked).unwrap()).is_ok(), "{linked}");
|
||
let text = std::fs::read_to_string(&path).unwrap();
|
||
let name = std::path::Path::new(linked).file_name().unwrap().to_string_lossy().to_string();
|
||
assert!(text.contains(&format!("href=\"{name}\"")), "{text}");
|
||
assert!(s.execute("document.exportSelection", &json!({"format": "svg", "images": "link"})).is_err(), "nowhere to put the images");
|
||
let _ = std::fs::remove_dir_all(dir);
|
||
}
|
||
|
||
#[test]
|
||
fn preserve_editing_reopens_the_native_document() {
|
||
let mut s = session(2);
|
||
s.execute("artboard.setProps", &json!({"index": 1, "name": "Back"})).unwrap();
|
||
let text = svg(&mut s, json!({"preserveEditing": true}));
|
||
let r = s.execute("document.open", &json!({"name": "both.svg", "dataBase64": vectorcraft_format::base64_encode(text.as_bytes())})).unwrap();
|
||
assert_eq!(r["format"], "svg");
|
||
let doc = &s.doc().unwrap().doc;
|
||
assert_eq!(doc.artboards.len(), 2, "both artboards came back");
|
||
assert_eq!(doc.artboards[1].name, "Back");
|
||
assert_eq!(doc.title, "both.svg");
|
||
// Without it the SVG is imported (one artboard).
|
||
let text = svg(&mut s, json!({}));
|
||
s.execute("document.open", &json!({"name": "plain.svg", "dataBase64": vectorcraft_format::base64_encode(text.as_bytes())})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn saving_several_artboards_as_plain_svg_warns_they_are_lost() {
|
||
let artboards = |r: &Value| r["warnings"].as_array().unwrap().iter().filter(|w| w.as_str().unwrap().contains("artboards")).count();
|
||
let mut s = session(3);
|
||
let r = s.execute("document.save", &json!({"format": "svg"})).unwrap();
|
||
assert_eq!(artboards(&r), 1, "{r}");
|
||
// First: the status bar shows only the first warning.
|
||
assert!(r["warnings"][0].as_str().unwrap().contains("1 of the document's 3 artboards"), "{r}");
|
||
assert_eq!(artboards(&s.execute("file.saveAs", &json!({"format": "svgz"})).unwrap()), 1);
|
||
// Preserve Editing keeps them all (they reopen); a single artboard loses none.
|
||
for p in [json!({"format": "svg", "preserveEditing": true}), json!({"format": "svg", "svg": {"preserveEditing": true}})] {
|
||
let r = s.execute("document.save", &p).unwrap();
|
||
assert_eq!(artboards(&r), 0, "{p}: {r}");
|
||
}
|
||
assert_eq!(artboards(&session(1).execute("document.save", &json!({"format": "svg"})).unwrap()), 0);
|
||
}
|
||
|
||
/// #3867: a Preserve Editing SVG opened from disk and saved with plain Save keeps the full
|
||
/// document (every artboard), not just the artwork of the first.
|
||
#[test]
|
||
fn reopened_preserve_editing_svg_saves_as_the_full_document() {
|
||
let dir = tmp_dir("reopen");
|
||
let path = dir.join("keep.svg").to_string_lossy().to_string();
|
||
session(3).execute("document.save", &json!({"path": path, "svg": {"preserveEditing": true}})).unwrap();
|
||
let mut s = Session::new();
|
||
s.execute("document.open", &json!({"path": path})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 3);
|
||
s.execute("document.save", &json!({})).unwrap();
|
||
let mut t = Session::new();
|
||
let r = t.execute("document.open", &json!({"path": path})).unwrap();
|
||
assert_eq!(r["restored"], json!(true), "{r}");
|
||
assert_eq!(t.doc().unwrap().doc.artboards.len(), 3, "every artboard survives open and Save");
|
||
let _ = std::fs::remove_dir_all(dir);
|
||
}
|
||
|
||
/// #3869: small capitals, superscript and subscript, and tab leaders, which SVG text can't draw,
|
||
/// are written as outlines (as vertical type and ruby are) rather than as plain text at full size;
|
||
/// type without them stays text.
|
||
#[test]
|
||
fn type_svg_text_cant_draw_is_written_as_outlines() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 500, "height": 200})).unwrap();
|
||
s.execute("text.create", &json!({"x": 50, "y": 100, "text": "Small caps x2 H2O", "size": 30})).unwrap();
|
||
let plain = svg(&mut s, json!({}));
|
||
assert!(plain.contains("<text"), "plain type stays text: {plain}");
|
||
for style in [json!({"smallCaps": true}), json!({"position": "superscript"}), json!({"position": "subscript"})] {
|
||
let mut t = Session::new();
|
||
t.execute("file.new", &json!({"width": 500, "height": 200})).unwrap();
|
||
let id = t.execute("text.create", &json!({"x": 50, "y": 100, "text": "Small caps x2 H2O", "size": 30})).unwrap()["id"].clone();
|
||
let mut p = style.clone();
|
||
p["id"] = id;
|
||
p["start"] = json!(0);
|
||
p["end"] = json!(5);
|
||
t.execute("text.setRangeStyle", &p).unwrap();
|
||
let out = svg(&mut t, json!({}));
|
||
assert!(!out.contains("<text") && out.contains("<path"), "{style}: {out}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn save_writes_svg_and_remembers_its_options() {
|
||
let mut s = session(1);
|
||
let dir = tmp_dir("save");
|
||
let path = dir.join("doc.svg").to_string_lossy().to_string();
|
||
let r = s.execute("document.save", &json!({"path": path, "svg": {"styling": "css", "decimals": 2}})).unwrap();
|
||
assert_eq!(r["path"], json!(path));
|
||
assert!(std::fs::read_to_string(&path).unwrap().contains("<style>"));
|
||
let st = s.doc().unwrap();
|
||
assert_eq!(st.path.as_deref(), Some(path.as_str()));
|
||
assert!(!st.is_dirty());
|
||
s.execute("shape.ellipse", &json!({"x": 100, "y": 10, "width": 30, "height": 30})).unwrap();
|
||
s.execute("document.save", &json!({})).unwrap();
|
||
let text = std::fs::read_to_string(&path).unwrap();
|
||
assert!(text.contains("<style>") && vectorcraft_testkit::svg_shapes(&text) == 2, "Save reuses the options: {text}");
|
||
assert!(s.execute("document.save", &json!({"path": dir.join("x.png").to_string_lossy()})).is_err(), "PNG is an export");
|
||
// A native save switches back.
|
||
let native = dir.join("doc.vectorcraft").to_string_lossy().to_string();
|
||
s.execute("document.save", &json!({"path": native})).unwrap();
|
||
assert!(vectorcraft_format::sniff(&std::fs::read(&native).unwrap()));
|
||
assert_eq!(
|
||
Value::Object(s.doc().unwrap().save_options.clone()),
|
||
json!({"pdfCompatible":true}),
|
||
"a native save retains its PDF choice without SVG options"
|
||
);
|
||
let _ = std::fs::remove_dir_all(dir);
|
||
}
|
||
|
||
#[test]
|
||
fn formats_list_the_svg_options_with_their_defaults() {
|
||
let mut s = Session::new();
|
||
let r = s.execute("document.formats", &json!({})).unwrap();
|
||
let svg = r["formats"].as_array().unwrap().iter().find(|f| f["id"] == "svg").unwrap();
|
||
let defaults = serde_json::to_value(vectorcraft_svg::ExportOptions::default()).unwrap();
|
||
for (k, v) in defaults.as_object().unwrap() {
|
||
assert_eq!(&svg["options"][k]["default"], v, "{k}");
|
||
}
|
||
for k in ["useArtboards", "range", "svg"] {
|
||
assert!(svg["options"].get(k).is_some(), "{k}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn fewer_tspans_reaches_the_writer() {
|
||
let mut s = Session::new();
|
||
let src = r##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><text x="10" y="40" font-size="12">plain <tspan fill="#ff0000">red</tspan> plain</text></svg>"##;
|
||
s.execute("document.open", &json!({"name": "two-runs.svg", "dataBase64": vectorcraft_format::base64_encode(src.as_bytes())})).unwrap();
|
||
let placed = |svg: &str| svg.matches("<tspan x=").count();
|
||
assert_eq!(placed(&svg(&mut s, json!({}))), 3, "a tspan per style run");
|
||
assert_eq!(placed(&svg(&mut s, json!({"fewerTspans": true}))), 1, "a tspan per line");
|
||
}
|
||
|
||
/// Gunzip SVGZ bytes.
|
||
fn gunzip(bytes: &[u8]) -> String {
|
||
assert!(vectorcraft_svg::is_svgz(bytes), "gzip magic");
|
||
vectorcraft_svg::text_of(bytes).unwrap().into_owned()
|
||
}
|
||
|
||
#[test]
|
||
fn svgz_is_the_svg_export_gzipped() {
|
||
let mut s = session(2);
|
||
let opts = json!({"styling": "css", "range": "2"});
|
||
let mut p = opts.clone();
|
||
p["format"] = json!("svgz");
|
||
let svgz = vectorcraft_format::base64_decode(s.execute("document.serialize", &p).unwrap()["dataBase64"].as_str().unwrap()).unwrap();
|
||
assert_eq!(gunzip(&svgz), svg(&mut s, opts));
|
||
// The format comes from the extension too, and Save writes it.
|
||
let dir = tmp_dir("svgz");
|
||
let path = dir.join("art.svgz").to_string_lossy().to_string();
|
||
s.execute("document.export", &json!({"path": path, "range": "all"})).unwrap();
|
||
assert!(gunzip(&std::fs::read(dir.join("art-Artboard-1.svgz")).unwrap()).contains("<svg"));
|
||
let saved = dir.join("saved.svgz").to_string_lossy().to_string();
|
||
s.execute("document.save", &json!({"path": saved, "svg": {}})).unwrap();
|
||
assert!(gunzip(&std::fs::read(&saved).unwrap()).contains("<svg"));
|
||
assert_eq!(s.doc().unwrap().path.as_deref(), Some(saved.as_str()));
|
||
let _ = std::fs::remove_dir_all(dir);
|
||
}
|
||
|
||
#[test]
|
||
fn svgz_opens_and_pastes_like_svg() {
|
||
let mut s = session(1);
|
||
s.execute("shape.ellipse", &json!({"x": 100, "y": 20, "width": 40, "height": 30})).unwrap();
|
||
let text = svg(&mut s, json!({}));
|
||
let open = |s: &mut Session, name: &str, bytes: &[u8]| {
|
||
s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
|
||
let mut doc = (*s.doc().unwrap().doc).clone();
|
||
doc.title.clear();
|
||
vectorcraft_format::save(&doc, false)
|
||
};
|
||
let plain = open(&mut s, "a.svg", text.as_bytes());
|
||
// Detected by the magic bytes whatever the name says.
|
||
assert_eq!(open(&mut s, "a.svg", &vectorcraft_svg::compress(&text)), plain);
|
||
assert_eq!(open(&mut s, "a.svgz", &vectorcraft_svg::compress(&text)), plain);
|
||
let r = s.execute("clipboard.importSvg", &json!({"dataBase64": vectorcraft_format::base64_encode(&vectorcraft_svg::compress(&text))})).unwrap();
|
||
assert_eq!(r["count"], 2);
|
||
assert!(s.execute("document.open", &json!({"name": "bad.svgz", "dataBase64": vectorcraft_format::base64_encode(&[0x1f, 0x8b, 1, 2])})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn svg_darkness_preserves_source_model_and_gradient_appearance() {
|
||
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
|
||
use vectorcraft_doc::{ColorMode, Effect};
|
||
use vectorcraft_geom::Rect;
|
||
for kind in ["rgb", "cmykSource", "gradient", "cmykDocument"] {
|
||
let mut s = session(1);
|
||
s.edit("paint source", |d, _| {
|
||
if kind == "cmykDocument" {
|
||
d.color_mode = ColorMode::Cmyk;
|
||
}
|
||
let id = d.layers[0].children().unwrap()[0].id;
|
||
let n = d.node_mut(id).unwrap();
|
||
n.appearance.fill_mut().unwrap().paint = match kind {
|
||
"cmykSource" => Paint::solid(Color::cmyk(0.15, 0.3, 0.4, 0.2)),
|
||
"gradient" => Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(
|
||
GradientKind::Linear,
|
||
vec![GradientStop::new(0.0, Color::BLACK), GradientStop::new(1.0, Color::rgb(0.8, 0.4, 0.2))],
|
||
)))),
|
||
_ => Paint::solid(Color::rgb(0.8, 0.4, 0.2)),
|
||
};
|
||
n.appearance.items.retain(|i| i.is_fill());
|
||
n.appearance.effects = vec![Effect {
|
||
id: "stylize.dropShadow".into(),
|
||
visible: true,
|
||
params: json!({"shade":"darkness","darkness":25,"x":60,"y":0,"blur":0,"opacity":100,"mode":"normal"}),
|
||
}];
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
let native = s.doc().unwrap().doc.clone();
|
||
let text = svg(&mut s, json!({}));
|
||
if kind == "rgb" {
|
||
assert!(text.contains("feComponentTransfer") && !text.contains("<image"));
|
||
} else {
|
||
assert!(text.contains("<image"), "{kind}: the shadow must preserve rendered appearance");
|
||
}
|
||
let back = vectorcraft_svg::import(&text).unwrap();
|
||
let region = Rect::new(0.0, 0.0, 200.0, 100.0);
|
||
let expected = vectorcraft_render::Renderer::new().render_region(&native, region, 1.0, true);
|
||
let actual = vectorcraft_render::Renderer::new().render_region(&back, region, 1.0, true);
|
||
for x in 80..115 {
|
||
let (a, b) = (actual.pixel(x, 30), expected.pixel(x, 30));
|
||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 8), "{kind} at {x}: {a:?} vs {b:?}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn svg_darkness_uses_rendered_shadow_in_non_srgb_working_space() {
|
||
const CHILD: &str = "VECTORCRAFT_TEST_SHADOW_RGB_PROFILE";
|
||
if std::env::var_os(CHILD).is_none() {
|
||
// Color settings are process-wide: exercise this profile in a separate test process.
|
||
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||
.args(["--exact", "cmd::fileio::tests_svg::svg_darkness_uses_rendered_shadow_in_non_srgb_working_space", "--nocapture"])
|
||
.env(CHILD, "1")
|
||
.output()
|
||
.unwrap();
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
assert!(
|
||
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
|
||
"isolated color profile test failed:\n{stdout}\n{}",
|
||
String::from_utf8_lossy(&output.stderr),
|
||
);
|
||
return;
|
||
}
|
||
use vectorcraft_color::{Color, Paint, cms};
|
||
let settings = cms::ColorSettings { rgb: cms::WIDE_GAMUT_RGB.into(), ..Default::default() };
|
||
cms::set_active(&settings).unwrap();
|
||
assert!(!cms::active().rgb_is_srgb());
|
||
let mut s = session(1);
|
||
s.edit("RGB source", |d, _| {
|
||
let id = d.layers[0].children().unwrap()[0].id;
|
||
let n = d.node_mut(id).unwrap();
|
||
n.appearance.fill_mut().unwrap().paint = Paint::solid(Color::rgb(0.8, 0.4, 0.2));
|
||
n.appearance.items.retain(|i| i.is_fill());
|
||
n.appearance.effects = vec![vectorcraft_doc::Effect {
|
||
id: "stylize.dropShadow".into(),
|
||
visible: true,
|
||
params: json!({"shade":"darkness","darkness":25,"x":60,"y":0,"blur":0,"opacity":100,"mode":"normal"}),
|
||
}];
|
||
Ok(())
|
||
})
|
||
.unwrap();
|
||
let region = vectorcraft_geom::Rect::new(0.0, 0.0, 200.0, 100.0);
|
||
let expected = vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, region, 1.0, true);
|
||
let text = svg(&mut s, json!({}));
|
||
assert!(text.contains("<image"), "a non-sRGB source must preserve its adjusted rendered colors");
|
||
cms::set_active(&cms::ColorSettings::default()).unwrap();
|
||
let back = vectorcraft_svg::import(&text).unwrap();
|
||
let actual = vectorcraft_render::Renderer::new().render_region(&back, region, 1.0, true);
|
||
for x in 80..115 {
|
||
let (a, b) = (actual.pixel(x, 30), expected.pixel(x, 30));
|
||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 8), "at {x}: {a:?} vs {b:?}");
|
||
}
|
||
}
|