diff --git a/crates/engine/src/cmd/clipboard/flavours.rs b/crates/engine/src/cmd/clipboard/flavours.rs index 8e4c57d06..b2e1114e1 100644 --- a/crates/engine/src/cmd/clipboard/flavours.rs +++ b/crates/engine/src/cmd/clipboard/flavours.rs @@ -127,6 +127,8 @@ pub struct Flavour { 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; +/// What Copy says when the art is too large for its PNG. +const TOO_LARGE_FOR_PNG: &str = "The art is too large to copy as a picture: other apps get no PNG of it"; /// 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 @@ -290,7 +292,18 @@ impl Clipboard { 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() + [(TEXT, text), (SVG, svg), (PDF, pdf && area), (PNG, area && self.png_scale().is_some())] + .into_iter() + .filter_map(|(m, on)| on.then_some(m)) + .collect() + } + + /// The scale (pixels a point) of the PNG Copy offers: at most [`COPY_PNG_SIDE`] pixels a side, + /// and `None` when even [`MIN_COPY_PNG_SCALE`], the encoder's smallest, can't bring it that small + /// (rather than a PNG of up to a gigabyte of pixels). + fn png_scale(&self) -> Option { + let side = self.bounds().map_or(1.0, |b| b.width().max(b.height())); + Some((COPY_PNG_SIDE / side).min(1.0)).filter(|s| *s >= MIN_COPY_PNG_SCALE) } /// What Copy offers other apps, best first, with Include SVG Code (`svg`) and On Copy: PDF @@ -308,6 +321,9 @@ impl Clipboard { let svg = types.contains(&SVG).then(|| self.svg()).flatten(); let mut out = vec![]; let mut warnings: Vec = vec![]; + if !self.is_empty() && self.png_scale().is_none() { + warnings.push(TOO_LARGE_FOR_PNG.to_string()); + } let mut keep = |(bytes, notes): (Vec, Vec)| { for w in notes { if !warnings.contains(&w) { @@ -321,18 +337,10 @@ impl Clipboard { TEXT => self.text().or_else(|| svg.clone()).map(String::into_bytes), SVG => svg.clone().map(String::into_bytes), 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).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))) - } + _ => self + .png_scale() + .and_then(|scale| self.png(scale).inspect_err(|e| log::warn!("clipboard PNG: {e}")).ok().flatten()) + .map(|(png, _, _, notes)| keep((png, notes))), }; out.extend(data.map(|data| Flavour { mime, data })); } diff --git a/crates/engine/src/tests/clipflavours.rs b/crates/engine/src/tests/clipflavours.rs index c490c2684..313935c21 100644 --- a/crates/engine/src/tests/clipflavours.rs +++ b/crates/engine/src/tests/clipflavours.rs @@ -358,8 +358,10 @@ fn copy_leaves_out_the_png_of_art_too_large_to_scale_down() { 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)); + assert!(!s.clipboard.flavour_types(false, false).contains(&PNG), "not offered either"); + let (flavours, notes) = s.clipboard.flavours_with_warnings(false, false); + assert!(flavours.iter().all(|f| f.mime != PNG)); + assert!(notes.iter().any(|n| n.contains("too large to copy as a picture")), "{notes:?}"); // 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]}));