From 54eed6ec4c3704ca58e5369385055debe4f934a5 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 05:22:09 +0700 Subject: [PATCH] Fix #597: an image copied in a browser pastes as the picture, not its address A browser's Copy Image puts the picture on the clipboard with the image's address as text (and HTML). Paste takes text before bitmaps, as word processors offer a picture of copied text, so it pasted the address. - `clipboard::is_address`: text that is only an address (http, https, file, ftp, data or blob). - Desktop paste (Windows, macOS, Linux): such text gives way to the clipboard's bitmap when it has one. An address alone pastes as text, as before; nothing is fetched. - Web: a paste whose text is only an address takes its picture file. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- apps/vectorcraft-web/src/web.rs | 5 +++-- crates/engine/src/cmd/clipboard.rs | 21 ++++++++++++++++++++- crates/engine/src/cmd/clipboard/flavours.rs | 9 +++++++++ crates/ui-egui/src/sysclip.rs | 4 +++- crates/ui-egui/src/tests_sysclip.rs | 16 ++++++++++++++++ 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 7a24fc4a..872564fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372), an image copied in a browser pasting as the picture, not its address (#597); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/apps/vectorcraft-web/src/web.rs b/apps/vectorcraft-web/src/web.rs index cba1b748..feeb9296 100644 --- a/apps/vectorcraft-web/src/web.rs +++ b/apps/vectorcraft-web/src/web.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use std::sync::{Arc, Mutex}; use vectorcraft_engine::Session; -use vectorcraft_engine::cmd::clipboard::{FILE_HEAD, Flavour, PASTE_ORDER, file_flavour}; +use vectorcraft_engine::cmd::clipboard::{FILE_HEAD, Flavour, PASTE_ORDER, file_flavour, is_address}; use vectorcraft_engine::cmd::fileio; use vectorcraft_engine::cmd::recovery::{self, Hold, RecoveryStore}; use vectorcraft_ui_egui::graphics::GraphicsLoss; @@ -200,7 +200,8 @@ fn track_paste(pasted: &Rc>) { let on_paste = wasm_bindgen::closure::Closure::::new(move |e: web_sys::ClipboardEvent| { let Some(data) = e.clipboard_data() else { return }; let Some(files) = data.files().filter(|f| f.length() > 0) else { return }; - if data.get_data("text").is_ok_and(|t| !t.is_empty()) { + // Text is egui's, but for the address a browser's Copy Image puts next to the picture. + if data.get_data("text").is_ok_and(|t| !t.is_empty() && !is_address(&t)) { return; } e.prevent_default(); diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index 4cae4ecb..aaf8d5af 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -21,7 +21,7 @@ use vectorcraft_color::Swatch; use vectorcraft_doc::{Document, GraphicStyle, ImageBlob, Node, NodeId, PatternDef, Symbol, TextStyleDef}; use vectorcraft_geom::Rect; -pub use flavours::{BITMAP, EMF, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour}; +pub use flavours::{BITMAP, EMF, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address}; pub(crate) use resources::SwatchChoices; use super::*; @@ -220,6 +220,25 @@ mod tests { s } + /// The address a browser's Copy Image puts next to the picture (#597), and text that isn't one. + #[test] + fn an_address_alone_is_told_from_other_text() { + for a in [ + "https://example.com/cat.png", + " http://x.org/a?b=1 +", + "HTTPS://EXAMPLE.COM", + "file:///C:/art/cat.png", + "data:image/png;base64,iVBOR", + "blob:https://x.org/1", + ] { + assert!(is_address(a), "{a}"); + } + for t in ["Pasted words", "see https://example.com", "https://a.org https://b.org", "https://", "example.com/cat.png", ""] { + assert!(!is_address(t), "{t}"); + } + } + #[test] fn copy_exports_svg_that_round_trips_through_paste() { let mut s = session(); diff --git a/crates/engine/src/cmd/clipboard/flavours.rs b/crates/engine/src/cmd/clipboard/flavours.rs index 7fdedc4a..95ee15ee 100644 --- a/crates/engine/src/cmd/clipboard/flavours.rs +++ b/crates/engine/src/cmd/clipboard/flavours.rs @@ -30,6 +30,15 @@ pub const EMF: &str = "image/emf"; /// What Paste reads from other apps, best first: vector art before text, text before bitmaps /// (word processors offer a picture of copied text too). pub const PASTE_ORDER: [&str; 5] = [SVG, PDF, EMF, TEXT, BITMAP]; +/// Is `text` only an address (a web page's, an image's, a file's)? A browser's Copy Image puts the +/// image's address as text next to the picture: then the picture is what Paste takes (#597). An +/// address alone pastes as text, and nothing is fetched. +pub fn is_address(text: &str) -> bool { + const SCHEMES: [&str; 6] = ["http://", "https://", "file://", "ftp://", "data:", "blob:"]; + let t = text.trim(); + !t.chars().any(char::is_whitespace) && SCHEMES.iter().any(|s| t.get(..s.len()).is_some_and(|h| h.eq_ignore_ascii_case(s)) && t.len() > s.len()) +} + /// How much of a copied file [`file_flavour`] needs to tell what it is. pub const FILE_HEAD: usize = 4096; diff --git a/crates/ui-egui/src/sysclip.rs b/crates/ui-egui/src/sysclip.rs index aefd3b86..3d150748 100644 --- a/crates/ui-egui/src/sysclip.rs +++ b/crates/ui-egui/src/sysclip.rs @@ -8,7 +8,7 @@ //! reads from a paste itself ([`VectorcraftApp::paste_from_host`]). use serde_json::{Value, json}; -use vectorcraft_engine::cmd::clipboard::{EMF, Flavour, PASTE_ORDER, PDF, SVG, TEXT, looks_like_svg}; +use vectorcraft_engine::cmd::clipboard::{BITMAP, EMF, Flavour, PASTE_ORDER, PDF, SVG, TEXT, is_address, looks_like_svg}; use crate::VectorcraftApp; @@ -90,6 +90,8 @@ impl VectorcraftApp { return Ok(()); } let Some(f) = cb.read(&PASTE_ORDER) else { return Ok(()) }; + // Text that is only an address goes with a browser's copied picture: the picture. + let f = if f.mime == TEXT && is_address(&String::from_utf8_lossy(&f.data)) { cb.read(&[BITMAP]).unwrap_or(f) } else { f }; import_command(&f, center) } // Text only: SVG markup is art, other text is left alone. diff --git a/crates/ui-egui/src/tests_sysclip.rs b/crates/ui-egui/src/tests_sysclip.rs index 3b793843..72d1ce6b 100644 --- a/crates/ui-egui/src/tests_sysclip.rs +++ b/crates/ui-egui/src/tests_sysclip.rs @@ -167,6 +167,22 @@ fn text_from_another_app_pastes_as_point_text_and_svg_text_as_art() { assert_eq!(count(&app), before); } +/// A browser's Copy Image: the picture with its address as text (and HTML, which Paste doesn't +/// read). The picture is pasted; an address alone pastes as text, nothing is fetched (#597). +#[test] +fn an_image_copied_in_a_browser_pastes_as_the_picture_not_its_address() { + let (mut app, board) = app(); + let url = b"https://example.com/images/cat.png".to_vec(); + copy_elsewhere(&board, vec![(TEXT, url.clone()), ("text/html", b"".to_vec()), (PNG, blue_png())]); + run(&mut app, "edit.paste", json!({})); + let NodeKind::Image(im) = &pasted(&app)[0].kind else { panic!("not an image: {:?}", pasted(&app)[0].kind) }; + assert!(im.link.is_none() && (im.width, im.height) == (4, 2)); + copy_elsewhere(&board, vec![(TEXT, url)]); + run(&mut app, "edit.paste", json!({})); + let NodeKind::Text(t) = &pasted(&app)[0].kind else { panic!("not type") }; + assert_eq!(t.plain_text(), "https://example.com/images/cat.png"); +} + #[test] fn a_pdf_from_another_app_pastes_as_vectors_ahead_of_its_bitmap() { let (mut app, board) = app();