Clipboard: leave out the PNG of art too large to scale to 4096 pixels

Copy scaled its PNG down to fit 4096 pixels a side but never below 0.01
pixels a point, so art over 409,600 pt across produced a PNG far past
that side, up to the general raster export limit of a gigabyte of
pixels, on an ordinary copy. Such art now gets no PNG flavour.
This commit is contained in:
Dylan M. Taylor
2026-11-03 15:57:05 +07:00
committed by bflatastic
parent 400a49666a
commit 15cd642be8
2 changed files with 31 additions and 2 deletions
+12 -2
View File
@@ -125,6 +125,8 @@ pub struct Flavour {
/// Longest side (px) of the PNG Copy offers: larger art is scaled down to fit.
const COPY_PNG_SIDE: f64 = 4096.0;
/// The smallest scale (pixels a point) the PNG encoder renders at.
const MIN_COPY_PNG_SCALE: f64 = 0.01;
/// 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
@@ -293,7 +295,8 @@ impl Clipboard {
/// What Copy offers other apps, best first, with Include SVG Code (`svg`) and On Copy: PDF
/// (`pdf`) as given (see `clipboard.flavours`). A [`Flavour`] that can't be made (art too large
/// to render) is left out; the PNG is scaled down to at most [`COPY_PNG_SIDE`] pixels a side.
/// to render) is left out; the PNG is scaled down to at most [`COPY_PNG_SIDE`] pixels a side, and
/// left out when even [`MIN_COPY_PNG_SCALE`] can't bring it that small.
pub fn flavours(&self, svg: bool, pdf: bool) -> Vec<Flavour> {
self.flavours_with_warnings(svg, pdf).0
}
@@ -320,7 +323,14 @@ impl Clipboard {
PDF => self.pdf().inspect_err(|e| log::warn!("clipboard PDF: {e}")).ok().flatten().map(&mut keep),
_ => {
let side = self.bounds().map_or(1.0, |b| b.width().max(b.height()));
let scale = (COPY_PNG_SIDE / side).clamp(0.01, 1.0);
let scale = (COPY_PNG_SIDE / side).min(1.0);
// The encoder renders at no fewer than MIN_COPY_PNG_SCALE pixels a point: art
// too large to fit COPY_PNG_SIDE at that scale gets no PNG, rather than one
// of up to a gigabyte of pixels.
if !(scale >= MIN_COPY_PNG_SCALE) {
log::warn!("clipboard PNG: art {side} pt across is too large to copy as a picture");
continue;
}
self.png(scale).inspect_err(|e| log::warn!("clipboard PNG: {e}")).ok().flatten().map(|(png, _, _, notes)| keep((png, notes)))
}
};
+19
View File
@@ -350,6 +350,25 @@ fn type_only_copies_offer_their_text() {
assert!(run(&mut s, "clipboard.exportText", json!({}))["text"].is_null());
}
/// Art too large to fit the clipboard's PNG side at the encoder's smallest scale gets no PNG,
/// instead of a picture hundreds of megabytes large.
#[test]
fn copy_leaves_out_the_png_of_art_too_large_to_scale_down() {
let mut s = session();
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 1_000_000, "height": 1_000_000}))["id"].clone();
run(&mut s, "select.set", json!({"ids": [id]}));
run(&mut s, "edit.copy", json!({}));
assert!(s.clipboard.flavour_types(false, false).contains(&PNG));
assert!(s.clipboard.flavours(false, false).iter().all(|f| f.mime != PNG));
// Art that fits at that scale still gets its PNG, at most 4096 pixels a side.
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 200_000, "height": 100}))["id"].clone();
run(&mut s, "select.set", json!({"ids": [id]}));
run(&mut s, "edit.copy", json!({}));
let png = s.clipboard.flavours(false, false).into_iter().find(|f| f.mime == PNG).expect("a PNG");
let img = image::load_from_memory(&png.data).unwrap();
assert!(img.width() <= 4096 && img.height() <= 4096, "{}x{}", img.width(), img.height());
}
#[test]
fn copy_offers_flavours_by_the_preferences() {
let mut s = session();