mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Save for Office Documents, the pictures that css.export rasterizes and the PNG and PDF that Copy offers other apps were encoded with fileio::encode, which drops the encoder's warnings. When the art held an image that the renderer could not decode, the PNGs lacked it; type in a font that is not installed came out in the fallback font in the Office PNG and Copy's PNG and PDF. No reply or status message said so. The app also dropped the warnings that document.exportSelection returns. document.exportForOffice, clipboard.exportPng and clipboard.exportPdf now return `warnings`, and css.export adds the notes of its pictures to its `warnings`, each once. Save for Office Documents and document.exportSelection show the first note in the status bar in Save As's message, and the CSS Properties panel's Export Selected CSS… and Export All… show it in Export As's message. After Copy or Cut, once the PNG and PDF are on the system clipboard, the status bar shows their first note. The files are written as before. Docs: docs/mcp.md (Clipboard, CSS Properties), docs/control-protocol.md.
140 lines
7.0 KiB
Rust
140 lines
7.0 KiB
Rust
//! CSS Properties: `css.selection` (rectangles' backgrounds, borders and radii, type's fonts,
|
|
//! gradients), `css.generate` over the document and `css.export` writing the style sheet and the
|
|
//! pictures of rasterized art.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::*;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
|
s
|
|
}
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
|
|
fn css(v: &Value) -> &str {
|
|
v["css"].as_str().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn a_rounded_rectangle_gives_background_border_and_radius() {
|
|
let mut s = session();
|
|
let id = run(&mut s, "shape.rectangle", json!({"x": 10, "y": 20, "width": 120, "height": 60, "radius": 6}))["id"].as_u64().unwrap();
|
|
run(&mut s, "paint.setFill", json!({"color": "#3366cc"}));
|
|
run(&mut s, "paint.setStroke", json!({"color": "#000000"}));
|
|
run(&mut s, "layer.setProps", json!({"id": id, "name": "Button"}));
|
|
let r = run(&mut s, "css.selection", json!({"position": true}));
|
|
let text = css(&r);
|
|
for decl in ["background-color: #3366cc;", "border: 1px solid #000000;", "border-radius: 6px;", "left: 10px;", "top: 20px;", "width: 120px;"] {
|
|
assert!(text.contains(decl), "{decl} in {text}");
|
|
}
|
|
assert!(text.starts_with(".Button {\n"), "{text}");
|
|
assert_eq!(r["rules"][0]["id"], json!(id));
|
|
assert_eq!(r["rules"][0]["selector"], json!(".Button"));
|
|
assert!(r["rules"][0].get("unsupported").is_none());
|
|
// A query: no undo step, no change.
|
|
let rev = s.doc().unwrap().revision;
|
|
run(&mut s, "css.selection", json!({"units": "mm"}));
|
|
assert_eq!(s.doc().unwrap().revision, rev);
|
|
assert!(s.execute("css.selection", &json!({"units": "furlong"})).is_err(), "unknown units are refused");
|
|
}
|
|
|
|
#[test]
|
|
fn type_gives_font_properties_and_gradients_css_gradients() {
|
|
let mut s = session();
|
|
let t = run(&mut s, "text.create", json!({"x": 20, "y": 50, "text": "Title", "size": 24}))["id"].as_u64().unwrap();
|
|
let text = css(&run(&mut s, "css.selection", json!({"ids": [t]}))).to_string();
|
|
for decl in ["font-family: 'Source Sans 3';", "font-size: 24px;", "color: #"] {
|
|
assert!(text.contains(decl), "{decl} in {text}");
|
|
}
|
|
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 100, "width": 100, "height": 50}));
|
|
run(&mut s, "paint.setFill", json!({"gradient": {"kind": "linear"}}));
|
|
let text = css(&run(&mut s, "css.selection", json!({}))).to_string();
|
|
assert!(text.contains("background-image: linear-gradient(90deg, "), "{text}");
|
|
}
|
|
|
|
#[test]
|
|
fn generate_covers_the_document_and_an_empty_selection_has_none() {
|
|
let mut s = session();
|
|
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
|
run(&mut s, "shape.ellipse", json!({"x": 20, "y": 0, "width": 10, "height": 10}));
|
|
run(&mut s, "select.none", json!({}));
|
|
let none = run(&mut s, "css.selection", json!({}));
|
|
assert_eq!(css(&none), "");
|
|
let all = run(&mut s, "css.generate", json!({}));
|
|
let sel: Vec<&str> = all["rules"].as_array().unwrap().iter().map(|r| r["selector"].as_str().unwrap()).collect();
|
|
assert_eq!(sel, [".rectangle", ".ellipse"]);
|
|
assert!(css(&all).contains("border-radius: 50%;"), "{}", css(&all));
|
|
let named = run(&mut s, "css.generate", json!({"unnamed": false}));
|
|
assert_eq!((named["rules"].as_array().unwrap().len(), named["skipped"].as_u64()), (0, Some(2)));
|
|
}
|
|
|
|
#[test]
|
|
fn export_writes_the_sheet_and_rasterized_art() {
|
|
let mut s = session();
|
|
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 40}));
|
|
run(&mut s, "shape.star", json!({"cx": 100, "cy": 100, "radius1": 40, "radius2": 20}));
|
|
// Without a path: the CSS as data, the pictures as base64.
|
|
let r = run(&mut s, "css.export", json!({"scope": "all", "rasterize": true}));
|
|
let data = r["data"].as_str().unwrap();
|
|
assert!(data.contains(".rectangle {") && data.contains(".path {\n") && data.contains("background-image: url(path.png);"), "{data}");
|
|
assert!(data.ends_with("}\n"));
|
|
assert_eq!(r["rules"], json!(2));
|
|
let images = r["images"].as_array().unwrap();
|
|
assert_eq!(images.len(), 1, "only the star is rasterized");
|
|
assert_eq!(images[0]["name"], json!("path.png"));
|
|
let png = vectorcraft_format::base64_decode(images[0]["dataBase64"].as_str().unwrap()).unwrap();
|
|
assert!(png.starts_with(b"\x89PNG"));
|
|
// The selection alone (the star, selected last).
|
|
let r = run(&mut s, "css.export", json!({}));
|
|
assert_eq!(r["rules"], json!(1));
|
|
assert!(r["images"].as_array().unwrap().is_empty(), "no rasterize: no pictures");
|
|
assert!(s.execute("css.export", &json!({"scope": "page"})).is_err());
|
|
run(&mut s, "select.none", json!({}));
|
|
assert!(s.execute("css.export", &json!({})).is_err(), "nothing selected: nothing to export");
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn export_to_a_path_puts_the_pictures_next_to_the_sheet() {
|
|
let mut s = session();
|
|
run(&mut s, "shape.star", json!({"cx": 100, "cy": 100, "radius1": 40, "radius2": 20}));
|
|
let dir = std::env::temp_dir().join(format!("vc-css-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("site.css");
|
|
let r = run(&mut s, "css.export", json!({"path": path.to_string_lossy(), "rasterize": true}));
|
|
let written = std::fs::read_to_string(&path).unwrap();
|
|
assert!(written.contains("url(path.png)"), "{written}");
|
|
assert_eq!(r["bytes"], json!(written.len()));
|
|
let png = dir.join("path.png");
|
|
assert_eq!(r["images"], json!([png.to_string_lossy()]));
|
|
assert!(std::fs::read(&png).unwrap().starts_with(b"\x89PNG"));
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn export_says_once_when_a_rasterized_image_could_not_be_decoded() {
|
|
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
|
|
let notes = |r: &Value| r["warnings"].as_array().unwrap().iter().filter(|w| w.as_str() == Some(NOTE)).count();
|
|
let mut s = session();
|
|
run(&mut s, "shape.star", json!({"cx": 100, "cy": 100, "radius1": 40, "radius2": 20}));
|
|
assert_eq!(notes(&run(&mut s, "css.export", json!({"scope": "all", "rasterize": true}))), 0);
|
|
// Two images the renderer can't decode, each rasterized to a picture of its own.
|
|
crate::tests_menucmds::place_undecodable_png(&mut s);
|
|
crate::tests_menucmds::place_undecodable_png(&mut s);
|
|
let r = run(&mut s, "css.export", json!({"scope": "all", "rasterize": true}));
|
|
assert_eq!(r["images"].as_array().unwrap().len(), 3, "{r}");
|
|
assert_eq!(notes(&r), 1, "{}", r["warnings"]);
|
|
let dir = std::env::temp_dir().join(format!("vc-css-undecoded-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let r = run(&mut s, "css.export", json!({"path": dir.join("site.css").to_string_lossy(), "scope": "all", "rasterize": true}));
|
|
assert_eq!(r["images"].as_array().unwrap().len(), 3, "the pictures are still written");
|
|
assert_eq!(notes(&r), 1, "{}", r["warnings"]);
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|