diff --git a/crates/ui-egui/src/app/clipboard.rs b/crates/ui-egui/src/app/clipboard.rs index 3ef59a8af..4b7cc74d9 100644 --- a/crates/ui-egui/src/app/clipboard.rs +++ b/crates/ui-egui/src/app/clipboard.rs @@ -74,8 +74,13 @@ impl SheetApp { /// Cmd/Ctrl+V with a picture and no text on the clipboard (egui's `Event::PasteImage`). pub fn paste_color_image(&mut self, image: &egui::ColorImage) { let [w, h] = image.size; + // A side too large for u32 is too large to paste; check before copying the pixels. + let (w, h) = (u32::try_from(w).unwrap_or(u32::MAX), u32::try_from(h).unwrap_or(u32::MAX)); + if let Err(e) = check_picture_size(w, h) { + return self.alert(e); + } let rgba = image.pixels.iter().flat_map(|c| c.to_srgba_unmultiplied()).collect(); - match u32::try_from(w).ok().zip(u32::try_from(h).ok()).and_then(|(w, h)| image::RgbaImage::from_raw(w, h, rgba)) { + match image::RgbaImage::from_raw(w, h, rgba) { Some(image) => self.paste_picture(image), None => self.alert("The picture on the clipboard can't be read.".into()), } @@ -104,15 +109,21 @@ impl SheetApp { const MAX_PICTURE_SIDE: u32 = 16_384; const MAX_PICTURE_PIXELS: u64 = 64 * 1024 * 1024; -/// PNG bytes of a clipboard picture, refusing sizes that would exhaust memory. -fn encode_png(image: &image::RgbaImage) -> Result, String> { - let (w, h) = image.dimensions(); +/// Refuse empty clipboard pictures and sizes that would exhaust memory. +fn check_picture_size(w: u32, h: u32) -> Result<(), String> { if w == 0 || h == 0 { return Err("The picture on the clipboard is empty.".into()); } if w > MAX_PICTURE_SIDE || h > MAX_PICTURE_SIDE || u64::from(w) * u64::from(h) > MAX_PICTURE_PIXELS { return Err("The picture on the clipboard is too large to paste.".into()); } + Ok(()) +} + +/// PNG bytes of a clipboard picture, refusing sizes that would exhaust memory. +fn encode_png(image: &image::RgbaImage) -> Result, String> { + let (w, h) = image.dimensions(); + check_picture_size(w, h)?; let mut png = Vec::new(); image .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) diff --git a/crates/ui-egui/tests/it/clipboard.rs b/crates/ui-egui/tests/it/clipboard.rs index ee52dc162..38c6ea259 100644 --- a/crates/ui-egui/tests/it/clipboard.rs +++ b/crates/ui-egui/tests/it/clipboard.rs @@ -522,6 +522,15 @@ fn a_malformed_clipboard_picture_is_refused_without_inserting() { h.input_mut().events.push(Event::PasteImage(std::sync::Arc::new(egui::ColorImage::new([0, 0], Vec::new())))); h.run_steps(2); assert!(pictures(&h).is_empty()); + + // Wider than the 16384-pixel limit: refused before its pixels are copied. + dismiss(&mut h); + let wide = egui::ColorImage::new([20_000, 1], vec![egui::Color32::WHITE; 20_000]); + h.input_mut().events.push(Event::PasteImage(std::sync::Arc::new(wide))); + h.run_steps(2); + assert!(pictures(&h).is_empty()); + let message = format!("{:?}", h.state().message); + assert!(message.contains("too large"), "{message}"); } #[test] diff --git a/docs/cell-pictures.md b/docs/cell-pictures.md index 1257476c5..0803128a4 100644 --- a/docs/cell-pictures.md +++ b/docs/cell-pictures.md @@ -19,7 +19,8 @@ The command API accepts the same placement: This initial feature handles static pictures. A picture on the operating system's clipboard (a screenshot, an image copied in a browser) pastes with Cmd/Ctrl+V or Paste as a floating picture -at the active cell, as in Excel. Pasting it into a cell, converting between floating and cell +at the active cell, as in Excel (desktop builds only: the web build's paste receives clipboard +text alone, so a picture with no text pastes nothing there). Pasting it into a cell, converting between floating and cell pictures, and the `IMAGE()` function remain separate work. Formula references and arithmetic do not carry pictures: the calculation engine uses a `#VALUE!` fallback, including when a picture is referenced from another cell. Text-only exports also use that fallback. The picture cell still counts as occupied and diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 76ecc39e0..ae2bf9db6 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -308,7 +308,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md | | 2026-10-12 | minor | Page Layout view: left/right/top/bottom margins are dragged on the rulers (#792) | | 2026-10-12 | minor | Alt+Down opens the active filter header's existing popup, with validation priority and mirrored anchoring on right-to-left sheets (part of #441) | -| 2026-11-03 | minor | A picture on the system clipboard (a screenshot, an image copied in a browser or an image editor) pastes with Cmd/Ctrl+V, the ribbon's Paste and the Paste menu items as a floating picture whose top-left corner is the active cell, as Excel does; text on the clipboard still wins. Oversized or malformed pictures are refused with a message (#699, #3018, #3182) | +| 2026-11-03 | minor | A picture on the system clipboard (a screenshot, an image copied in a browser or an image editor) pastes with Cmd/Ctrl+V, the ribbon's Paste and the Paste menu items as a floating picture whose top-left corner is the active cell, as Excel does; text on the clipboard still wins. Desktop only: the web build's paste receives clipboard text alone. Oversized or malformed pictures are refused with a message (#699, #3018, #3182) | | 2026-10-12 | minor | Paste from the ribbon (button and options menu), the cell and header context menus and the macOS menu bar's Edit menu pastes what is on the system clipboard, as Cmd/Ctrl+V does, instead of only GridCraft's own copy (the web build, which can't read the clipboard on request, still pastes GridCraft's copy). Copy and Cut by keytip (H,C / H,X) publish to the system clipboard like the ribbon buttons. After a control-channel or MCP copy, which leaves the system clipboard alone, Paste pastes that copy until other text is copied. Once a cut has been pasted, Paste (ribbon, menus or Cmd/Ctrl+V) doesn't paste the cut's clipboard text again over the moved cells (Excel pastes a cut once), and the Paste keytip (H,V,P) pastes the system clipboard like the button. Regression test for the Paste button click freeze in 0.5.0 (#273, fixed by #272); the Paste button gets an accessible "Paste" name for screen readers | | 2026-10-11 | minor | Ribbon: scroll arrows on a tab wider than the window, Formulas › AutoSum as a split button with its function menu, View › Show in two columns so Headings is no longer cut off | | 2026-10-11 | minor | Theme tokens for icon, reference, on-accent and sheet-control colours; WCAG 2 contrast test over both themes (dark danger red and selected header text adjusted) |