mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2575 from 00xJS/fix/2213-large-image-clipboard-text
Fix #2213: a copy with a large image no longer puts its SVG on the clipboard as text
This commit is contained in:
@@ -122,6 +122,13 @@ pub struct Flavour {
|
||||
const COPY_PNG_SIDE: f64 = 4096.0;
|
||||
/// Most bytes of text `clipboard.importText` takes.
|
||||
const MAX_TEXT: usize = 1 << 20;
|
||||
/// Most bytes of images the SVG markup may embed and still go out as plain text. A paste reads the
|
||||
/// clipboard's text first (eframe reads it on every paste key; on Linux and macOS it is the one
|
||||
/// format the app writes, and checking that the clipboard still holds our copy reads it again), so
|
||||
/// a placed photo in it made each paste move megabytes of base64 through the system clipboard on
|
||||
/// the UI thread, and on a GNOME desktop the app stopped responding (#2213). A copy with more image
|
||||
/// data than this offers its SVG under its own type and its PNG, not as text.
|
||||
const COPY_TEXT_IMAGE_BYTES: usize = 1 << 20;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -157,7 +164,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Clipboard Formats",
|
||||
[],
|
||||
None,
|
||||
"{} → {flavours: [mime…]} what Copy offers other apps for the current clipboard, best first, by the Clipboard Handling preferences: text/plain (a type-only copy's text, else the SVG markup with copyAsSvg), image/svg+xml (copyAsSvg), application/pdf (copyAsPdf), image/png (always; PDF and PNG only for objects with an area); the app publishes them on Copy and Cut",
|
||||
"{} → {flavours: [mime…]} what Copy offers other apps for the current clipboard, best first, by the Clipboard Handling preferences: text/plain (a type-only copy's text, else the SVG markup with copyAsSvg when the images it embeds total at most 1 MiB), image/svg+xml (copyAsSvg), application/pdf (copyAsPdf), image/png (always; PDF and PNG only for objects with an area); the app publishes them on Copy and Cut",
|
||||
always,
|
||||
flavours
|
||||
),
|
||||
@@ -263,12 +270,17 @@ impl Clipboard {
|
||||
fileio::encode_with_warnings(&doc, "pdf", &json!({ "preserveEditing": false })).map(Some)
|
||||
}
|
||||
|
||||
/// Bytes of the encoded images the objects show (what their SVG embeds as base64).
|
||||
fn image_bytes(&self) -> usize {
|
||||
self.images.values().fold(0usize, |sum, b| sum.saturating_add(b.bytes.len()))
|
||||
}
|
||||
|
||||
/// The MIME types of [`Self::flavours`], without making them.
|
||||
pub fn flavour_types(&self, svg: bool, pdf: bool) -> Vec<&'static str> {
|
||||
if self.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let text = svg || self.nodes.iter().all(type_only);
|
||||
let text = (svg && self.image_bytes() <= COPY_TEXT_IMAGE_BYTES) || self.nodes.iter().all(type_only);
|
||||
// Art without an area (a lone guide-like line) has no page or picture.
|
||||
let area = self.bounds().is_some_and(|b| b.width() > 0.0 && b.height() > 0.0);
|
||||
[(TEXT, text), (SVG, svg), (PDF, pdf && area), (PNG, area)].into_iter().filter_map(|(m, on)| on.then_some(m)).collect()
|
||||
|
||||
@@ -315,6 +315,53 @@ fn copy_offers_flavours_by_the_preferences() {
|
||||
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();
|
||||
|
||||
+2
-1
@@ -2882,7 +2882,8 @@ They are kept across restarts, in the desktop app and in `vectorcraft-cli` alike
|
||||
|
||||
Besides SVG, Copy offers other apps PNG and PDF of the copied objects, and their text when they are all type.
|
||||
`clipboard.flavours` lists what Copy offers for the current clipboard, best first, by the Clipboard Handling
|
||||
preferences: `text/plain` (the text of a type-only copy, else the SVG markup with `copyAsSvg`), `image/svg+xml`
|
||||
preferences: `text/plain` (the text of a type-only copy, else the SVG markup with `copyAsSvg` when the images it
|
||||
embeds total at most 1 MiB, so a paste never reads a photo back as text, #2213), `image/svg+xml`
|
||||
(`copyAsSvg`), `application/pdf` (`copyAsPdf`) and `image/png` (always; PDF and PNG need objects with an
|
||||
area). Each one has a query command:
|
||||
`clipboard.exportSvg`, `clipboard.exportText` (null unless every copied object is type), `clipboard.exportPdf`
|
||||
|
||||
Reference in New Issue
Block a user