mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Clipboard: a .svgz file copied in a file manager pastes as its art
Paste takes a file copied in a file manager by its flavor, which file_flavour tells from the file's first 4,096 bytes through fileio::detect. detect names gzip data SVGZ unless it is a compressed native file, and file_flavour gave no flavor for the format id svgz. So a copied .svgz file did not paste as its art, although clipboard.importSvg, the SVG flavor's command, reads SVGZ data. A copied file whose gzip data unpacks to SVG markup now pastes through the SVG flavor. file_flavour unpacks the head it is given with the gzip decoder the SVGZ reader uses, up to where the head ends, and checks the first 4,096 unpacked bytes with looks_like_svg, as detect checks an uncompressed file, reading them as UTF-16 after a UTF-16 byte order mark. Other gzip files get no flavor, as before. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -77,6 +77,9 @@ pub fn file_flavour(name: &str, head: &[u8], mimes: &[&str]) -> Option<&'static
|
||||
let f = fileio::detect(name, head)?;
|
||||
let (asked, mime) = match f.id {
|
||||
"svg" => (SVG, SVG),
|
||||
// `detect` names gzip data SVGZ (a compressed native file aside); `clipboard.importSvg`
|
||||
// reads SVGZ data.
|
||||
"svgz" if unpacks_to_svg(head) => (SVG, SVG),
|
||||
"pdf" => (PDF, PDF),
|
||||
"eps" => (EPS, EPS),
|
||||
"ai" | "ait" if head.starts_with(b"%PDF") => (PDF, PDF),
|
||||
@@ -88,6 +91,25 @@ pub fn file_flavour(name: &str, head: &[u8], mimes: &[&str]) -> Option<&'static
|
||||
mimes.contains(&asked).then_some(mime)
|
||||
}
|
||||
|
||||
/// Does gzip data `head` (a file's first bytes, often a stream cut short) unpack to SVG markup?
|
||||
/// The first [`FILE_HEAD`] bytes it unpacks to are read as UTF-16 after a UTF-16 byte order mark,
|
||||
/// else as UTF-8, and checked with [`looks_like_svg`].
|
||||
fn unpacks_to_svg(head: &[u8]) -> bool {
|
||||
use std::io::Read as _;
|
||||
let mut raw = Vec::with_capacity(FILE_HEAD);
|
||||
// A stream cut short ends in an error after the bytes that unpacked: judge those.
|
||||
let _ = flate2::read::GzDecoder::new(head).take(FILE_HEAD as u64).read_to_end(&mut raw);
|
||||
let utf16 = |rest: &[u8], unit: fn([u8; 2]) -> u16| -> String {
|
||||
char::decode_utf16(rest.as_chunks::<2>().0.iter().map(|u| unit(*u))).map(|c| c.unwrap_or(char::REPLACEMENT_CHARACTER)).collect()
|
||||
};
|
||||
let text = match raw.as_slice() {
|
||||
[0xfe, 0xff, rest @ ..] => utf16(rest, u16::from_be_bytes),
|
||||
[0xff, 0xfe, rest @ ..] => utf16(rest, u16::from_le_bytes),
|
||||
_ => String::from_utf8_lossy(&raw).into_owned(),
|
||||
};
|
||||
looks_like_svg(&text)
|
||||
}
|
||||
|
||||
/// One representation of the clipboard's contents.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Flavour {
|
||||
|
||||
@@ -118,6 +118,66 @@ fn import_image_then_paste_gives_an_image() {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user