mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
398 lines
20 KiB
Rust
398 lines
20 KiB
Rust
//! Clipboard flavours (M4.57): PNG, PDF and plain text out; bitmaps, plain text and PDF in; the
|
||
//! Clipboard Handling preferences.
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_doc::{NodeKind, TextObject};
|
||
use vectorcraft_geom::Point;
|
||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||
|
||
use super::*;
|
||
use crate::cmd::clipboard::{AI, BITMAP, EPS, FILE_HEAD, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour};
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id}: {e}"))
|
||
}
|
||
|
||
/// A red 40 × 40 pt square, copied.
|
||
fn copy_red_square(s: &mut Session) {
|
||
let id = run(s, "shape.rectangle", json!({"x": 10, "y": 20, "width": 40, "height": 40}))["id"].clone();
|
||
run(s, "paint.setFill", json!({"color": "#ff0000"}));
|
||
run(s, "paint.setStroke", json!({"none": true}));
|
||
run(s, "select.set", json!({"ids": [id]}));
|
||
run(s, "edit.copy", json!({}));
|
||
}
|
||
|
||
fn copy_text(s: &mut Session, texts: &[&str]) {
|
||
let ids: Vec<Value> =
|
||
texts.iter().enumerate().map(|(i, t)| run(s, "text.create", json!({"x": 20, "y": 40 + 30 * i, "text": t}))["id"].clone()).collect();
|
||
run(s, "select.set", json!({ "ids": ids }));
|
||
run(s, "edit.copy", json!({}));
|
||
}
|
||
|
||
fn decode(v: &Value) -> Vec<u8> {
|
||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap()
|
||
}
|
||
|
||
/// The pasted objects (the selection after a paste).
|
||
fn pasted(s: &Session) -> Vec<vectorcraft_doc::Node> {
|
||
let st = s.doc().unwrap();
|
||
st.selection.objects.iter().map(|id| st.doc.node(*id).unwrap().clone()).collect()
|
||
}
|
||
|
||
fn text_of(n: &vectorcraft_doc::Node) -> &TextObject {
|
||
match &n.kind {
|
||
NodeKind::Text(t) => t,
|
||
k => panic!("not type: {k:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn exported_png_of_a_red_square_is_red() {
|
||
let mut s = session();
|
||
assert!(run(&mut s, "clipboard.exportPng", json!({}))["dataBase64"].is_null(), "nothing copied");
|
||
copy_red_square(&mut s);
|
||
let r = run(&mut s, "clipboard.exportPng", json!({"scale": 2}));
|
||
assert_eq!((r["width"].as_u64(), r["height"].as_u64()), (Some(80), Some(80)));
|
||
let img = image::load_from_memory(&decode(&r)).unwrap().to_rgba8();
|
||
assert_eq!(img.dimensions(), (80, 80));
|
||
assert_eq!(img.get_pixel(40, 40).0, [255, 0, 0, 255]);
|
||
assert_eq!(img.get_pixel(2, 77).0, [255, 0, 0, 255], "cropped to the square");
|
||
for bad in [json!({"scale": 0}), json!({"scale": -1}), json!({"scale": 65})] {
|
||
assert!(s.execute("clipboard.exportPng", &bad).is_err(), "{bad}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn exported_pdf_pastes_back_as_vectors() {
|
||
let mut s = session();
|
||
assert!(run(&mut s, "clipboard.exportPdf", json!({}))["dataBase64"].is_null());
|
||
copy_red_square(&mut s);
|
||
let pdf = run(&mut s, "clipboard.exportPdf", json!({}));
|
||
assert!(decode(&pdf).starts_with(b"%PDF"));
|
||
// Plain PDF: no native document inside (the default PDF preset embeds one).
|
||
let has_native = |b: &[u8]| b.windows(13).any(|w| w == b"/EmbeddedFile");
|
||
assert!(!has_native(&decode(&pdf)));
|
||
let saved = run(&mut s, "document.exportPdf", json!({}));
|
||
assert!(has_native(&decode(&saved)), "the check finds the native document");
|
||
let mut t = session();
|
||
let r = run(&mut t, "clipboard.importPdf", json!({"dataBase64": pdf["dataBase64"], "center": [200, 150]}));
|
||
assert!(r["count"].as_u64().unwrap() >= 1, "{r}");
|
||
run(&mut t, "edit.pasteInPlace", json!({}));
|
||
let st = t.doc().unwrap();
|
||
let b = st.doc.bounds_of(&st.selection.in_paint_order(&st.doc), false).unwrap();
|
||
assert!((b.center().x - 200.0).abs() < 0.5 && (b.center().y - 150.0).abs() < 0.5, "{b:?}");
|
||
assert!((b.width() - 40.0).abs() < 0.5 && (b.height() - 40.0).abs() < 0.5, "{b:?}");
|
||
let mut red = false;
|
||
for n in pasted(&t) {
|
||
n.walk(&mut |c| red |= c.appearance.fill_paint().color().is_some_and(|c| c.to_hex() == "#ff0000"));
|
||
}
|
||
assert!(red, "the square keeps its red fill");
|
||
assert!(t.execute("clipboard.importPdf", &json!({"dataBase64": vectorcraft_format::base64_encode(b"not a pdf")})).is_err());
|
||
assert!(t.execute("clipboard.importPdf", &json!({})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn the_png_and_pdf_say_when_an_image_or_a_font_is_missing() {
|
||
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
|
||
let warnings = |r: &Value| -> Vec<String> { r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).map(str::to_string).collect() };
|
||
let mut s = session();
|
||
copy_red_square(&mut s);
|
||
assert_eq!(run(&mut s, "clipboard.exportPng", json!({}))["warnings"], json!([]));
|
||
assert_eq!(run(&mut s, "clipboard.exportPdf", json!({}))["warnings"], json!([]));
|
||
// Type in a family that isn't installed: the PNG and the PDF each name it.
|
||
let text = run(&mut s, "text.create", json!({"x": 5, "y": 20, "text": "Ab", "font": "Nonexistent Sans Probe"}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [text]}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
let png = warnings(&run(&mut s, "clipboard.exportPng", json!({})));
|
||
assert!(png.iter().any(|w| w.contains("Nonexistent Sans Probe")), "{png:?}");
|
||
let pdf = warnings(&run(&mut s, "clipboard.exportPdf", json!({})));
|
||
assert!(pdf.iter().any(|w| w.contains("Nonexistent Sans Probe")), "{pdf:?}");
|
||
// Copy's formats carry the notes of both, each once.
|
||
let (flavours, notes) = s.clipboard.flavours_with_warnings(false, true);
|
||
assert_eq!(flavours.iter().map(|f| f.mime).collect::<Vec<_>>(), [TEXT, PDF, PNG]);
|
||
for w in png.iter().chain(&pdf) {
|
||
assert_eq!(notes.iter().filter(|n| *n == w).count(), 1, "{w} in {notes:?}");
|
||
}
|
||
// An image the renderer can't decode: the PNG leaves it out and says so.
|
||
let image = crate::tests_menucmds::place_undecodable_png(&mut s);
|
||
run(&mut s, "select.set", json!({"ids": [image.0]}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
assert_eq!(warnings(&run(&mut s, "clipboard.exportPng", json!({}))), [NOTE]);
|
||
let (flavours, notes) = s.clipboard.flavours_with_warnings(false, false);
|
||
assert_eq!(flavours.iter().map(|f| f.mime).collect::<Vec<_>>(), [PNG]);
|
||
assert_eq!(notes, [NOTE]);
|
||
}
|
||
|
||
#[test]
|
||
fn import_image_then_paste_gives_an_image() {
|
||
let mut s = session();
|
||
copy_red_square(&mut s);
|
||
let png = run(&mut s, "clipboard.exportPng", json!({}));
|
||
let r = run(&mut s, "clipboard.importImage", json!({"dataBase64": png["dataBase64"], "mime": "image/png", "center": [100, 100]}));
|
||
assert_eq!((r["count"].as_u64(), r["pixelWidth"].as_u64(), r["width"].as_f64()), (Some(1), Some(40), Some(40.0)));
|
||
let r = run(&mut s, "edit.paste", json!({"center": [300, 200]}));
|
||
assert_eq!(r["ids"].as_array().unwrap().len(), 1);
|
||
let n = &pasted(&s)[0];
|
||
let NodeKind::Image(im) = &n.kind else { panic!("not an image: {:?}", n.kind) };
|
||
assert!(im.link.is_none(), "embedded");
|
||
assert!(s.doc().unwrap().doc.images.contains_key(&im.key), "the blob came along");
|
||
let b = n.geometric_bounds().unwrap();
|
||
assert!((b.center().x - 300.0).abs() < 1e-6 && (b.center().y - 200.0).abs() < 1e-6 && (b.width() - 40.0).abs() < 1e-6, "{b:?}");
|
||
// Not a bitmap.
|
||
assert!(s.execute("clipboard.importImage", &json!({"dataBase64": png["dataBase64"], "mime": "image/svg+xml"})).is_err());
|
||
assert!(s.execute("clipboard.importImage", &json!({"dataBase64": vectorcraft_format::base64_encode(b"hello")})).is_err());
|
||
assert!(s.execute("clipboard.importImage", &json!({"dataBase64": "!!"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn copied_svgz_files_paste_as_svg() {
|
||
let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"><rect width=\"40\" height=\"40\" fill=\"#ff0000\"/></svg>";
|
||
let svgz = vectorcraft_svg::compress(svg);
|
||
let of = |name: &str, bytes: &[u8]| file_flavour(name, &bytes[..bytes.len().min(FILE_HEAD)], &PASTE_ORDER);
|
||
assert_eq!(of("art.svgz", &svgz), Some(SVG));
|
||
assert_eq!(of("art.gz", &svgz), Some(SVG), "the content tells");
|
||
assert_eq!(file_flavour("art.svgz", &svgz, &[BITMAP]), None);
|
||
// A file larger than FILE_HEAD: the head is a gzip stream cut short.
|
||
let mut seed = 1u32;
|
||
let mut big = String::from("<?xml version=\"1.0\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\">");
|
||
for _ in 0..2000 {
|
||
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||
big += &format!("<circle cx=\"{}\" cy=\"{}\" r=\"{}\"/>", seed >> 20, (seed >> 8) & 0xfff, seed & 0xff);
|
||
}
|
||
big += "</svg>";
|
||
// UTF-16 after a byte order mark, as SVG Options' UTF-16 encoding writes it.
|
||
let utf16 =
|
||
|text: &str, unit: fn(u16) -> [u8; 2], bom: [u8; 2]| -> Vec<u8> { bom.into_iter().chain(text.encode_utf16().flat_map(unit)).collect() };
|
||
assert_eq!(of("be.svgz", &vectorcraft_svg::compress_bytes(&utf16(svg, u16::to_be_bytes, [0xfe, 0xff]))), Some(SVG));
|
||
assert_eq!(of("le.svgz", &vectorcraft_svg::compress_bytes(&utf16(svg, u16::to_le_bytes, [0xff, 0xfe]))), Some(SVG));
|
||
// Stored without compression, the head unpacks to fewer than FILE_HEAD bytes (an odd number)
|
||
// before it ends.
|
||
let stored = |bytes: &[u8]| {
|
||
let mut gz = flate2::write::GzEncoder::new(vec![], flate2::Compression::none());
|
||
std::io::Write::write_all(&mut gz, bytes).unwrap();
|
||
gz.finish().unwrap()
|
||
};
|
||
assert_eq!(of("stored.svgz", &stored(big.as_bytes())), Some(SVG));
|
||
assert_eq!(of("stored16.svgz", &stored(&utf16(&big, u16::to_be_bytes, [0xfe, 0xff]))), Some(SVG));
|
||
let big = vectorcraft_svg::compress(&big);
|
||
assert!(big.len() > 2 * FILE_HEAD, "{}", big.len());
|
||
assert_eq!(of("big.svgz", &big), Some(SVG));
|
||
// Other gzip data, whole or cut short, pastes as nothing.
|
||
let notes = vectorcraft_svg::compress("plain text, not SVG");
|
||
assert_eq!(of("notes.txt.gz", ¬es), None);
|
||
assert_eq!(of("notes.svgz", ¬es), None);
|
||
let mut text = String::new();
|
||
for i in 0..3000 {
|
||
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||
text += &format!("line {i}: {seed}\n");
|
||
}
|
||
let text = vectorcraft_svg::compress(&text);
|
||
assert!(text.len() > FILE_HEAD, "{}", text.len());
|
||
assert_eq!(of("log.gz", &text), None);
|
||
// Cut within the gzip header (10 bytes) or at its end: nothing unpacks.
|
||
for cut in [2, 5, 10] {
|
||
assert_eq!(of("art.svgz", &svgz[..cut]), None, "{cut} bytes");
|
||
}
|
||
// The SVG flavor's command reads the SVGZ bytes: the red square pastes.
|
||
let mut s = session();
|
||
let r = run(&mut s, "clipboard.importSvg", json!({"dataBase64": vectorcraft_format::base64_encode(&svgz), "center": [200, 150]}));
|
||
assert_eq!(r["count"], 1, "{r}");
|
||
run(&mut s, "edit.paste", json!({}));
|
||
let n = &pasted(&s)[0];
|
||
assert_eq!(n.appearance.fill_paint().color().map(|c| c.to_hex()).as_deref(), Some("#ff0000"));
|
||
let b = n.geometric_bounds().unwrap();
|
||
assert!((b.width() - 40.0).abs() < 1e-6 && (b.height() - 40.0).abs() < 1e-6, "{b:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn copied_files_paste_by_their_content() {
|
||
let mut s = session();
|
||
copy_red_square(&mut s);
|
||
let png = decode(&run(&mut s, "clipboard.exportPng", json!({})));
|
||
let pdf = decode(&run(&mut s, "clipboard.exportPdf", json!({})));
|
||
let mut jpeg = vec![];
|
||
image::RgbImage::new(2, 2).write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg).unwrap();
|
||
let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"4\" height=\"4\"/></svg>";
|
||
let of = |name: &str, bytes: &[u8]| file_flavour(name, &bytes[..bytes.len().min(FILE_HEAD)], &PASTE_ORDER);
|
||
assert_eq!(of("red.png", &png), Some(PNG));
|
||
// The content tells, whatever the name says; bitmaps keep their own type.
|
||
assert_eq!(of("photo.txt", &jpeg), Some("image/jpeg"));
|
||
assert_eq!(of("art.svg", svg), Some(SVG));
|
||
assert_eq!(of("doc.pdf", &pdf), Some(PDF));
|
||
// Files that aren't art paste as nothing (not as their path).
|
||
assert_eq!(of("notes.txt", b"hello"), None);
|
||
assert_eq!(of("archive.zip", b"PK\x03\x04"), None);
|
||
// Only what was asked for.
|
||
assert_eq!(file_flavour("red.png", &png, &[TEXT, SVG]), None);
|
||
assert_eq!(file_flavour("art.svg", svg, &[BITMAP]), None);
|
||
}
|
||
|
||
#[test]
|
||
fn copied_ai_files_paste_as_the_pdf_or_postscript_inside() {
|
||
let mut s = session();
|
||
copy_red_square(&mut s);
|
||
let pdf = decode(&run(&mut s, "clipboard.exportPdf", json!({})));
|
||
let ps = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 40 40\n%%EndComments\n1 0 0 setrgbcolor\nnewpath 0 0 moveto 40 0 lineto 40 40 lineto closepath fill\nshowpage\n";
|
||
let of = |name: &str, bytes: &[u8]| file_flavour(name, &bytes[..bytes.len().min(FILE_HEAD)], &PASTE_ORDER);
|
||
for name in ["art.ai", "art.ait", "ART.AI"] {
|
||
// Asked for as PDF; Paste reads it from its editing data (tests_aiimport).
|
||
assert_eq!(of(name, &pdf), Some(AI), "{name}");
|
||
assert_eq!(of(name, ps), Some(EPS), "{name}");
|
||
}
|
||
// Neither PDF nor PostScript: nothing to paste, whatever the name says.
|
||
assert_eq!(of("art.ai", b"hello"), None);
|
||
// Only what was asked for.
|
||
assert_eq!(file_flavour("art.ai", &pdf, &[EPS]), None);
|
||
assert_eq!(file_flavour("art.ait", ps, &[PDF]), None);
|
||
// clipboard.importEps, the EPS flavor's command, reads the PostScript.
|
||
let r = run(&mut s, "clipboard.importEps", json!({"dataBase64": vectorcraft_format::base64_encode(ps)}));
|
||
assert_eq!(r["count"], 1, "{r}");
|
||
}
|
||
|
||
#[test]
|
||
fn import_text_then_paste_gives_point_text() {
|
||
let mut s = session();
|
||
let r = run(&mut s, "clipboard.importText", json!({"text": "Hello\r\nworld\r\n"}));
|
||
assert_eq!(r["count"], 1);
|
||
run(&mut s, "edit.paste", json!({"center": [200, 150]}));
|
||
let n = &pasted(&s)[0];
|
||
let t = text_of(n);
|
||
assert!(matches!(t.kind, vectorcraft_doc::TextKind::Point));
|
||
assert_eq!(t.plain_text(), "Hello\nworld");
|
||
let b = n.geometric_bounds().unwrap();
|
||
assert!((b.center().x - 200.0).abs() < 1.0 && (b.center().y - 150.0).abs() < 1.0, "{b:?}");
|
||
for bad in [json!({}), json!({"text": " \n\n"}), json!({"text": "x".repeat((1 << 20) + 1)})] {
|
||
assert!(s.execute("clipboard.importText", &bad).is_err());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn type_only_copies_offer_their_text() {
|
||
let mut s = session();
|
||
assert!(run(&mut s, "clipboard.exportText", json!({}))["text"].is_null());
|
||
copy_text(&mut s, &["One", "Two"]);
|
||
assert_eq!(run(&mut s, "clipboard.exportText", json!({}))["text"], "One\nTwo");
|
||
// A group of type is type.
|
||
run(&mut s, "object.group", json!({}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
assert_eq!(run(&mut s, "clipboard.exportText", json!({}))["text"], "One\nTwo");
|
||
// Type with a path is not.
|
||
copy_red_square(&mut s);
|
||
run(&mut s, "select.all", json!({}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
assert!(run(&mut s, "clipboard.exportText", json!({}))["text"].is_null());
|
||
}
|
||
|
||
#[test]
|
||
fn copy_offers_flavours_by_the_preferences() {
|
||
let mut s = session();
|
||
assert!(s.clipboard_flavours().is_empty());
|
||
assert_eq!(run(&mut s, "clipboard.flavours", json!({}))["flavours"], json!([]));
|
||
copy_red_square(&mut s);
|
||
let mimes = |s: &Session| s.clipboard_flavours().iter().map(|f| f.mime).collect::<Vec<_>>();
|
||
// Defaults: Include SVG Code on, PDF off.
|
||
assert_eq!(mimes(&s), [TEXT, SVG, PNG]);
|
||
assert_eq!(run(&mut s, "clipboard.flavours", json!({}))["flavours"], json!([TEXT, SVG, PNG]));
|
||
let f = s.clipboard_flavours();
|
||
assert!(String::from_utf8_lossy(&f[0].data).contains("<svg") && f[0].data == f[1].data);
|
||
assert!(f[2].data.starts_with(b"\x89PNG"));
|
||
run(&mut s, "prefs.set", json!({"key": "copyAsPdf", "value": true}));
|
||
assert_eq!(mimes(&s), [TEXT, SVG, PDF, PNG]);
|
||
assert!(s.clipboard_flavours()[2].data.starts_with(b"%PDF"));
|
||
run(&mut s, "prefs.set", json!({"values": {"copyAsSvg": false, "copyAsPdf": false}}));
|
||
assert_eq!(mimes(&s), [PNG]);
|
||
// Type only: its text is the plain text, even without SVG.
|
||
copy_text(&mut s, &["Words"]);
|
||
assert_eq!(mimes(&s), [TEXT, PNG]);
|
||
assert_eq!(s.clipboard_flavours()[0].data, b"Words");
|
||
run(&mut s, "prefs.set", json!({"key": "copyAsSvg", "value": true}));
|
||
let f = s.clipboard_flavours();
|
||
assert_eq!((f[0].data.as_slice(), f[1].mime), (b"Words".as_slice(), SVG));
|
||
}
|
||
|
||
/// A PNG of `side` × `side` noise pixels: noise keeps it about `side² × 3` bytes, as a photo.
|
||
fn noise_png(side: u32) -> Vec<u8> {
|
||
let mut x: u32 = 0x9e37_79b9;
|
||
let px = (0..side * side * 3)
|
||
.map(|_| {
|
||
x ^= x << 13;
|
||
x ^= x >> 17;
|
||
x ^= x << 5;
|
||
(x >> 24) as u8
|
||
})
|
||
.collect();
|
||
let mut out = vec![];
|
||
image::RgbImage::from_raw(side, side, px).unwrap().write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
|
||
out
|
||
}
|
||
|
||
/// #2213: a placed photo in the plain-text flavour made every paste read megabytes of SVG back
|
||
/// through the system clipboard. Past 1 MiB of embedded images the SVG goes out under its own type
|
||
/// and the PNG only; a small picture, vector art of any size and type keep their text.
|
||
#[test]
|
||
fn a_copy_with_a_large_image_offers_no_svg_text() {
|
||
let mut s = session();
|
||
let mimes = |s: &Session| s.clipboard_flavours().iter().map(|f| f.mime).collect::<Vec<_>>();
|
||
for (side, text) in [(700, false), (64, true)] {
|
||
let png = noise_png(side);
|
||
assert_eq!(png.len() > 1 << 20, !text, "{} bytes", png.len());
|
||
run(&mut s, "clipboard.importImage", json!({"dataBase64": vectorcraft_format::base64_encode(&png)}));
|
||
run(&mut s, "edit.paste", json!({}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
let want: &[&str] = if text { &[TEXT, SVG, PNG] } else { &[SVG, PNG] };
|
||
assert_eq!(mimes(&s), want, "{side} px");
|
||
assert_eq!(run(&mut s, "clipboard.flavours", json!({}))["flavours"], json!(want), "{side} px");
|
||
// The SVG still embeds the picture, for apps that read it.
|
||
let svg = s.clipboard_flavours().into_iter().find(|f| f.mime == SVG).unwrap();
|
||
assert!(String::from_utf8_lossy(&svg.data).contains("data:image/png;base64,"));
|
||
run(&mut s, "edit.clear", json!({}));
|
||
}
|
||
// Vector art of more than 1 MiB of SVG keeps its text: only embedded images count.
|
||
let rects: String =
|
||
(0..30_000).map(|i| format!(r##"<rect x="{}.25" y="{}.75" width="3.5" height="3.5" fill="#{:06x}"/>"##, i % 400, i / 100, i * 977)).collect();
|
||
run(&mut s, "clipboard.importSvg", json!({"svg": format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300">{rects}</svg>"#)}));
|
||
run(&mut s, "edit.paste", json!({}));
|
||
run(&mut s, "edit.copy", json!({}));
|
||
let f = s.clipboard_flavours();
|
||
assert!(f[0].mime == TEXT && f[0].data.len() > 1 << 20, "{} bytes of text", f[0].data.len());
|
||
}
|
||
|
||
/// The Type tool pastes the text it copied with its formatting, unless pasted text is kept plain.
|
||
fn paste_copied_range(plain: bool) -> f64 {
|
||
let mut s = session();
|
||
let v = ViewInfo::default();
|
||
let id = run(&mut s, "text.create", json!({"x": 100, "y": 100, "text": "Big small", "size": 10}))["id"].clone();
|
||
run(&mut s, "text.setRangeStyle", json!({"id": id, "start": 0, "end": 3, "size": 40}));
|
||
if plain {
|
||
run(&mut s, "prefs.set", json!({"key": "pasteTextFormatting", "value": "plain"}));
|
||
}
|
||
s.select_tool("type", v).unwrap();
|
||
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!() };
|
||
let at = t.xf * Point::new(30.0, -3.0);
|
||
for k in [PointerKind::Down, PointerKind::Up] {
|
||
s.pointer(&PointerEvent::new(k, at.x, at.y), v).unwrap();
|
||
}
|
||
s.set_tool_option("select", &json!({"start": 0, "end": 3}));
|
||
let r = run(&mut s, "text.getRange", json!({"id": id, "start": 0, "end": 3}));
|
||
s.set_tool_option("copy", &r["runs"]);
|
||
s.tool_key(ToolKey::End, Mods::default(), v).unwrap();
|
||
s.tool_text("Big", v).unwrap();
|
||
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
||
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||
let t = text_of(&n);
|
||
assert_eq!(t.plain_text(), "Big smallBig");
|
||
t.runs.last().unwrap().style.size
|
||
}
|
||
|
||
#[test]
|
||
fn keep_plain_text_preference_drops_pasted_formatting() {
|
||
assert_eq!(paste_copied_range(false), 40.0, "Keep Formatting");
|
||
assert_eq!(paste_copied_range(true), 10.0, "Keep Plain Text");
|
||
}
|