diff --git a/apps/vectorcraft/src/clipboard.rs b/apps/vectorcraft/src/clipboard.rs
--- a/apps/vectorcraft/src/clipboard.rs
+++ b/apps/vectorcraft/src/clipboard.rs
@@ -17,7 +17,7 @@ use std::io::{Cursor, Read};
 use std::path::PathBuf;
 
 use vectorcraft_engine::cmd::clipboard::{BITMAP, EPS, FILE_HEAD, Flavour, PDF, PNG, SVG, TEXT, file_flavour};
-use vectorcraft_ui_egui::SystemClipboard;
+use vectorcraft_ui_egui::{ClipboardRead, SystemClipboard};
 
 /// This platform's system clipboard.
 pub fn system_clipboard() -> Box<dyn SystemClipboard> {
@@ -240,10 +240,16 @@ mod win {
             // A clipboard manager may have copied our data again as its own.
             let Some(ours) = self.text.as_deref() else { return false };
             let Ok(_open) = clipboard_win::Clipboard::new_attempts(10) else { return false };
-            read_one(TEXT).is_some_and(|f| f.data == ours.as_bytes())
+            read_one(TEXT).and_then(ClipboardRead::flavour).is_some_and(|f| f.data == ours.as_bytes())
         }
 
         fn read(&mut self, mimes: &[&'static str]) -> Option<Flavour> {
+            self.read_deferred(mimes)?.flavour()
+        }
+
+        /// A device-independent bitmap is copied off the clipboard here and decoded and encoded
+        /// as PNG by the work that comes back.
+        fn read_deferred(&mut self, mimes: &[&'static str]) -> Option<ClipboardRead> {
             let mut files = vec![];
             {
                 let _open = clipboard_win::Clipboard::new_attempts(10).ok()?;
@@ -251,7 +257,7 @@ mod win {
                 let _ = raw::get_file_list_path(&mut files);
             }
             // The files are read with the clipboard closed again, not keeping other apps waiting.
-            copied_file(files, mimes).or_else(|| {
+            copied_file(files, mimes).map(ClipboardRead::Now).or_else(|| {
                 let _open = clipboard_win::Clipboard::new_attempts(10).ok()?;
                 mimes.iter().find_map(|m| read_one(m))
             })
@@ -275,17 +281,39 @@ mod win {
         })
     }
 
-    /// One flavour, while the clipboard is open.
-    fn read_one(mime: &'static str) -> Option<Flavour> {
+    /// A device-independent bitmap as a PNG flavour.
+    fn dib_png(dib: &[u8]) -> Option<Flavour> {
+        png_flavour(&dib_image(dib)?)
+    }
+
+    /// One flavour, while the clipboard is open. A device-independent bitmap is copied here, and
+    /// the work that comes back decodes it. When it can't be decoded, that work reads the next of
+    /// [`DIBS`] the clipboard holds, unless something was copied since.
+    fn read_one(mime: &'static str) -> Option<ClipboardRead> {
         let mut data = vec![];
         let mime = match mime {
             BITMAP if read_registered(PNG, &mut data) => PNG,
             BITMAP => {
-                return DIBS.into_iter().find_map(|id| {
-                    data.clear();
-                    raw::get_vec(id, &mut data).ok()?;
-                    png_flavour(&dib_image(&data)?)
-                });
+                let seq = raw::seq_num();
+                let (at, dib) = DIBS.into_iter().enumerate().find_map(|(at, id)| {
+                    let mut dib = vec![];
+                    raw::get_vec(id, &mut dib).ok()?;
+                    Some((at, dib))
+                })?;
+                let rest = DIBS.get(at + 1..).unwrap_or_default().to_vec();
+                return Some(ClipboardRead::Later(Box::new(move || {
+                    dib_png(&dib).or_else(|| {
+                        let _open = clipboard_win::Clipboard::new_attempts(10).ok()?;
+                        if raw::seq_num() != seq {
+                            return None;
+                        }
+                        rest.into_iter().find_map(|id| {
+                            let mut dib = vec![];
+                            raw::get_vec(id, &mut dib).ok()?;
+                            dib_png(&dib)
+                        })
+                    })
+                })));
             }
             TEXT => {
                 raw::get_string(&mut data).ok()?;
@@ -300,7 +328,7 @@ mod win {
         {
             data.truncate(end);
         }
-        (!data.is_empty()).then_some(Flavour { mime, data })
+        (!data.is_empty()).then_some(ClipboardRead::Now(Flavour { mime, data }))
     }
 }
 
@@ -370,22 +398,27 @@ mod portable {
         }
 
         fn read(&mut self, mimes: &[&'static str]) -> Option<Flavour> {
+            self.read_deferred(mimes)?.flavour()
+        }
+
+        /// arboard's image is read here and encoded as PNG by the work that comes back.
+        fn read_deferred(&mut self, mimes: &[&'static str]) -> Option<ClipboardRead> {
             let cb = self.cb().ok()?;
             if let Some(f) = cb.get().file_list().ok().and_then(|paths| copied_file(paths, mimes)) {
-                return Some(f);
+                return Some(ClipboardRead::Now(f));
             }
             mimes.iter().find_map(|m| {
                 #[cfg(target_os = "macos")]
                 if let Some(f) = super::mac::read(m) {
-                    return Some(f);
+                    return Some(ClipboardRead::Now(f));
                 }
                 match *m {
-                    TEXT => cb.get_text().ok().filter(|t| !t.is_empty()).map(|t| Flavour { mime: TEXT, data: t.into_bytes() }),
+                    TEXT => cb.get_text().ok().filter(|t| !t.is_empty()).map(|t| ClipboardRead::Now(Flavour { mime: TEXT, data: t.into_bytes() })),
                     BITMAP => {
                         let img = cb.get_image().ok()?;
-                        let rgba =
-                            image::RgbaImage::from_raw(u32::try_from(img.width).ok()?, u32::try_from(img.height).ok()?, img.bytes.into_owned())?;
-                        png_flavour(&rgba)
+                        let (width, height) = (u32::try_from(img.width).ok()?, u32::try_from(img.height).ok()?);
+                        let bytes = img.bytes.into_owned();
+                        Some(ClipboardRead::Later(Box::new(move || png_flavour(&image::RgbaImage::from_raw(width, height, bytes)?))))
                     }
                     _ => None,
                 }
diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs
--- a/crates/engine/src/cmd/clipboard.rs
+++ b/crates/engine/src/cmd/clipboard.rs
@@ -22,6 +22,7 @@ use vectorcraft_doc::{Document, GraphicStyle, ImageBlob, Node, NodeId, PatternDe
 use vectorcraft_geom::Rect;
 
 pub use flavours::{AI, BITMAP, EMF, EPS, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address, names_a_picture};
+pub use flavours::{ImageRead, read_image};
 pub(crate) use resources::SwatchChoices;
 pub use resources::{ArtResources, art_resources};
 
diff --git a/crates/engine/src/cmd/clipboard/flavours.rs b/crates/engine/src/cmd/clipboard/flavours.rs
--- a/crates/engine/src/cmd/clipboard/flavours.rs
+++ b/crates/engine/src/cmd/clipboard/flavours.rs
@@ -413,12 +413,36 @@ fn flavours(s: &mut Session, _: &Value) -> Result<Value> {
     Ok(json!({ "flavours": s.clipboard_flavour_types() }))
 }
 
+/// A bitmap read as `clipboard.importImage` reads its `dataBase64` ([`read_image`]), for
+/// [`Session::import_image_read`].
+pub struct ImageRead(fileio::RasterImage);
+
+/// Read `bytes` as `clipboard.importImage` reads its `dataBase64`, on any thread: the formats it
+/// stores as PNG are decoded and encoded here.
+pub fn read_image(bytes: &[u8]) -> Result<ImageRead> {
+    fileio::raster_image(bytes).map(ImageRead).map_err(|e| bad("clipboard.importImage", e.to_string()))
+}
+
+impl Session {
+    /// `clipboard.importImage` with `p` (its `mime` and `center`) for an image [`read_image`] read
+    /// (off the UI thread in the desktop app), which it loads instead of reading `dataBase64`.
+    pub fn import_image_read(&mut self, read: ImageRead, p: &Value) -> Result<Value> {
+        self.image_hand_off = Some(read);
+        let r = self.execute("clipboard.importImage", p);
+        self.image_hand_off = None;
+        r
+    }
+}
+
 fn import_image(s: &mut Session, p: &Value) -> Result<Value> {
     const C: &str = "clipboard.importImage";
     if let Some(m) = str_param(p, "mime").filter(|m| !m.starts_with("image/") || *m == SVG) {
         return Err(bad(C, format!("`{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText")));
     }
-    let img = fileio::raster_image(&data_param(p, C)?).map_err(|e| bad(C, e.to_string()))?;
+    let ImageRead(img) = match s.image_hand_off.take() {
+        Some(read) => read,
+        None => read_image(&data_param(p, C)?)?,
+    };
     let (sx, sy) = pt_per_px(img.ppi);
     let (w, h) = (img.width, img.height);
     let image = ImageObject {
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -1481,6 +1481,9 @@ pub struct Session {
     /// While [`Session::place_queue_read`] runs `file.place.queue`: the files read for it, which
     /// the command loads the place cursor with instead of reading them; `None` otherwise.
     pub(crate) queue_hand_off: Option<cmd::place::QueueRead>,
+    /// While [`Session::import_image_read`] runs `clipboard.importImage`: the image read for it,
+    /// which the command loads instead of reading `dataBase64`; `None` otherwise.
+    pub(crate) image_hand_off: Option<cmd::clipboard::ImageRead>,
     /// Whether a click or drag of the place cursor asks the frontend to place its file
     /// ([`UiRequest::Place`]) instead of running `file.place`: the desktop app sets it to read the
     /// file off the UI thread. False: the file is placed at once.
@@ -1610,6 +1613,7 @@ impl Session {
             batch_steps: None,
             place_hand_off: None,
             queue_hand_off: None,
+            image_hand_off: None,
             place_cursor_requests: false,
             package_hand_off: None,
             screens_hand_off: None,
diff --git a/crates/engine/src/tests_clipflavours.rs b/crates/engine/src/tests_clipflavours.rs
--- a/crates/engine/src/tests_clipflavours.rs
+++ b/crates/engine/src/tests_clipflavours.rs
@@ -150,6 +150,30 @@ fn import_image_then_paste_gives_an_image() {
     assert!(s.execute("clipboard.importImage", &json!({"dataBase64": "!!"})).is_err());
 }
 
+/// An image read apart from the session (off the UI thread in the desktop app) loads as
+/// `clipboard.importImage` loads its bytes: the same reply and clipboard, and the same errors.
+#[test]
+fn an_image_read_apart_loads_as_import_image_loads_it() {
+    let mut bmp = vec![];
+    image::RgbImage::from_pixel(30, 20, image::Rgb([200, 40, 40])).write_to(&mut std::io::Cursor::new(&mut bmp), image::ImageFormat::Bmp).unwrap();
+    let (mut at_once, mut apart) = (session(), session());
+    let p = json!({"mime": "image/bmp", "center": [100, 80]});
+    let mut with_data = p.clone();
+    with_data["dataBase64"] = json!(vectorcraft_format::base64_encode(&bmp));
+    let r = run(&mut at_once, "clipboard.importImage", with_data);
+    let read = crate::cmd::clipboard::read_image(&bmp).unwrap();
+    assert_eq!(apart.import_image_read(read, &p).unwrap(), r);
+    assert_eq!(apart.clipboard, at_once.clipboard);
+    assert!(apart.image_hand_off.is_none());
+    // Errors: bytes that aren't a picture, and a type that isn't a bitmap.
+    let not_a_picture = at_once.execute("clipboard.importImage", &json!({"dataBase64": vectorcraft_format::base64_encode(b"hello")})).unwrap_err();
+    assert_eq!(crate::cmd::clipboard::read_image(b"hello").err().map(|e| e.to_string()), Some(not_a_picture.to_string()));
+    let read = crate::cmd::clipboard::read_image(&bmp).unwrap();
+    let svg = apart.import_image_read(read, &json!({"mime": SVG})).unwrap_err();
+    assert!(svg.to_string().contains("isn't a bitmap"), "{svg}");
+    assert!(apart.image_hand_off.is_none() && apart.clipboard == at_once.clipboard, "nothing loaded");
+}
+
 #[test]
 fn copied_svgz_files_paste_as_svg() {
     let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"><rect width=\"40\" height=\"40\" fill=\"#ff0000\"/></svg>";
diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv
--- a/crates/ui-egui/src/i18n/bg.tsv
+++ b/crates/ui-egui/src/i18n/bg.tsv
@@ -4294,6 +4294,7 @@ axis	Both	И двете
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Неуспешно поставяне на {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Неуспешно поставяне от клипборда: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Неуспешно поставяне: документът, в който се поставяше, е затворен
 @msg	Saved {path} with {_1} note(s): {first}	{path} е запазен с {_1} бележка/и: {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} е експортиран с {_1} бележка/и: {first}
 @msg	Exported {what}	Експортирано: {what}
@@ -4303,6 +4304,7 @@ axis	Both	И двете
 @msg	Exporting {_1}	Експортиране на {_1}
 @msg	Packaging {_1}	Пакетиране на {_1}
 @msg	Placing {_1}	Вмъкване на {_1}
+@msg	Pasting	Поставяне
 @msg	Saving recovery data for {_1}	Записване на данни за възстановяване на {_1}
 @msg	Discarded {_1} recovered document(s)	Изхвърлени са {_1} възстановени документ(и)
 @msg	Restored {_1} recovered document(s): save them to keep them	Възстановени са {_1} документ(и): запазете ги, за да ги съхраните
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -4701,6 +4701,7 @@ imageTrace	Outline	Obrys
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	{name} nelze umístit: {e}
 @msg	Couldn't paste from the clipboard: {e}	Nelze vložit ze schránky: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Nelze vložit: dokument, do kterého se vkládalo, byl zavřen
 @msg	Saved {path} with {_1} note(s): {first}	Uloženo: {path}, poznámky ({_1}): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Exportováno: {what}, poznámky ({_1}): {first}
 @msg	Exported {what}	Exportováno: {what}
@@ -4710,6 +4711,7 @@ imageTrace	Outline	Obrys
 @msg	Exporting {_1}	Exportování {_1}
 @msg	Packaging {_1}	Balení {_1}
 @msg	Placing {_1}	Umísťování {_1}
+@msg	Pasting	Vkládání
 @msg	Saving recovery data for {_1}	Ukládání dat pro obnovení pro {_1}
 @msg	Discarded {_1} recovered document(s)	Zahozeno obnovených dokumentů: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	Obnoveno dokumentů: {_1}; pokud je chcete zachovat, uložte je
diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv
--- a/crates/ui-egui/src/i18n/da.tsv
+++ b/crates/ui-egui/src/i18n/da.tsv
@@ -4390,6 +4390,7 @@ imageTrace	Outline	Kontur
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Kunne ikke placere {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Kunne ikke indsætte fra udklipsholderen: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Kunne ikke indsætte: dokumentet, der blev indsat i, var lukket
 @msg	Saved {path} with {_1} note(s): {first}	Gemte {path} med {_1} note(r): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Eksporterede {what} med {_1} note(r): {first}
 @msg	Exported {what}	Eksporterede {what}
@@ -4399,6 +4400,7 @@ imageTrace	Outline	Kontur
 @msg	Exporting {_1}	Eksporterer {_1}
 @msg	Packaging {_1}	Pakker {_1}
 @msg	Placing {_1}	Placerer {_1}
+@msg	Pasting	Indsætter
 @msg	Saving recovery data for {_1}	Gemmer gendannelsesdata for {_1}
 @msg	Discarded {_1} recovered document(s)	Forkastede {_1} gendannet(e) dokument(er)
 @msg	Restored {_1} recovered document(s): save them to keep them	Genvandt {_1} gendannet(e) dokument(er): gem dem for at beholde dem
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -4324,6 +4324,7 @@ theme	Appearance	Erscheinungsbild
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	{name} konnte nicht platziert werden: {e}
 @msg	Couldn't paste from the clipboard: {e}	Aus der Zwischenablage konnte nicht eingefügt werden: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Konnte nicht eingefügt werden: Das Dokument, in das eingefügt wurde, wurde geschlossen
 @msg	Saved {path} with {_1} note(s): {first}	{path} mit {_1} Hinweis(en) gespeichert: {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} mit {_1} Hinweis(en) exportiert: {first}
 @msg	Exported {what}	{what} exportiert
@@ -4333,6 +4334,7 @@ theme	Appearance	Erscheinungsbild
 @msg	Exporting {_1}	{_1} wird exportiert
 @msg	Packaging {_1}	{_1} wird verpackt
 @msg	Placing {_1}	{_1} wird platziert
+@msg	Pasting	Wird eingefügt
 @msg	Saving recovery data for {_1}	Wiederherstellungsdaten für {_1} werden gespeichert
 @msg	Discarded {_1} recovered document(s)	{_1} wiederhergestellte(s) Dokument(e) verworfen
 @msg	Restored {_1} recovered document(s): save them to keep them	{_1} wiederhergestellte(s) Dokument(e) geöffnet: Speichern Sie sie, um sie zu behalten
diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv
--- a/crates/ui-egui/src/i18n/el.tsv
+++ b/crates/ui-egui/src/i18n/el.tsv
@@ -4358,6 +4358,7 @@ theme	Appearance	Εμφάνιση
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Δεν ήταν δυνατή η τοποθέτηση του {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Δεν ήταν δυνατή η επικόλληση από το πρόχειρο: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Δεν ήταν δυνατή η επικόλληση: το έγγραφο στο οποίο γινόταν η επικόλληση έκλεισε
 @msg	Saved {path} with {_1} note(s): {first}	Αποθηκεύτηκε το {path} με {_1} σημειώσεις: {first}
 @msg	Exported {what} with {_1} note(s): {first}	Εξήχθη το {what} με {_1} σημειώσεις: {first}
 @msg	Exported {what}	Εξήχθη: {what}
@@ -4367,6 +4368,7 @@ theme	Appearance	Εμφάνιση
 @msg	Exporting {_1}	Εξαγωγή {_1}
 @msg	Packaging {_1}	Συσκευασία {_1}
 @msg	Placing {_1}	Τοποθέτηση {_1}
+@msg	Pasting	Επικόλληση
 @msg	Saving recovery data for {_1}	Αποθήκευση δεδομένων ανάκτησης για {_1}
 @msg	Discarded {_1} recovered document(s)	Απορρίφθηκαν {_1} ανακτημένα έγγραφα
 @msg	Restored {_1} recovered document(s): save them to keep them	Επαναφέρθηκαν {_1} ανακτημένα έγγραφα: αποθηκεύστε τα για να διατηρηθούν
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -4400,6 +4400,7 @@ axis	Both	Ambos
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	No se pudo colocar {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	No se pudo pegar desde el portapapeles: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	No se pudo pegar: se cerró el documento en el que se estaba pegando
 @msg	Saved {path} with {_1} note(s): {first}	Se guardó {path} con {_1} nota(s): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Se exportó {what} con {_1} nota(s): {first}
 @msg	Exported {what}	Se exportó {what}
@@ -4409,6 +4410,7 @@ axis	Both	Ambos
 @msg	Exporting {_1}	Exportando {_1}
 @msg	Packaging {_1}	Empaquetando {_1}
 @msg	Placing {_1}	Colocando {_1}
+@msg	Pasting	Pegando
 @msg	Saving recovery data for {_1}	Guardando datos de recuperación de {_1}
 @msg	Discarded {_1} recovered document(s)	Se descartaron {_1} documento(s) recuperado(s)
 @msg	Restored {_1} recovered document(s): save them to keep them	Se restauraron {_1} documento(s) recuperado(s): guárdalos para conservarlos
diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv
--- a/crates/ui-egui/src/i18n/fi.tsv
+++ b/crates/ui-egui/src/i18n/fi.tsv
@@ -4608,6 +4608,7 @@ imageTrace	Outline	Ääriviiva
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Kohdetta {name} ei voitu sijoittaa: {e}
 @msg	Couldn't paste from the clipboard: {e}	Leikepöydältä ei voitu liittää: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Liittäminen ei onnistunut: asiakirja, johon liitettiin, on suljettu
 @msg	Saved {path} with {_1} note(s): {first}	Tallennettiin {path}, {_1} huomautus(ta): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Vietiin {what}, {_1} huomautus(ta): {first}
 @msg	Exported {what}	Vietiin {what}
@@ -4617,6 +4618,7 @@ imageTrace	Outline	Ääriviiva
 @msg	Exporting {_1}	Viedään {_1}
 @msg	Packaging {_1}	Paketoidaan {_1}
 @msg	Placing {_1}	Sijoitetaan {_1}
+@msg	Pasting	Liitetään
 @msg	Saving recovery data for {_1}	Tallennetaan palautustietoja: {_1}
 @msg	Discarded {_1} recovered document(s)	Hylättiin {_1} palautettu(a) asiakirja(a)
 @msg	Restored {_1} recovered document(s): save them to keep them	Palautettiin {_1} asiakirja(a): tallenna ne säilyttääksesi ne
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -4396,6 +4396,7 @@ axis	Both	Les deux
 @msg	{path}: {e}	{path} : {e}
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Impossible d’importer {name} : {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Impossible de coller, car le document de destination a été fermé
 @msg	Couldn't paste from the clipboard: {e}	Impossible de coller depuis le presse-papiers : {e}
 @msg	Saved {path} with {_1} note(s): {first}	{path} enregistré avec {_1} remarque(s) : {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} exporté avec {_1} remarque(s) : {first}
@@ -4406,6 +4407,7 @@ axis	Both	Les deux
 @msg	Exporting {_1}	Exportation de {_1}
 @msg	Packaging {_1}	Assemblage de {_1}
 @msg	Placing {_1}	Importation de {_1}
+@msg	Pasting	Collage
 @msg	Saving recovery data for {_1}	Enregistrement des données de récupération de {_1}
 @msg	Discarded {_1} recovered document(s)	{_1} document(s) récupéré(s) supprimé(s)
 @msg	Restored {_1} recovered document(s): save them to keep them	{_1} document(s) récupéré(s) restauré(s) : enregistrez-les pour les conserver
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -4600,6 +4600,7 @@ imageTrace	Outline	Körvonal
 @msg	Couldn't copy to the system clipboard: {e}	Nem sikerült a rendszer vágólapjára másolni: {e}
 @msg	Couldn't open {name}: {e}	Nem sikerült megnyitni: {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Nem sikerült beilleszteni a vágólapról: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Nem sikerült beilleszteni: a dokumentumot, amelybe a beillesztés történt, bezárták
 @msg	Couldn't place {name}: the document it was dropped on was closed	Nem sikerült elhelyezni: {name}; a céldokumentumot bezárták
 @msg	Couldn't place {name}: the document it was placed in was closed	Nem sikerült elhelyezni: {name}; a dokumentumot, amelybe került volna, bezárták
 @msg	Couldn't place {name}: {e}	Nem sikerült elhelyezni: {name}: {e}
@@ -4642,6 +4643,7 @@ imageTrace	Outline	Körvonal
 @msg	Exporting {_1}	Exportálás: {_1}
 @msg	Packaging {_1}	Csomagolás: {_1}
 @msg	Placing {_1}	Elhelyezés: {_1}
+@msg	Pasting	Beillesztés
 @msg	file version {0} is newer than this VectorCraft supports ({VERSION})	a fájl verziója újabb, mint amit ez a VectorCraft támogat: {0} (támogatott: {VERSION})
 @msg	Flatten Transparency: select objects to flatten	Átlátszóság lapítása: jelöld ki a lapítandó objektumokat
 @msg	Flatten Transparency: {e}	Átlátszóság lapítása: {e}
diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv
--- a/crates/ui-egui/src/i18n/id.tsv
+++ b/crates/ui-egui/src/i18n/id.tsv
@@ -4390,6 +4390,7 @@ imageTrace	Outline	Kontur
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Tidak dapat menempatkan {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Tidak dapat menempel dari papan klip: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Tidak dapat menempel: dokumen tempatnya ditempel sudah ditutup
 @msg	Saved {path} with {_1} note(s): {first}	{path} disimpan dengan {_1} catatan: {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} diekspor dengan {_1} catatan: {first}
 @msg	Exported {what}	{what} diekspor
@@ -4399,6 +4400,7 @@ imageTrace	Outline	Kontur
 @msg	Exporting {_1}	Mengekspor {_1}
 @msg	Packaging {_1}	Mengemas {_1}
 @msg	Placing {_1}	Menempatkan {_1}
+@msg	Pasting	Menempel
 @msg	Saving recovery data for {_1}	Menyimpan data pemulihan untuk {_1}
 @msg	Discarded {_1} recovered document(s)	{_1} dokumen hasil pemulihan dibuang
 @msg	Restored {_1} recovered document(s): save them to keep them	{_1} dokumen hasil pemulihan dipulihkan: simpan agar tidak hilang
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -4440,6 +4440,7 @@ axis	Both	Entrambi
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Impossibile inserire {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Impossibile incollare dagli appunti: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Impossibile incollare: il documento in cui si incollava è stato chiuso
 @msg	Saved {path} with {_1} note(s): {first}	Salvato {path} con {_1} nota/e: {first}
 @msg	Exported {what} with {_1} note(s): {first}	Esportato {what} con {_1} nota/e: {first}
 @msg	Exported {what}	Esportato {what}
@@ -4449,6 +4450,7 @@ axis	Both	Entrambi
 @msg	Exporting {_1}	Esportazione di {_1}
 @msg	Packaging {_1}	Creazione del pacchetto {_1}
 @msg	Placing {_1}	Inserimento di {_1}
+@msg	Pasting	Incollamento
 @msg	Saving recovery data for {_1}	Salvataggio dei dati di recupero per {_1}
 @msg	Discarded {_1} recovered document(s)	Documenti recuperati scartati: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	Documenti recuperati ripristinati: {_1}. Salvali per conservarli
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -4338,6 +4338,7 @@ swatch	Bright Violet	ビビッドバイオレット
 @msg	Exporting {_1}	{_1} を書き出し中
 @msg	Packaging {_1}	{_1} をパッケージ中
 @msg	Placing {_1}	{_1} を配置中
+@msg	Pasting	貼り付け中
 @msg	Exported {what}	{what} を書き出しました
 @msg	"{0}" is not a colour: write it as #rrggbb	「{0}」はカラーではありません: #rrggbb の形式で指定してください
 @msg	could not decode image: {0}	画像をデコードできませんでした: {0}
@@ -4547,6 +4548,7 @@ swatch	Bright Violet	ビビッドバイオレット
 @msg	{_1} is reserved by the system	{_1} はシステムで予約されています
 @msg	Couldn't copy to the system clipboard: {e}	システムのクリップボードにコピーできませんでした: {e}
 @msg	Couldn't paste from the clipboard: {e}	クリップボードから貼り付けられませんでした: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	貼り付けられませんでした: 貼り付け先のドキュメントは閉じられています
 @msg	nothing selected	何も選択されていません
 @msg	the {tool} tool has no options	{tool} ツールにはオプションがありません
 @msg	unknown tool `{tool}`	`{tool}` は不明なツールです
diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv
--- a/crates/ui-egui/src/i18n/ka.tsv
+++ b/crates/ui-egui/src/i18n/ka.tsv
@@ -4154,6 +4154,7 @@ font-technology	Variable	ცვლადი
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	{name}-ის განთავსება ვერ მოხერხდა: {e}
 @msg	Couldn't paste from the clipboard: {e}	გაცვლის ბუფერიდან ჩასმა ვერ მოხერხდა: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	ჩასმა ვერ მოხერხდა: დოკუმენტი, რომელშიც ის ჩაისმებოდა, დაიხურა
 @msg	Saved {path} with {_1} note(s): {first}	შენახულია {path}, შენიშვნები: {_1}: {first}
 @msg	Exported {what} with {_1} note(s): {first}	ექსპორტირებულია {what}, შენიშვნები: {_1}: {first}
 @msg	Exported {what}	ექსპორტირებულია {what}
@@ -4163,6 +4164,7 @@ font-technology	Variable	ცვლადი
 @msg	Exporting {_1}	ექსპორტირდება: {_1}
 @msg	Packaging {_1}	იფუთება: {_1}
 @msg	Placing {_1}	თავსდება: {_1}
+@msg	Pasting	ჩასმა
 @msg	Saving recovery data for {_1}	{_1}-ის აღდგენის მონაცემების შენახვა
 @msg	Discarded {_1} recovered document(s)	უარყოფილია აღდგენილი დოკუმენტები: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	აღდგენილია დოკუმენტები: {_1}: შესანარჩუნებლად შეინახეთ ისინი
diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv
--- a/crates/ui-egui/src/i18n/ko.tsv
+++ b/crates/ui-egui/src/i18n/ko.tsv
@@ -4270,6 +4270,7 @@ theme	Appearance	외관
 @msg	Exporting {_1}	{_1} 내보내는 중
 @msg	Packaging {_1}	{_1} 패키지 만드는 중
 @msg	Placing {_1}	{_1} 가져오는 중
+@msg	Pasting	붙여넣는 중
 @msg	Exported {what}	{what}을(를) 내보냈습니다
 @msg	"{0}" is not a colour: write it as #rrggbb	"{0}"은(는) 색상이 아닙니다: #rrggbb 형식으로 입력하세요
 @msg	could not decode image: {0}	이미지를 디코딩할 수 없습니다: {0}
@@ -4479,6 +4480,7 @@ theme	Appearance	외관
 @msg	{_1} is reserved by the system	{_1}은(는) 시스템에서 예약되어 있습니다
 @msg	Couldn't copy to the system clipboard: {e}	시스템 클립보드로 복사할 수 없습니다: {e}
 @msg	Couldn't paste from the clipboard: {e}	클립보드에서 붙여넣을 수 없습니다: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	붙여넣을 수 없습니다: 붙여넣던 문서가 닫혔습니다
 @msg	nothing selected	선택된 항목이 없습니다
 @msg	the {tool} tool has no options	{tool} 도구에는 옵션이 없습니다
 @msg	unknown tool `{tool}`	알 수 없는 도구 `{tool}`
diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv
--- a/crates/ui-egui/src/i18n/nl.tsv
+++ b/crates/ui-egui/src/i18n/nl.tsv
@@ -4402,6 +4402,7 @@ imageTrace	Outline	Contour
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Kon {name} niet plaatsen: {e}
 @msg	Couldn't paste from the clipboard: {e}	Kon niet van het klembord plakken: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Kon niet plakken: het document waarin werd geplakt, is gesloten
 @msg	Saved {path} with {_1} note(s): {first}	{path} opgeslagen met {_1} opmerking(en): {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} geëxporteerd met {_1} opmerking(en): {first}
 @msg	Exported {what}	{what} geëxporteerd
@@ -4411,6 +4412,7 @@ imageTrace	Outline	Contour
 @msg	Exporting {_1}	Bezig met exporteren van {_1}
 @msg	Packaging {_1}	Bezig met verpakken van {_1}
 @msg	Placing {_1}	Bezig met plaatsen van {_1}
+@msg	Pasting	Bezig met plakken
 @msg	Saving recovery data for {_1}	Bezig met opslaan van herstelgegevens voor {_1}
 @msg	Discarded {_1} recovered document(s)	{_1} hersteld(e) document(en) verworpen
 @msg	Restored {_1} recovered document(s): save them to keep them	{_1} hersteld(e) document(en) teruggezet: sla ze op om ze te behouden
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -4526,6 +4526,7 @@ imageTrace	Outline	Kontur
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Nie udało się umieścić {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Nie udało się wkleić ze schowka: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Nie udało się wkleić: dokument, do którego wklejano, został zamknięty
 @msg	Saved {path} with {_1} note(s): {first}	Zapisano {path}, uwagi ({_1}): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Wyeksportowano {what}, uwagi ({_1}): {first}
 @msg	Exported {what}	Wyeksportowano {what}
@@ -4535,6 +4536,7 @@ imageTrace	Outline	Kontur
 @msg	Exporting {_1}	Eksportowanie {_1}
 @msg	Packaging {_1}	Pakowanie {_1}
 @msg	Placing {_1}	Umieszczanie {_1}
+@msg	Pasting	Wklejanie
 @msg	Saving recovery data for {_1}	Zapisywanie danych odzyskiwania dla {_1}
 @msg	Discarded {_1} recovered document(s)	Odrzucone odzyskane dokumenty: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	Przywrócone odzyskane dokumenty: {_1} — zapisz je, aby je zachować
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -3971,6 +3971,7 @@ axis	Both	Ambos
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Não foi possível inserir {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Não foi possível colar da área de transferência: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Não foi possível colar: o documento em que estava sendo colado foi fechado
 @msg	Saved {path} with {_1} note(s): {first}	{path} salvo com {_1} observação(ões): {first}
 @msg	Exported {what} with {_1} note(s): {first}	{what} exportado com {_1} observação(ões): {first}
 @msg	Exported {what}	{what} exportado
@@ -3980,6 +3981,7 @@ axis	Both	Ambos
 @msg	Exporting {_1}	Exportando {_1}
 @msg	Packaging {_1}	Empacotando {_1}
 @msg	Placing {_1}	Inserindo {_1}
+@msg	Pasting	Colando
 @msg	Saving recovery data for {_1}	Salvando dados de recuperação de {_1}
 @msg	Discarded {_1} recovered document(s)	{_1} documento(s) recuperado(s) descartado(s)
 @msg	Restored {_1} recovered document(s): save them to keep them	{_1} documento(s) recuperado(s) restaurado(s): salve-os para mantê-los
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -4436,6 +4436,7 @@ axis	Both	Оба
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Не удалось поместить {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Не удалось вставить из буфера обмена: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Не удалось вставить: документ, в который выполнялась вставка, был закрыт
 @msg	Saved {path} with {_1} note(s): {first}	Сохранено: {path} с примечаниями ({_1}) : {first}
 @msg	Exported {what} with {_1} note(s): {first}	Экспортировано: {what} с примечаниями ({_1}) : {first}
 @msg	Exported {what}	Экспортировано: {what}
@@ -4445,6 +4446,7 @@ axis	Both	Оба
 @msg	Exporting {_1}	Экспорт {_1}
 @msg	Packaging {_1}	Упаковка {_1}
 @msg	Placing {_1}	Размещение {_1}
+@msg	Pasting	Вставка
 @msg	Saving recovery data for {_1}	Сохранение данных восстановления для {_1}
 @msg	Discarded {_1} recovered document(s)	Отменено восстановленных документов: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	Восстановлено документов: {_1}: сохраните их, чтобы оставить
diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv
--- a/crates/ui-egui/src/i18n/sv.tsv
+++ b/crates/ui-egui/src/i18n/sv.tsv
@@ -4309,6 +4309,7 @@ axis	Both	Båda
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Kunde inte placera {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Kunde inte klistra in från urklippet: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Kunde inte klistra in: dokumentet det klistrades in i har stängts
 @msg	Saved {path} with {_1} note(s): {first}	Sparade {path} med {_1} anteckning(ar): {first}
 @msg	Exported {what} with {_1} note(s): {first}	Exporterade {what} med {_1} anteckning(ar): {first}
 @msg	Exported {what}	Exporterade {what}
@@ -4318,6 +4319,7 @@ axis	Both	Båda
 @msg	Exporting {_1}	Exporterar {_1}
 @msg	Packaging {_1}	Paketerar {_1}
 @msg	Placing {_1}	Placerar {_1}
+@msg	Pasting	Klistrar in
 @msg	Saving recovery data for {_1}	Sparar återställningsdata för {_1}
 @msg	Discarded {_1} recovered document(s)	Kastade {_1} återställda dokument
 @msg	Restored {_1} recovered document(s): save them to keep them	Återställde {_1} återskapade dokument: spara dem för att behålla dem
diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv
--- a/crates/ui-egui/src/i18n/tr.tsv
+++ b/crates/ui-egui/src/i18n/tr.tsv
@@ -4155,6 +4155,7 @@ axis	Both	Her İkisi
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	{name} yerleştirilemedi: {e}
 @msg	Couldn't paste from the clipboard: {e}	Panodan yapıştırılamadı: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Yapıştırılamadı: yapıştırıldığı belge kapatıldı
 @msg	Saved {path} with {_1} note(s): {first}	{path}, {_1} notlarıyla kaydedildi: {first}
 @msg	Exported {what} with {_1} note(s): {first}	{_1} notlarıyla birlikte dışa aktarılan {what}: {first}
 @msg	Exported {what}	Dışa aktarılan {what}
@@ -4164,6 +4165,7 @@ axis	Both	Her İkisi
 @msg	Exporting {_1}	{_1}'yi dışa aktarma
 @msg	Packaging {_1}	{_1} paketleniyor
 @msg	Placing {_1}	{_1} yerleştiriliyor
+@msg	Pasting	Yapıştırılıyor
 @msg	Saving recovery data for {_1}	{_1} için kurtarma verileri kaydediliyor
 @msg	Discarded {_1} recovered document(s)	Atılan {_1} kurtarılan belge(ler)
 @msg	Restored {_1} recovered document(s): save them to keep them	Geri yüklenen {_1} kurtarılan belge(ler): saklamak için bunları kaydedin
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -4424,6 +4424,7 @@ axis	Both	Обидві
 @msg	{status} ({e})	{status} ({e})
 @msg	Couldn't place {name}: {e}	Не вдалося помістити {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Не вдалося вставити з буфера обміну: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Не вдалося вставити: документ, у який виконувалося вставлення, було закрито
 @msg	Saved {path} with {_1} note(s): {first}	Збережено: {path} з примітками ({_1}) : {first}
 @msg	Exported {what} with {_1} note(s): {first}	Експортовано: {what} з примітками ({_1}) : {first}
 @msg	Exported {what}	Експортовано: {what}
@@ -4433,6 +4434,7 @@ axis	Both	Обидві
 @msg	Exporting {_1}	Експорт {_1}
 @msg	Packaging {_1}	Пакування {_1}
 @msg	Placing {_1}	Розміщення {_1}
+@msg	Pasting	Вставлення
 @msg	Saving recovery data for {_1}	Збереження даних відновлення для {_1}
 @msg	Discarded {_1} recovered document(s)	Скасовано відновлених документів: {_1}
 @msg	Restored {_1} recovered document(s): save them to keep them	Відновлено документів: {_1}: збережіть їх, щоб залишити
diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv
--- a/crates/ui-egui/src/i18n/vi.tsv
+++ b/crates/ui-egui/src/i18n/vi.tsv
@@ -4248,6 +4248,7 @@ theme	Appearance	Giao diện
 @msg	Couldn't copy to the system clipboard: {e}	Không thể sao chép vào khay nhớ tạm hệ thống: {e}
 @msg	Couldn't open {name}: {e}	Không thể mở {name}: {e}
 @msg	Couldn't paste from the clipboard: {e}	Không thể dán từ khay nhớ tạm: {e}
+@msg	Couldn't paste: the document it was pasted in was closed	Không thể dán: tài liệu mà nó được dán vào đã đóng
 @msg	Couldn't place {name}: the document it was dropped on was closed	Không thể đặt {name}: tài liệu mà nó được thả vào đã đóng
 @msg	Couldn't place {name}: the document it was placed in was closed	Không thể đặt {name}: tài liệu mà nó được đặt vào đã đóng
 @msg	Couldn't place {name}: {e}	Không thể đặt {name}: {e}
@@ -4288,6 +4289,7 @@ theme	Appearance	Giao diện
 @msg	Exporting {_1}	Đang xuất {_1}
 @msg	Packaging {_1}	Đang đóng gói {_1}
 @msg	Placing {_1}	Đang đặt {_1}
+@msg	Pasting	Đang dán
 @msg	file version {0} is newer than this VectorCraft supports ({VERSION})	phiên bản tệp {0} mới hơn VectorCraft hỗ trợ ({VERSION})
 @msg	Flatten Transparency: select objects to flatten	Dẹp phẳng độ trong suốt: hãy chọn đối tượng để dẹp phẳng
 @msg	Flatten Transparency: {e}	Dẹp phẳng độ trong suốt: {e}
diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv
--- a/crates/ui-egui/src/i18n/zh-hans.tsv
+++ b/crates/ui-egui/src/i18n/zh-hans.tsv
@@ -4070,6 +4070,7 @@ imageTrace	Outline	轮廓
 @msg	Exporting {_1}	正在导出 {_1}
 @msg	Packaging {_1}	正在打包 {_1}
 @msg	Placing {_1}	正在置入 {_1}
+@msg	Pasting	正在粘贴
 @msg	Exported {what}	已导出 {what}
 @msg	"{0}" is not a colour: write it as #rrggbb	"{0}" 不是颜色：请写为 #rrggbb
 @msg	could not decode image: {0}	无法解码图像：{0}
@@ -4271,6 +4272,7 @@ imageTrace	Outline	轮廓
 @msg	{_1} is reserved by the system	{_1} 已被系统保留
 @msg	Couldn't copy to the system clipboard: {e}	无法复制到系统剪贴板：{e}
 @msg	Couldn't paste from the clipboard: {e}	无法从剪贴板粘贴：{e}
+@msg	Couldn't paste: the document it was pasted in was closed	无法粘贴：要粘贴到的文档已关闭
 @msg	nothing selected	未选择任何对象
 @msg	the {tool} tool has no options	{tool} 工具没有选项
 @msg	unknown tool `{tool}`	未知工具 `{tool}`
diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv
--- a/crates/ui-egui/src/i18n/zh-hant.tsv
+++ b/crates/ui-egui/src/i18n/zh-hant.tsv
@@ -2033,6 +2033,7 @@ file-filter	Templates	範本
 @msg	Exporting {_1}	正在匯出 {_1}
 @msg	Packaging {_1}	正在封裝 {_1}
 @msg	Placing {_1}	正在置入 {_1}
+@msg	Pasting	正在貼上
 @msg	Exported {what}	已匯出 {what}
 @msg	"{0}" is not a colour: write it as #rrggbb	"{0}" 不是顏色：請寫成 #rrggbb
 @msg	could not decode image: {0}	無法解碼影像：{0}
@@ -2234,6 +2235,7 @@ file-filter	Templates	範本
 @msg	{_1} is reserved by the system	{_1} 已被系統保留
 @msg	Couldn't copy to the system clipboard: {e}	無法複製到系統剪貼簿：{e}
 @msg	Couldn't paste from the clipboard: {e}	無法從剪貼簿貼上：{e}
+@msg	Couldn't paste: the document it was pasted in was closed	無法貼上：要貼上的文件已關閉
 @msg	nothing selected	未選取任何物件
 @msg	the {tool} tool has no options	{tool} 工具沒有選項
 @msg	unknown tool `{tool}`	未知的工具 `{tool}`
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -192,7 +192,7 @@ use vectorcraft_engine::{Session, ViewInfo};
 
 pub use control::{ControlRequest, ControlResponse, Ticket as ControlTicket};
 pub use state::{UiState, View};
-pub use sysclip::{ClipboardProbeFactory, SystemClipboard};
+pub use sysclip::{ClipboardProbeFactory, ClipboardRead, MakeFlavour, SystemClipboard};
 
 /// A file type a dialog filters by: its label, which shows translated, and its extensions without
 /// the dot.
@@ -1079,10 +1079,6 @@ impl VectorcraftApp {
             }
         }
         if id.starts_with("edit.paste") {
-            if let Err(e) = self.adopt_system_clipboard() {
-                self.ui.status = e.clone();
-                return Err(e);
-            }
             // Paste (also without formatting) goes to the centre of the view.
             if matches!(id, "edit.paste" | "edit.pasteWithoutFormatting")
                 && ["center", "dx", "dy"].iter().all(|k| params.get(k).is_none())
@@ -1091,6 +1087,16 @@ impl VectorcraftApp {
             {
                 p.insert("center".into(), serde_json::json!([c.x, c.y]));
             }
+            // A Paste the user gives decodes a bitmap off the UI thread ([`placing::adopt_for_paste`]),
+            // and runs with these params once it is decoded.
+            match placing::adopt_for_paste(self, id, &params) {
+                Err(e) => {
+                    self.ui.status = e.clone();
+                    return Err(e);
+                }
+                Ok(Some(started)) => return Ok(started),
+                Ok(None) => {}
+            }
             if let Some(r) = dialogs::swatch_conflict::ask(self, id, &params) {
                 return r;
             }
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -3970,7 +3970,11 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
         && p.as_object().is_some_and(|o| !o.contains_key("to"))
         && app.session.active().is_some_and(|st| st.selection.anchors.is_empty());
     let p = if follows_align_to { crate::panels::align::with_align_to(app, p) } else { p };
-    if let Err(e) = app.run(id, p) {
+    // A Paste the user gives decodes a bitmap off the UI thread ([`crate::placing::adopt_for_paste`]).
+    app.placing.for_user = id.starts_with("edit.paste");
+    let r = app.run(id, p);
+    app.placing.for_user = false;
+    if let Err(e) = r {
         app.status(e);
     } else if matches!(id, "object.pattern.make" | "object.pattern.edit") {
         app.ui.open_panel = Some("patternOptions".into());
diff --git a/crates/ui-egui/src/placing.rs b/crates/ui-egui/src/placing.rs
--- a/crates/ui-egui/src/placing.rs
+++ b/crates/ui-egui/src/placing.rs
@@ -4,23 +4,120 @@
 //! the document it was placed in once it is ready, as it would be at once: the same object, undo
 //! step, selection and errors. The files the Place dialog loads the place cursor with are read, and
 //! their thumbnails made, the same way, and so is the file each click or drag of the place cursor
-//! places. Files are read and placed in the order they were started. `file.place` and
-//! `file.place.queue` from MCP and scripts run at once, and so does every place on wasm (no
-//! threads), with `VECTORCRAFT_SYNC_OPEN` set, and in unit tests that do not set
-//! `off_the_ui_thread` ([`available`]).
+//! places. A Paste the user gives decodes another app's bitmap on the same thread, and runs once it
+//! is decoded ([`adopt_for_paste`]). Files are read and placed in the order they were started.
+//! `file.place`, `file.place.queue` and `edit.paste*` from MCP and scripts run at once, and so does
+//! every place and paste on wasm (no threads), with `VECTORCRAFT_SYNC_OPEN` set, and in unit tests
+//! that do not set `off_the_ui_thread` ([`available`]).
 
+use std::collections::HashMap;
 use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
+use std::sync::{Arc, Mutex};
 
 use serde_json::{Value, json};
+use vectorcraft_engine::cmd::clipboard::{Clipboard, Flavour, ImageRead, read_image};
 use vectorcraft_engine::cmd::place::{PlaceRead, QueueRead};
 use vectorcraft_engine::file_access::ScopeHandle;
 
 use crate::VectorcraftApp;
 
-/// What was read: one file to place, or the files to load the place cursor with.
+/// What was read: one file to place, the files to load the place cursor with, or a bitmap to paste.
 enum Read {
     Place(Box<PlaceRead>),
     Queue(QueueRead),
+    Paste(Box<Paste>),
+}
+
+/// A paste whose bitmap was decoded off the UI thread ([`adopt_for_paste`]).
+struct Paste {
+    /// The paste command and its params.
+    id: String,
+    params: Value,
+    /// What it pastes (`None`: a bitmap the host converts couldn't be converted, and the paste
+    /// takes the internal clipboard as it was when the paste started, as at once).
+    pasted: Option<Pasted>,
+    /// The view's center when the paste started, which the pasted objects are centered on.
+    center: Option<[f64; 2]>,
+    /// The objects selected when the paste started, which Paste in Front and Paste in Back paste
+    /// next to (`None`: the paste follows an earlier one of its chain, and takes the selection
+    /// that one left, [`Chain::left`]).
+    selection: Option<Vec<vectorcraft_doc::NodeId>>,
+    /// The number of its chain in [`Placing::chains`].
+    chain: u64,
+    /// Where new art went when the paste started, which is where the paste goes.
+    insertion: Insertion,
+    /// The latest Copy or Cut when the paste started ([`crate::sysclip::Publishing::latest`]).
+    copied: u64,
+    /// The internal clipboard when the paste started, which it pastes when [`Self::pasted`] is
+    /// `None`.
+    clipboard: Clipboard,
+}
+
+/// Where new art goes in the active document: the current layer, the isolated group or object, and
+/// the drawing mode with the object Draw Inside draws into.
+#[derive(Clone, PartialEq)]
+struct Insertion {
+    layer: Option<vectorcraft_doc::NodeId>,
+    isolation: Option<vectorcraft_doc::NodeId>,
+    draw_mode: vectorcraft_engine::DrawMode,
+    draw_inside: Option<vectorcraft_doc::NodeId>,
+}
+
+impl Insertion {
+    /// Where new art goes in `app`'s active document now.
+    fn of(app: &VectorcraftApp) -> Option<Self> {
+        let st = app.session.doc().ok()?;
+        Some(Self { layer: st.active_layer, isolation: st.isolation, draw_mode: app.session.draw_mode, draw_inside: app.session.draw_inside })
+    }
+
+    /// Make it where new art goes in `app`'s active document → where new art went before.
+    fn set(self, app: &mut VectorcraftApp) -> Option<Self> {
+        let st = app.session.doc_mut().ok()?;
+        let (layer, isolation) = (std::mem::replace(&mut st.active_layer, self.layer), std::mem::replace(&mut st.isolation, self.isolation));
+        let draw_mode = std::mem::replace(&mut app.session.draw_mode, self.draw_mode);
+        let draw_inside = std::mem::replace(&mut app.session.draw_inside, self.draw_inside);
+        Some(Self { layer, isolation, draw_mode, draw_inside })
+    }
+
+    /// Put back each part of `self` (where new art went before [`Self::set`] set `started`) that
+    /// differs from `started`, what the user changed meanwhile; a part the paste itself changed
+    /// stays as the paste left it.
+    fn restore_changed(self, started: &Self, app: &mut VectorcraftApp) {
+        if let Ok(st) = app.session.doc_mut() {
+            if self.layer != started.layer {
+                st.active_layer = self.layer;
+            }
+            if self.isolation != started.isolation {
+                st.isolation = self.isolation;
+            }
+        }
+        if self.draw_mode != started.draw_mode {
+            app.session.draw_mode = self.draw_mode;
+        }
+        if self.draw_inside != started.draw_inside {
+            app.session.draw_inside = self.draw_inside;
+        }
+    }
+}
+
+/// Pastes decoded off the UI thread that follow each other in one document: each was given while the
+/// one before waited, with the selection and the insertion context that one started with or, once
+/// it ran, left.
+struct Chain {
+    /// The selection and the insertion context a paste given now must have to join the chain.
+    context: (Vec<vectorcraft_doc::NodeId>, Insertion),
+    /// The selection the chain's latest paste left, which the next one takes.
+    left: Option<Vec<vectorcraft_doc::NodeId>>,
+    /// How many of its pastes wait.
+    waiting: usize,
+}
+
+/// What a paste whose bitmap was decoded off the UI thread pastes.
+enum Pasted {
+    /// The bitmap's type and the image read from it.
+    Image(&'static str, Result<ImageRead, String>),
+    /// Another flavour: the text that names the picture, when the picture couldn't be made.
+    Flavour(Flavour),
 }
 
 /// Files being read for Place.
@@ -34,6 +131,10 @@ pub struct Pending {
     /// Whether the file was dropped on the canvas (its messages say so, as
     /// [`crate::place::drop_files`]'s do).
     dropped: bool,
+    /// Whether it is a paste's bitmap ([`adopt_for_paste`]) rather than a file.
+    paste: bool,
+    /// The number of the paste's chain ([`Placing::chains`]).
+    chain: Option<u64>,
     done: Receiver<Result<Read, String>>,
 }
 
@@ -46,6 +147,17 @@ pub struct Placing {
     pub pending: Vec<Pending>,
     /// Set by unit tests whose app reads the files the user places off the UI thread.
     pub(crate) off_the_ui_thread: bool,
+    /// Set while [`crate::menus::invoke`] runs a Paste the user gave.
+    pub(crate) for_user: bool,
+    /// Set while a paste whose bitmap was converted off the UI thread runs: it takes the internal
+    /// clipboard as it is.
+    adopted: bool,
+    /// The chains of pastes decoded off the UI thread that have pastes waiting, by number.
+    chains: HashMap<u64, Chain>,
+    /// For each document, the number of its latest chain, which a paste given there may join.
+    latest_chain: HashMap<u64, u64>,
+    /// The number the next chain takes.
+    next_chain: u64,
     /// The thread that reads the files, one at a time in the order they were started: started with
     /// the first read, it ends when the app does.
     reader: Option<Sender<Job>>,
@@ -97,6 +209,71 @@ pub fn start_queue(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
     }
 }
 
+/// Before paste command `id` with `p`: what another app put on the system clipboard replaces the
+/// internal clipboard ([`VectorcraftApp::adopt_system_clipboard`]) → `None`, and the paste runs at
+/// once. For a Paste the user gives ([`crate::menus::invoke`]) where placed files are read off the
+/// UI thread ([`available`]), the bitmap's bytes are copied off the system clipboard at once and
+/// decoded on the thread that reads placed files, after the files started before it →
+/// `{background: true, status}`: a bitmap the host converts to PNG is converted there
+/// ([`crate::SystemClipboard::read_deferred`]), and the bitmap is read as `clipboard.importImage`
+/// reads it ([`read_image`]). The paste runs once it is decoded, in the document it was given in,
+/// as it would at once ([`poll`]). When no thread could start, the bitmap is read at once.
+pub fn adopt_for_paste(app: &mut VectorcraftApp, id: &str, p: &Value) -> Result<Option<Value>, String> {
+    if std::mem::take(&mut app.placing.adopted) {
+        app.clipboard_image_request = None;
+        return Ok(None);
+    }
+    if !(app.placing.for_user && available(app)) {
+        return app.adopt_system_clipboard().map(|()| None);
+    }
+    let center = app.view().map(|v| [v.center.x, v.center.y]);
+    let Some(make) = app.adopt_system_clipboard_or_defer(true)? else { return Ok(None) };
+    let copied = app.clipboard_publishing.latest();
+    let (doc, objects) = app.session.doc().map(|st| (st.uid, st.selection.objects.clone())).map_err(|e| e.to_string())?;
+    let insertion = Insertion::of(app).ok_or("no document")?;
+    // A paste given while one of the document's latest chain waits, with the selection and the
+    // insertion context that chain expects, joins it: it goes next to what the paste before it
+    // leaves selected, as at once. Otherwise it starts a chain.
+    let context = (objects, insertion.clone());
+    let joins =
+        app.placing.latest_chain.get(&doc).copied().filter(|c| app.placing.chains.get(c).is_some_and(|ch| ch.waiting > 0 && ch.context == context));
+    let (chain, selection) = match joins {
+        Some(chain) => (chain, None),
+        None => (app.placing.next_chain, Some(context.0.clone())),
+    };
+    let clipboard = app.session.clipboard.clone();
+    let (id, params) = (id.to_string(), p.clone());
+    // The work stays here when no thread could start.
+    let slot = Arc::new(Mutex::new(Some(make)));
+    let work = slot.clone();
+    let started = spawn_labeled(app, "clipboard".into(), "Pasting".into(), false, Some(chain), move || {
+        let make = work.lock().ok().and_then(|mut m| m.take());
+        let pasted = make.and_then(|m| m()).map(|f| {
+            if crate::sysclip::imports_as_image(f.mime) {
+                Pasted::Image(f.mime, read_image(&f.data).map_err(|e| e.to_string()))
+            } else {
+                Pasted::Flavour(f)
+            }
+        });
+        Ok(Read::Paste(Box::new(Paste { id, params, pasted, center, selection, chain, insertion, copied, clipboard })))
+    })?;
+    if started.is_some() {
+        if joins.is_none() {
+            app.placing.next_chain += 1;
+            app.placing.chains.insert(chain, Chain { context, left: None, waiting: 0 });
+            app.placing.latest_chain.insert(doc, chain);
+        }
+        if let Some(ch) = app.placing.chains.get_mut(&chain) {
+            ch.waiting += 1;
+        }
+        return Ok(started);
+    }
+    match slot.lock().ok().and_then(|mut m| m.take()).and_then(|m| m()) {
+        Some(f) => app.adopt_flavour(&f, center).map(|()| None),
+        None => Ok(None),
+    }
+}
+
 /// Run `read` for the active document on the thread that reads the files, after the reads started
 /// before it (so a batch of large pictures is decoded one at a time, as at once) →
 /// `{background: true, status}`, or `None` when that thread could not start.
@@ -106,6 +283,18 @@ fn spawn(
     shown: String,
     dropped: bool,
     read: impl FnOnce() -> Result<Read, String> + Send + 'static,
+) -> Result<Option<Value>, String> {
+    spawn_labeled(app, name, format!("Placing {shown}"), dropped, None, read)
+}
+
+/// [`spawn`] with the status bar's `label`; `chain` for a paste's bitmap, its chain's number.
+fn spawn_labeled(
+    app: &mut VectorcraftApp,
+    name: String,
+    label: String,
+    dropped: bool,
+    chain: Option<u64>,
+    read: impl FnOnce() -> Result<Read, String> + Send + 'static,
 ) -> Result<Option<Value>, String> {
     let doc = app.session.doc().map_err(|e| e.to_string())?.uid;
     let (tx, done) = channel();
@@ -145,8 +334,7 @@ fn spawn(
         let _ = reader.send(job);
         app.placing.reader = Some(reader);
     }
-    let label = format!("Placing {shown}");
-    app.placing.pending.push(Pending { name, label: label.clone(), doc, dropped, done });
+    app.placing.pending.push(Pending { name, label: label.clone(), doc, dropped, paste: chain.is_some(), chain, done });
     let status = format!("{label}…");
     app.status(status.clone());
     Ok(Some(json!({ "background": true, "status": status })))
@@ -178,6 +366,16 @@ pub fn poll(app: &mut VectorcraftApp) {
             app.ui.status.clear();
         }
         finish(app, &p, r);
+        // A chain none of whose pastes waits any more is dropped, whether its pastes ran or not.
+        if let Some(chain) = p.chain
+            && let Some(ch) = app.placing.chains.get_mut(&chain)
+        {
+            ch.waiting = ch.waiting.saturating_sub(1);
+            if ch.waiting == 0 {
+                app.placing.chains.remove(&chain);
+                app.placing.latest_chain.retain(|_, c| *c != chain);
+            }
+        }
     }
 }
 
@@ -187,7 +385,9 @@ pub fn poll(app: &mut VectorcraftApp) {
 fn finish(app: &mut VectorcraftApp, p: &Pending, r: Result<Read, String>) {
     let Some(i) = app.session.documents().iter().position(|st| st.uid == p.doc) else {
         let name = &p.name;
-        if p.dropped {
+        if p.paste {
+            app.status("Couldn't paste: the document it was pasted in was closed");
+        } else if p.dropped {
             app.status(format!("Couldn't place {name}: the document it was dropped on was closed"));
         } else {
             app.status(format!("Couldn't place {name}: the document it was placed in was closed"));
@@ -198,6 +398,8 @@ fn finish(app: &mut VectorcraftApp, p: &Pending, r: Result<Read, String>) {
     let r = match r {
         Ok(Read::Place(read)) => app.session.place_read(*read).map(|_| ()).map_err(|e| e.to_string()),
         Ok(Read::Queue(read)) => app.session.place_queue_read(read).map(|r| crate::place::show_thumbnails(app, &r)).map_err(|e| e.to_string()),
+        Ok(Read::Paste(paste)) => return finish_paste(app, *paste),
+        Err(e) if p.paste => return app.status(format!("Couldn't paste from the clipboard: {e}")),
         Err(e) => Err(e),
     };
     app.sync_views();
@@ -209,11 +411,75 @@ fn finish(app: &mut VectorcraftApp, p: &Pending, r: Result<Read, String>) {
     }
 }
 
+/// Run a paste whose bitmap was decoded off the UI thread, in its document, which is the active one:
+/// the image replaces the internal clipboard, the objects selected when the paste started are
+/// selected again, and the paste command runs as it runs at once ([`VectorcraftApp::run`]), into
+/// the layer that was current when it started, with its errors in the status bar. A Copy or Cut
+/// made since the paste started keeps the internal clipboard it made.
+fn finish_paste(app: &mut VectorcraftApp, paste: Paste) {
+    let kept = (app.clipboard_publishing.latest() != paste.copied).then(|| app.session.clipboard.clone());
+    let adopted = match paste.pasted {
+        Some(Pasted::Image(mime, read)) => app.adopt_image_read(mime, paste.center, read),
+        Some(Pasted::Flavour(f)) => app.adopt_flavour(&f, paste.center),
+        None => {
+            app.session.clipboard = paste.clipboard;
+            Ok(())
+        }
+    };
+    let chain = paste.chain;
+    let selection = paste.selection.or_else(|| app.placing.chains.get_mut(&chain).and_then(|ch| ch.left.take()));
+    let left = match adopted {
+        Err(e) => {
+            app.status(e);
+            selection
+        }
+        Ok(()) => {
+            // Where new art goes now, before a selection changes the current layer.
+            let current = Insertion::of(app);
+            // The objects selected when the paste started (or that the paste it follows left) are
+            // selected again (those that are left), as `Session::place_read` selects the object
+            // Replace swaps.
+            let now = app.session.doc().map(|st| st.selection.objects.clone()).unwrap_or_default();
+            if let Some(selection) = selection.filter(|s| *s != now) {
+                let left: Vec<u64> =
+                    selection.iter().filter(|id| app.session.doc().is_ok_and(|st| st.doc.node(**id).is_some())).map(|id| id.0).collect();
+                let _ = app.session.execute("select.set", &json!({ "ids": left }));
+            }
+            // The paste goes where new art went when it started (the current layer, the isolated
+            // group, the object Draw Inside draws into), and what the user changed meanwhile is
+            // kept.
+            paste.insertion.clone().set(app);
+            app.placing.adopted = true;
+            // `run` shows an error in the status bar.
+            let _ = app.run(&paste.id, paste.params);
+            // Also when the paste did not reach the clipboard (the Type tool took it).
+            app.placing.adopted = false;
+            if let Some(current) = current {
+                current.restore_changed(&paste.insertion, app);
+            }
+            app.session.doc().ok().map(|st| st.selection.objects.clone())
+        }
+    };
+    // The chain's next paste takes the selection this one left, and a paste given with it and the
+    // insertion context joins the chain while a paste of it waits ([`poll`] drops the chain).
+    let context = app.session.doc().ok().map(|st| st.selection.objects.clone()).zip(Insertion::of(app));
+    if let Some(ch) = app.placing.chains.get_mut(&chain) {
+        ch.left = left;
+        if let Some(context) = context {
+            ch.context = context;
+        }
+    }
+    if let Some(kept) = kept {
+        app.session.clipboard = kept;
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use image::ImageFormat::{Bmp, Png, Tiff};
     use vectorcraft_doc::{Document, NodeId};
     use vectorcraft_engine::Session;
+    use vectorcraft_engine::cmd::clipboard::{BITMAP, Flavour, PNG, TEXT};
 
     use super::*;
     use crate::Services;
@@ -324,6 +590,427 @@ mod tests {
         }
     }
 
+    /// Another app's bitmap on the system clipboard, and the text beside it, read as the desktop
+    /// hosts read them: with `converts`, the bitmap as the work that makes its flavour
+    /// ([`crate::ClipboardRead::Later`], `None`: it can't be made), which notes the thread it runs
+    /// on in `thread`. The work of each read waits for its entry of `holds` to send, when it has
+    /// one.
+    struct OtherApps {
+        bitmap: Option<Flavour>,
+        text: Option<Flavour>,
+        converts: bool,
+        thread: Arc<Mutex<Option<String>>>,
+        holds: Vec<Option<std::sync::mpsc::Receiver<()>>>,
+    }
+
+    impl crate::SystemClipboard for OtherApps {
+        fn write(&mut self, _: &[Flavour]) -> Result<(), String> {
+            Ok(())
+        }
+        fn holds_ours(&mut self) -> bool {
+            false
+        }
+        fn read(&mut self, mimes: &[&'static str]) -> Option<Flavour> {
+            self.read_deferred(mimes)?.flavour()
+        }
+        fn has(&mut self, mimes: &[&'static str]) -> bool {
+            mimes.contains(&BITMAP) || (mimes.contains(&TEXT) && self.text.is_some())
+        }
+        fn read_deferred(&mut self, mimes: &[&'static str]) -> Option<crate::ClipboardRead> {
+            if mimes.contains(&TEXT)
+                && let Some(text) = &self.text
+            {
+                return Some(crate::ClipboardRead::Now(text.clone()));
+            }
+            if !mimes.contains(&BITMAP) {
+                return None;
+            }
+            let (bitmap, thread) = (self.bitmap.clone(), self.thread.clone());
+            if !self.converts {
+                return bitmap.map(crate::ClipboardRead::Now);
+            }
+            let hold = if self.holds.is_empty() { None } else { self.holds.remove(0) };
+            Some(crate::ClipboardRead::Later(Box::new(move || {
+                if let Some(hold) = hold {
+                    let _ = hold.recv_timeout(std::time::Duration::from_secs(10));
+                }
+                *thread.lock().unwrap() = std::thread::current().name().map(str::to_string);
+                bitmap
+            })))
+        }
+    }
+
+    /// [`app`] with `bitmap` and `text` on the system clipboard ([`OtherApps`]), the view centered
+    /// on (250, 180) → the app and the thread the bitmap was converted on.
+    fn pasting_app(background: bool, bitmap: Option<Flavour>, text: Option<&str>, converts: bool) -> (VectorcraftApp, Arc<Mutex<Option<String>>>) {
+        let thread = Arc::new(Mutex::new(None));
+        let mut app = app(background);
+        let text = text.map(|t| Flavour { mime: TEXT, data: t.as_bytes().to_vec() });
+        app.services.system_clipboard = Some(Box::new(OtherApps { bitmap, text, converts, thread: thread.clone(), holds: vec![] }));
+        let v = app.view_mut().unwrap();
+        (v.center, v.fitted) = (vectorcraft_geom::Point::new(250.0, 180.0), true);
+        (app, thread)
+    }
+
+    /// A Paste the user gives decodes another app's bitmap off the UI thread: a BMP as
+    /// `clipboard.importImage` reads it, and a bitmap the host converts to PNG (Windows'
+    /// device-independent bitmap, Linux's image) converted there too. Until then the document is
+    /// unchanged, and the status bar and the busy cursor show the paste; then it is pasted as at
+    /// once, where the view was centered when the paste was given: the same object, selection, undo
+    /// step and internal clipboard. `edit.paste` from MCP and scripts reads it at once.
+    #[test]
+    fn a_bitmap_the_user_pastes_is_read_off_the_ui_thread() {
+        let bmp = Flavour { mime: "image/bmp", data: picture(Bmp, 300, 200) };
+        let png = Flavour { mime: PNG, data: picture(Png, 300, 200) };
+        for (bitmap, converts) in [(bmp, false), (png, true)] {
+            for id in ["edit.paste", "edit.pasteInPlace"] {
+                let (mut at_once, _) = pasting_app(false, Some(bitmap.clone()), None, converts);
+                crate::menus::invoke(&mut at_once, id, json!({}));
+                let (mut app, _) = pasting_app(true, Some(bitmap.clone()), None, converts);
+                crate::menus::invoke(&mut app, id, json!({}));
+                // The view moves before the bitmap is read.
+                app.view_mut().unwrap().center = vectorcraft_geom::Point::new(40.0, 30.0);
+                placed(&mut app);
+                assert!(state(&app) == state(&at_once), "{} {id}: pasted where the view was", bitmap.mime);
+            }
+            let (mut at_once, _) = pasting_app(false, Some(bitmap.clone()), None, converts);
+            crate::menus::invoke(&mut at_once, "edit.paste", json!({}));
+            let (mut app, thread) = pasting_app(true, Some(bitmap.clone()), None, converts);
+            let before = state(&app);
+            app.placing.for_user = true;
+            let r = app.run("edit.paste", json!({}));
+            app.placing.for_user = false;
+            assert_eq!(r, Ok(json!({"background": true, "status": "Pasting…"})), "{}", bitmap.mime);
+            assert_eq!(app.ui.status, "Pasting…");
+            assert!(crate::opening::long_operation_busy(&app));
+            assert!(state(&app) == before, "{}: nothing is pasted until the bitmap is read", bitmap.mime);
+            placed(&mut app);
+            assert!(state(&app) == state(&at_once), "{}: pasted as at once", bitmap.mime);
+            assert!(app.session.clipboard == at_once.session.clipboard, "{}", bitmap.mime);
+            assert!(app.ui.status.is_empty(), "{}", app.ui.status);
+            if converts {
+                assert_eq!(thread.lock().unwrap().as_deref(), Some("vectorcraft-place"));
+            }
+            // From the menu or the keys too.
+            crate::menus::invoke(&mut app, "edit.paste", json!({}));
+            assert_eq!(app.placing.pending.len(), 1);
+            placed(&mut app);
+            // MCP and scripts: at once.
+            let r = app.run("edit.paste", json!({})).unwrap();
+            assert!(r["ids"].is_array() && app.placing.pending.is_empty(), "{r}");
+            assert_eq!(app.session.doc().unwrap().selection.objects.len(), 1);
+        }
+    }
+
+    /// A bitmap that can't be read reports as at once and pastes nothing; one the host can't
+    /// convert pastes the internal clipboard as it is, or the address beside it as text, as at
+    /// once.
+    #[test]
+    fn a_pasted_bitmap_that_cant_be_read_off_the_ui_thread_reports_as_at_once() {
+        let url = "https://example.com/images/cat.png";
+        let cases = [(Some(Flavour { mime: PNG, data: b"not a picture".to_vec() }), None, false), (None, None, true), (None, Some(url), true)];
+        for (bitmap, text, converts) in cases {
+            let (mut at_once, _) = pasting_app(false, bitmap.clone(), text, converts);
+            let (mut app, _) = pasting_app(true, bitmap, text, converts);
+            for app in [&mut at_once, &mut app] {
+                app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
+                app.run("edit.copy", json!({})).unwrap();
+                crate::menus::invoke(app, "edit.paste", json!({}));
+                assert_eq!(app.placing.pending.len(), usize::from(app.placing.off_the_ui_thread));
+                placed(app);
+            }
+            assert_eq!(app.ui.status, at_once.ui.status);
+            assert!(state(&app) == state(&at_once), "text: {text:?}, converts: {converts}");
+            let st = app.session.doc().unwrap();
+            let pasted = st.selection.objects.first().and_then(|id| st.doc.node(*id)).map(|n| &n.kind);
+            match (text, converts) {
+                (Some(_), _) => assert!(matches!(pasted, Some(vectorcraft_doc::NodeKind::Text(_))), "the address is pasted as text"),
+                (None, true) => assert!(matches!(pasted, Some(vectorcraft_doc::NodeKind::Path { .. })), "the copied square is pasted"),
+                (None, false) => assert!(app.ui.status.starts_with("Couldn't paste from the clipboard: "), "{}", app.ui.status),
+            }
+        }
+    }
+
+    /// A Copy made while a pasted bitmap is read keeps the internal clipboard it made, and a paste
+    /// whose document was closed meanwhile pastes nothing, with a message.
+    #[test]
+    fn a_copy_or_a_close_while_a_pasted_bitmap_is_read() {
+        let bitmap = Some(Flavour { mime: PNG, data: picture(Png, 60, 40) });
+        let (mut app, _) = pasting_app(true, bitmap.clone(), None, true);
+        crate::menus::invoke(&mut app, "edit.paste", json!({}));
+        app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
+        app.run("edit.copy", json!({})).unwrap();
+        let copied = app.session.clipboard.clone();
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let pasted = st.doc.node(st.selection.objects[0]).unwrap();
+        assert!(matches!(pasted.kind, vectorcraft_doc::NodeKind::Image(_)), "the bitmap is pasted");
+        assert!(app.session.clipboard == copied, "the copy stays on the internal clipboard");
+        // A bitmap that can't be converted pastes the internal clipboard as it was when the paste
+        // was given, and the later copy stays on it.
+        let (mut app, _) = pasting_app(true, None, None, true);
+        let square = |app: &mut VectorcraftApp, x: u32| {
+            app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap();
+            app.run("edit.copy", json!({})).unwrap();
+        };
+        square(&mut app, 10);
+        let first = app.session.clipboard.clone();
+        crate::menus::invoke(&mut app, "edit.pasteInPlace", json!({}));
+        square(&mut app, 200);
+        let second = app.session.clipboard.clone();
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let pasted = st.doc.node(st.selection.objects[0]).and_then(|n| n.geometric_bounds()).map(|b| b.x0);
+        assert!(first != second && pasted == Some(10.0), "the first copy is pasted: {pasted:?}");
+        assert!(app.session.clipboard == second, "the later copy stays on the internal clipboard");
+        let (mut app, _) = pasting_app(true, bitmap, None, true);
+        app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
+        crate::menus::invoke(&mut app, "edit.paste", json!({}));
+        app.session.execute("file.close", &json!({})).unwrap();
+        let left = state(&app);
+        placed(&mut app);
+        assert_eq!(app.ui.status, "Couldn't paste: the document it was pasted in was closed");
+        assert!(state(&app) == left, "the other document is unchanged");
+        assert!(app.placing.chains.is_empty() && app.placing.latest_chain.is_empty(), "no paste waits");
+    }
+
+    /// Paste in Front and Paste in Back put a bitmap decoded off the UI thread next to the objects
+    /// selected when the paste was given, as at once, when another object was selected meanwhile;
+    /// a second one given before the first is pasted goes next to the first, as at once.
+    #[test]
+    fn a_paste_in_front_read_off_the_ui_thread_keeps_its_place() {
+        let bitmap = Some(Flavour { mime: PNG, data: picture(Png, 60, 40) });
+        for id in ["edit.pasteInFront", "edit.pasteInBack"] {
+            for twice in [false, true] {
+                let (mut at_once, _) = pasting_app(false, bitmap.clone(), None, true);
+                let (mut app, _) = pasting_app(true, bitmap.clone(), None, true);
+                for app in [&mut at_once, &mut app] {
+                    let rect = |app: &mut VectorcraftApp, x: u32| {
+                        app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone()
+                    };
+                    let (a, b, c) = (rect(app, 10), rect(app, 40), rect(app, 70));
+                    app.run("select.set", json!({"ids": [b]})).unwrap();
+                    crate::menus::invoke(app, id, json!({}));
+                    if twice {
+                        crate::menus::invoke(app, id, json!({}));
+                    } else {
+                        let other = if id.ends_with("Front") { a } else { c };
+                        app.run("select.set", json!({"ids": [other]})).unwrap();
+                    }
+                    placed(app);
+                }
+                // Each object in paint order: `None` for a square, the paste order for a picture.
+                let order = |app: &VectorcraftApp| {
+                    let st = app.session.doc().unwrap();
+                    let children = st.doc.layers[0].children().unwrap();
+                    let mut pictures: Vec<NodeId> =
+                        children.iter().filter(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_))).map(|n| n.id).collect();
+                    pictures.sort();
+                    children.iter().map(|n| pictures.iter().position(|p| *p == n.id)).collect::<Vec<_>>()
+                };
+                assert_eq!(order(&app), order(&at_once), "{id}, twice: {twice}");
+            }
+        }
+    }
+
+    /// A second Paste in Front given before the first is pasted goes next to the first one's
+    /// picture, also when another object was selected between the two pastes' ends, and a third
+    /// given after the first was pasted, while the second waits, goes next to the second's.
+    #[test]
+    fn a_second_paste_in_front_follows_the_first() {
+        for third in [false, true] {
+            let (mut app, _) = pasting_app(true, None, None, true);
+            let (go, hold) = channel();
+            let thread = Arc::new(Mutex::new(None));
+            let bitmap = Some(Flavour { mime: PNG, data: picture(Png, 60, 40) });
+            app.services.system_clipboard = Some(Box::new(OtherApps { bitmap, text: None, converts: true, thread, holds: vec![None, Some(hold)] }));
+            let rect = |app: &mut VectorcraftApp, x: u32| {
+                app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone()
+            };
+            let (a, b) = (rect(&mut app, 10), rect(&mut app, 40));
+            app.run("select.set", json!({"ids": [b]})).unwrap();
+            crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+            crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+            let t0 = std::time::Instant::now();
+            while app.placing.pending.len() > 1 && t0.elapsed().as_secs() < 60 {
+                poll(&mut app);
+                std::thread::yield_now();
+            }
+            assert_eq!(app.placing.pending.len(), 1, "the first paste ran");
+            if third {
+                crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+            } else {
+                app.run("select.set", json!({"ids": [a]})).unwrap();
+            }
+            let _ = go.send(());
+            placed(&mut app);
+            let st = app.session.doc().unwrap();
+            let kinds: Vec<Option<NodeId>> =
+                st.doc.layers[0].children().unwrap().iter().map(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)).then_some(n.id)).collect();
+            let pictures: Vec<NodeId> = kinds.iter().flatten().copied().collect();
+            assert!(pictures.len() == 2 + usize::from(third) && pictures.is_sorted(), "each picture is above the one before: {kinds:?}");
+            assert_eq!(kinds.iter().position(Option::is_some), Some(2), "the pictures are above the square they were pasted on: {kinds:?}");
+        }
+    }
+
+    /// A paste given beside the picture an independent paste left, while another paste waits, goes
+    /// beside that picture.
+    #[test]
+    fn a_paste_beside_a_finished_picture_does_not_follow_another_chain() {
+        let (mut app, _) = pasting_app(true, None, None, true);
+        let (go, hold) = channel();
+        let bitmap = Some(Flavour { mime: PNG, data: picture(Png, 60, 40) });
+        let clip = OtherApps { bitmap, text: None, converts: true, thread: Arc::new(Mutex::new(None)), holds: vec![None, Some(hold)] };
+        app.services.system_clipboard = Some(Box::new(clip));
+        let rect =
+            |app: &mut VectorcraftApp, x: u32| app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
+        let (x, y) = (rect(&mut app, 10), rect(&mut app, 40));
+        app.run("select.set", json!({"ids": [x]})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        app.run("select.set", json!({"ids": [y]})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        let t0 = std::time::Instant::now();
+        while app.placing.pending.len() > 1 && t0.elapsed().as_secs() < 60 {
+            poll(&mut app);
+            std::thread::yield_now();
+        }
+        assert_eq!(app.placing.pending.len(), 1, "the first paste ran");
+        // The first picture is selected.
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        let _ = go.send(());
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let ids: Vec<Option<NodeId>> =
+            st.doc.layers[0].children().unwrap().iter().map(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)).then_some(n.id)).collect();
+        let last = ids.iter().flatten().max().copied();
+        assert_eq!(ids.iter().position(|id| *id == last && last.is_some()), Some(2), "the third picture is above the first: {ids:?}");
+    }
+
+    /// Pastes that follow each other in one document follow each other there, also when a paste in
+    /// another document runs between them.
+    #[test]
+    fn pastes_in_two_documents_follow_their_own() {
+        let (mut app, _) = pasting_app(true, None, None, true);
+        let (go, hold) = channel();
+        let bitmap = Some(Flavour { mime: PNG, data: picture(Png, 60, 40) });
+        let clip = OtherApps { bitmap, text: None, converts: true, thread: Arc::new(Mutex::new(None)), holds: vec![None, Some(hold)] };
+        app.services.system_clipboard = Some(Box::new(clip));
+        let rect =
+            |app: &mut VectorcraftApp, x: u32| app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
+        let (_, b) = (rect(&mut app, 10), rect(&mut app, 40));
+        app.run("select.set", json!({"ids": [b]})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        let t0 = std::time::Instant::now();
+        while app.placing.pending.len() > 1 && t0.elapsed().as_secs() < 60 {
+            poll(&mut app);
+            std::thread::yield_now();
+        }
+        assert_eq!(app.placing.pending.len(), 1, "the first paste ran");
+        // A paste in another document, then one more in the first.
+        app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
+        let x = rect(&mut app, 10);
+        app.run("select.set", json!({"ids": [x]})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        app.session.set_active(0);
+        app.sync_views();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        let _ = go.send(());
+        placed(&mut app);
+        let st = &app.session.documents()[0];
+        let kinds: Vec<Option<NodeId>> =
+            st.doc.layers[0].children().unwrap().iter().map(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)).then_some(n.id)).collect();
+        let pictures: Vec<NodeId> = kinds.iter().flatten().copied().collect();
+        assert!(pictures.len() == 3 && pictures.is_sorted(), "each picture is above the one before: {kinds:?}");
+        assert_eq!(kinds.iter().position(Option::is_some), Some(2), "the pictures are above the square they were pasted on: {kinds:?}");
+    }
+
+    /// In Draw Inside in an isolated path, a bitmap decoded off the UI thread is pasted inside the path,
+    /// whose new clipping group stays isolated, when the user went back to Draw Normal meanwhile.
+    #[test]
+    fn a_bitmap_pasted_inside_an_isolated_path_stays_isolated() {
+        let (mut app, _) = pasting_app(true, Some(Flavour { mime: PNG, data: picture(Png, 60, 40) }), None, true);
+        let path = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 200, "height": 200})).unwrap()["id"].clone();
+        app.run("object.isolate", json!({"id": path})).unwrap();
+        app.run("select.set", json!({"ids": [path]})).unwrap();
+        app.run("view.drawMode", json!({"mode": "inside"})).unwrap();
+        crate::menus::invoke(&mut app, "edit.paste", json!({}));
+        app.run("view.drawMode", json!({"mode": "normal"})).unwrap();
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let pasted = st.selection.objects[0];
+        let group = st.doc.parent_of(pasted);
+        assert!(
+            group.is_some_and(|g| st.doc.node(g).is_some_and(|n| n.children().is_some_and(|c| c.iter().any(|n| n.id.0 == path.as_u64().unwrap())))),
+            "pasted inside the path"
+        );
+        assert_eq!(st.isolation, group, "the clipping group is isolated");
+        assert_eq!(app.session.draw_mode, vectorcraft_engine::DrawMode::Normal, "Draw Normal stays");
+    }
+
+    /// A bitmap decoded off the UI thread is pasted into the layer that was current when the paste
+    /// was given, and a layer made current meanwhile stays current.
+    #[test]
+    fn a_bitmap_read_off_the_ui_thread_is_pasted_into_its_layer() {
+        let (mut app, _) = pasting_app(true, Some(Flavour { mime: PNG, data: picture(Png, 60, 40) }), None, true);
+        let first = app.session.doc().unwrap().doc.layers[0].id;
+        app.run("layer.new", json!({})).unwrap();
+        let second = app.session.doc().unwrap().current_layer().unwrap();
+        app.run("layer.setCurrent", json!({"id": first.0})).unwrap();
+        crate::menus::invoke(&mut app, "edit.paste", json!({}));
+        app.run("layer.setCurrent", json!({"id": second.0})).unwrap();
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let pasted = st.selection.objects[0];
+        assert_eq!(st.doc.layer_containing(pasted), Some(first), "pasted into the layer current at the paste");
+        assert_eq!(st.current_layer(), Some(second), "the layer made current meanwhile stays current");
+        // With nothing selected, a Paste in Front given in another layer while the first waits
+        // goes into that layer.
+        app.run("layer.setCurrent", json!({"id": first.0})).unwrap();
+        app.run("select.none", json!({})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        app.run("layer.setCurrent", json!({"id": second.0})).unwrap();
+        app.run("select.none", json!({})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        assert_eq!(app.placing.pending.len(), 2);
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let tops: Vec<_> = [first, second]
+            .iter()
+            .map(|l| st.doc.node(*l).and_then(|n| n.children()).and_then(|c| c.last()).map(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_))))
+            .collect();
+        assert_eq!(tops, [Some(true), Some(true)], "each layer has a picture on top");
+        // A group isolated while the paste waits keeps its isolation, and the paste goes into the
+        // layer, where it was given.
+        let squares: Vec<Value> = (0..2)
+            .map(|i| app.run("shape.rectangle", json!({"x": 10 + 30 * i, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone())
+            .collect();
+        app.run("select.set", json!({"ids": squares})).unwrap();
+        let group = NodeId(app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap());
+        app.run("select.none", json!({})).unwrap();
+        crate::menus::invoke(&mut app, "edit.paste", json!({}));
+        app.run("object.isolate", json!({"id": group.0})).unwrap();
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        let pasted = st.selection.objects[0];
+        assert_eq!(st.doc.parent_of(pasted), Some(second), "pasted into the layer, not the group isolated meanwhile");
+        assert_eq!(st.isolation, Some(group), "the group stays isolated");
+        // An object selected in another layer while the paste waits makes that layer current, and
+        // it stays current after the paste.
+        app.run("object.exitIsolation", json!({})).unwrap();
+        let in_second = app.run("shape.rectangle", json!({"x": 300, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
+        app.run("layer.setCurrent", json!({"id": first.0})).unwrap();
+        let in_first = app.run("shape.rectangle", json!({"x": 300, "y": 100, "width": 20, "height": 20})).unwrap()["id"].clone();
+        app.run("select.set", json!({"ids": [in_first]})).unwrap();
+        crate::menus::invoke(&mut app, "edit.pasteInFront", json!({}));
+        app.run("select.set", json!({"ids": [in_second]})).unwrap();
+        assert_eq!(app.session.doc().unwrap().current_layer(), Some(second));
+        placed(&mut app);
+        let st = app.session.doc().unwrap();
+        assert_eq!(st.doc.layer_containing(st.selection.objects[0]), Some(first), "pasted next to the object selected at the paste");
+        assert_eq!(st.current_layer(), Some(second), "the layer of the object selected meanwhile stays current");
+    }
+
     /// The Place dialog's OK with one file, with several (the place cursor and its thumbnails), a
     /// drop on the canvas and [`crate::io::place_bytes`] read their files off the UI thread;
     /// `file.place` and `file.place.queue` from MCP and scripts read them at once, with the replies
diff --git a/crates/ui-egui/src/sysclip.rs b/crates/ui-egui/src/sysclip.rs
--- a/crates/ui-egui/src/sysclip.rs
+++ b/crates/ui-egui/src/sysclip.rs
@@ -12,7 +12,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
 use std::sync::mpsc::channel;
 
 use serde_json::{Value, json};
-use vectorcraft_engine::cmd::clipboard::{AI, BITMAP, EMF, EPS, Flavour, PASTE_ORDER, PDF, SVG, TEXT, looks_like_svg, names_a_picture};
+use vectorcraft_engine::cmd::clipboard::{AI, BITMAP, EMF, EPS, Flavour, ImageRead, PASTE_ORDER, PDF, SVG, TEXT, looks_like_svg, names_a_picture};
 
 use crate::VectorcraftApp;
 use crate::background::{self, Writer};
@@ -27,6 +27,34 @@ pub trait SystemClipboard {
     fn read(&mut self, mimes: &[&'static str]) -> Option<Flavour>;
     /// Does the clipboard hold one of `mimes`? Cheap enough to ask a few times a second.
     fn has(&mut self, mimes: &[&'static str]) -> bool;
+    /// [`Self::read`], except that a bitmap the host converts to PNG (Windows' device-independent
+    /// bitmap, the image Linux gives) comes back as the work that converts it
+    /// ([`ClipboardRead::Later`]), for a paste that runs it off the UI thread. By default every
+    /// flavour is read at once.
+    fn read_deferred(&mut self, mimes: &[&'static str]) -> Option<ClipboardRead> {
+        self.read(mimes).map(ClipboardRead::Now)
+    }
+}
+
+/// What [`SystemClipboard::read_deferred`] read.
+pub enum ClipboardRead {
+    /// The flavour.
+    Now(Flavour),
+    /// The work that makes the flavour from the bytes read (a bitmap decoded and encoded as PNG).
+    Later(MakeFlavour),
+}
+
+/// Makes a flavour from bytes read off the clipboard, on any thread; `None` when it can't be made.
+pub type MakeFlavour = Box<dyn FnOnce() -> Option<Flavour> + Send>;
+
+impl ClipboardRead {
+    /// The flavour, made at once when it is work.
+    pub fn flavour(self) -> Option<Flavour> {
+        match self {
+            Self::Now(f) => Some(f),
+            Self::Later(make) => make(),
+        }
+    }
 }
 
 /// Makes a second system-clipboard handle, to check off the UI thread whether Paste has something
@@ -50,6 +78,17 @@ impl Publishing {
     pub(crate) fn is_pending(&self) -> bool {
         self.pending
     }
+
+    /// The number of the latest Copy or Cut: it changes with each Copy or Cut, and with each copy
+    /// through egui that drops the formats not written yet.
+    pub(crate) fn latest(&self) -> u64 {
+        self.latest.load(Ordering::SeqCst)
+    }
+}
+
+/// Does a flavour of type `mime` load with `clipboard.importImage` ([`import_command`])?
+pub(crate) fn imports_as_image(mime: &str) -> bool {
+    ![TEXT, SVG, PDF, AI, EPS, EMF].contains(&mime)
 }
 
 /// The command (and its params) that loads `f` into the internal clipboard, centred on `center`.
@@ -208,44 +247,91 @@ impl VectorcraftApp {
     /// copy, and so does what we are still making the formats of. An error means nothing should be
     /// pasted.
     pub(crate) fn adopt_system_clipboard(&mut self) -> Result<(), String> {
+        self.adopt_system_clipboard_or_defer(false).map(drop)
+    }
+
+    /// [`Self::adopt_system_clipboard`]. With `defer`, a bitmap is not loaded here: the work that
+    /// makes its flavour comes back (a bitmap the host converts to PNG is converted by it,
+    /// [`SystemClipboard::read_deferred`]; text that names the picture is the flavour when the
+    /// picture can't be made), and the internal clipboard is left as it is.
+    pub(crate) fn adopt_system_clipboard_or_defer(&mut self, defer: bool) -> Result<Option<MakeFlavour>, String> {
         // A paste command may run before the next tick consumes an image signal: this is that
         // paste, so a queued `PasteImage` must not read the clipboard a second time.
         self.clipboard_image_request = None;
         let pasted = self.clipboard_in.take();
         let center = self.view().map(|v| [v.center.x, v.center.y]);
         let (cmd, params) = match (pasted, self.services.system_clipboard.as_mut()) {
+            (Some(f), _) if defer && imports_as_image(f.mime) => return Ok(Some(Box::new(move || Some(f)))),
             // A picture or file the host read from the paste.
             (Some(f), _) if f.mime != TEXT => import_command(&f, center),
             (_, Some(cb)) => {
                 if self.clipboard_publishing.pending || cb.holds_ours() {
-                    return Ok(());
+                    return Ok(None);
                 }
-                let Some(f) = cb.read(&PASTE_ORDER) else { return Ok(()) };
+                let mut read = |mimes: &[&'static str]| if defer { cb.read_deferred(mimes) } else { cb.read(mimes).map(ClipboardRead::Now) };
+                let f = match read(&PASTE_ORDER) {
+                    Some(ClipboardRead::Now(f)) => f,
+                    Some(ClipboardRead::Later(make)) => return Ok(Some(make)),
+                    None => return Ok(None),
+                };
                 // Text that only names a picture (its address, or its image markup) goes with a
                 // browser's copied picture: the picture.
                 let text = String::from_utf8_lossy(&f.data);
                 // Blank text is what some hosts put beside a copied picture. It is not the paste.
-                let f = if f.mime == TEXT && (text.trim().is_empty() || names_a_picture(&text)) { cb.read(&[BITMAP]).unwrap_or(f) } else { f };
+                let f = if f.mime == TEXT && (text.trim().is_empty() || names_a_picture(&text)) {
+                    match read(&[BITMAP]) {
+                        Some(ClipboardRead::Now(picture)) => picture,
+                        // The text, when the picture can't be made.
+                        Some(ClipboardRead::Later(make)) => return Ok(Some(Box::new(move || make().or(Some(f))))),
+                        None => f,
+                    }
+                } else {
+                    f
+                };
+                if defer && imports_as_image(f.mime) {
+                    return Ok(Some(Box::new(move || Some(f))));
+                }
                 import_command(&f, center)
             }
             // Text only: SVG markup is art, other text is left alone.
             (pasted, None) => {
                 let text =
                     pasted.map(|f| String::from_utf8_lossy(&f.data).into_owned()).or_else(|| self.services.clipboard_read.as_mut().and_then(|f| f()));
-                let Some(text) = text.filter(|t| looks_like_svg(t)) else { return Ok(()) };
+                let Some(text) = text.filter(|t| looks_like_svg(t)) else { return Ok(None) };
                 if self.clipboard_published.as_deref() == Some(text.as_str()) {
-                    return Ok(());
+                    return Ok(None);
                 }
                 self.clipboard_published = Some(text.clone());
                 ("clipboard.importSvg", json!({ "svg": text, "center": center }))
             }
         };
+        self.session.execute(cmd, &params).map(|_| None).map_err(|e| {
+            self.clipboard_published = None;
+            format!("Couldn't paste from the clipboard: {e}")
+        })
+    }
+
+    /// Before a paste whose flavour was made off the UI thread ([`crate::placing::adopt_for_paste`]):
+    /// `f` replaces the internal clipboard, centered on `center`, as
+    /// [`Self::adopt_system_clipboard`] loads it. An error means nothing should be pasted.
+    pub(crate) fn adopt_flavour(&mut self, f: &Flavour, center: Option<[f64; 2]>) -> Result<(), String> {
+        let (cmd, params) = import_command(f, center);
         self.session.execute(cmd, &params).map(drop).map_err(|e| {
             self.clipboard_published = None;
             format!("Couldn't paste from the clipboard: {e}")
         })
     }
 
+    /// [`Self::adopt_flavour`] for a bitmap of type `mime` decoded off the UI thread: the image `read`
+    /// replaces the internal clipboard, centered on `center`.
+    pub(crate) fn adopt_image_read(&mut self, mime: &str, center: Option<[f64; 2]>, read: Result<ImageRead, String>) -> Result<(), String> {
+        let params = json!({ "mime": mime, "center": center });
+        read.and_then(|read| self.session.import_image_read(read, &params).map(drop).map_err(|e| e.to_string())).map_err(|e| {
+            self.clipboard_published = None;
+            format!("Couldn't paste from the clipboard: {e}")
+        })
+    }
+
     /// The text on the system clipboard once the formats a Copy or Cut is making are written, for a
     /// paste into a text field or type.
     pub(crate) fn written_clipboard_text(&mut self) -> Option<String> {
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -594,6 +594,9 @@ the Windows clipboard and, with SVG, from the macOS pasteboard, and an `.eps` fi
 and pastes nothing. Cmd+V also pastes a bitmap or PDF with no text beside it (on the key's release, as egui sends no
 Paste event for it). Screenshots paste on every platform: Windows reads `PNG`, else `CF_DIBV5`/`CF_DIB`, macOS the
 pasteboard's PNG or TIFF, Linux `image/png`, and the web the picture the browser's paste event carries (#457).
+In the desktop app a Paste from the menus or the keys (`ui.key`) decodes a bitmap off the UI thread: `ui.inspect` lists
+`clipboard` in `placing` until it is pasted, and an import error then shows in the status bar. `engine.execute`
+`edit.paste*` reads it at once.
 
 File menu details: `file.openRecent1`…`file.openRecent30` open the recent files the `recentFilesCount` preference
 lists (0 hides them all; `file.recentFiles` returns the listed paths), `file.reveal` shows the document's file in the
diff --git a/docs/mcp.md b/docs/mcp.md
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -3526,8 +3526,15 @@ also before its formats are published. While a text field or the Type tool has t
 them. Windows carries every format both ways and pastes a bitmap from PNG, else
 from the device-independent bitmap screenshots copy (`CF_DIBV5`, then `CF_DIB`; alpha kept, opaque when it is zero
 throughout, at most 32768 px a side); macOS and Linux carry one format (the text, else the PNG) and paste text and
-bitmaps (macOS the pasteboard's PNG, else TIFF, as screenshots copy them). On the web Copy publishes SVG text, and
-Paste takes SVG text and the pictures and files a paste carries (a screenshot pastes as an embedded image). A `.ai`
+bitmaps (macOS the pasteboard's PNG, else TIFF, as screenshots copy them). A Paste from the menus or the keys decodes
+a bitmap (a copied image file too) on the thread that reads placed files, with "Pasting" in the status bar and the
+busy spinner, and pastes it once it is decoded, in the document it was given in: Windows' device-independent bitmap and
+Linux's image are encoded as PNG there, and the bitmap is read as `clipboard.importImage` reads it. The paste is
+centered where the view was and goes where new art went (the current layer, the isolated group, the object Draw
+Inside drew into), and Paste in Front and Paste in Back paste next
+to the objects selected when it was given (a second paste given before the first is pasted goes next to the first).
+A Copy or Cut made meanwhile keeps the internal clipboard it made. `edit.paste*` over MCP and scripts reads the bitmap at once.
+On the web Copy publishes SVG text, and Paste takes SVG text and the pictures and files a paste carries (a screenshot pastes as an embedded image). A `.ai`
 or `.ait` file that holds a PDF, copied in a file manager or carried by a paste, is loaded with `clipboard.importPdf
 {editingData: true}`, and its objects go into the active layer.
 
diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md
--- a/docs/target-app-parity.md
+++ b/docs/target-app-parity.md
@@ -960,6 +960,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
   - File › Place reads Word `.docx` documents as area type with the same formatting, taken from the document defaults, the paragraph and character styles (their toggle properties reversing each other) and direct formatting, the theme's fonts included. Table rows are paragraphs with their cells separated by tabs; hidden and deleted text, field instructions, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests_textimport.rs`).
   - A Word paragraph justified with a low, medium or high kashida length places justified with Insert Kashidas set to Short, Medium or Long (`file.place`, `crates/engine/src/tests_textimport.rs`).
   - On the desktop, File › Place reads and decodes files off the UI thread, one at a time, and places each once it is read, with "Placing" in the status bar and the busy spinner until then: the Place dialog's OK, a file dropped on the canvas, a click or drag of the place cursor, and the OK of the PDF, DXF and text import options. The files and thumbnails the place cursor is loaded with are read the same way, and the Place dialog's lines about its files are read on a thread of their own. Files are placed in the order they were started; a file is not placed while a dialog is open or a drag is under way, and a file whose document was closed meanwhile is not placed. `file.place` and `file.place.queue` over MCP and scripts reply as before.
+  - On the desktop, a Paste from the menus or the keys decodes another app's bitmap, or a copied image file, on the same thread, with "Pasting" in the status bar and the busy spinner, and pastes it once it is decoded: Windows' device-independent bitmap and Linux's image are encoded as PNG there instead of on the UI thread. A Copy or Cut made meanwhile keeps its internal clipboard. `edit.paste*` over MCP and scripts reads the bitmap at once.
   - On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
   - On the desktop with Enable Background Export on (Preferences › File Handling), File › Package… collects the linked files and fonts and writes the package from the document as it was at OK on the thread that writes background saves and exports, with "Packaging" in the status bar and the busy spinner until it is written; a save of unsaved changes that Package starts is written first. `file.package` over MCP and scripts replies as before.
   - On the desktop with Enable Background Export on, Export for Screens and the Asset Export panel's Export encode and write their files (into the folder, or one ZIP in it) from the document as it was at OK on the thread that writes background saves and exports, with "Exporting" in the status bar and the export progress dialog, which counts the files and whose Cancel stops before the next file. `document.exportForScreens` and `assets.export` over MCP and scripts reply as before.
