From 15cd642be80a4abbed14fbbdb4afe14a05ce66f1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 2 Nov 2026 20:31:28 -0500 Subject: [PATCH] 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. --- crates/engine/src/cmd/clipboard/flavours.rs | 14 ++++++++++++-- crates/engine/src/tests/clipflavours.rs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/clipboard/flavours.rs b/crates/engine/src/cmd/clipboard/flavours.rs index 8d9fe0b3f..1a088cf3e 100644 --- a/crates/engine/src/cmd/clipboard/flavours.rs +++ b/crates/engine/src/cmd/clipboard/flavours.rs @@ -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 { 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))) } }; diff --git a/crates/engine/src/tests/clipflavours.rs b/crates/engine/src/tests/clipflavours.rs index fd72f2980..c490c2684 100644 --- a/crates/engine/src/tests/clipflavours.rs +++ b/crates/engine/src/tests/clipflavours.rs @@ -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();