Fix #1499: PDF and EPS another app copied paste as editable vector art

Paste read PDF only from the Windows clipboard and EPS nowhere, so on macOS a copied
equation or drawing pasted nothing new (the last thing copied in the app came back).
- macOS: the pasteboard's PDF, EPS and SVG are read, ahead of text and bitmaps.
- Windows: EPS is read beside PDF.
- Everywhere: an .eps file copied in a file manager pastes.
- clipboard.importEps loads an EPS into the clipboard as document.open reads one.
This commit is contained in:
bflatastic committed 2026-10-13 06:34:12 +07:00
1 parent a046fd30fe
commit b3f6edddfe
23 files changed
+191 -58

No files matched your search

+93 -46
View File
@@ -2,20 +2,21 @@
//! (`Services::system_clipboard`).
//!
//! - Windows: text, SVG, PDF, PNG and an opaque bitmap (for apps that don't read PNG) in one go;
//! Paste reads each of them, a bitmap as PNG, else as the device-independent bitmap that
//! Paste reads each of them and EPS, a bitmap as PNG, else as the device-independent bitmap that
//! screenshots and most apps copy (`CF_DIBV5`, `CF_DIB`; the system makes them of a `CF_BITMAP`).
//! - macOS and Linux: one format at a time, the text (a type-only copy's text or the SVG markup),
//! else the PNG as a bitmap; Paste reads text and bitmaps (on macOS the pasteboard's PNG or TIFF
//! as they are, as screenshots copy them). On Wayland, arboard's data-control
//! else the PNG as a bitmap; Paste reads text and bitmaps, and on macOS the pasteboard's SVG, PDF
//! and EPS as vector art (#1499) and its PNG or TIFF as they are, as screenshots copy them (on
//! Linux arboard reads no other formats). On Wayland, arboard's data-control
//! backend reads the compositor's clipboard (X11 through XWayland only sees what X11 apps
//! copied), falling back to X11 where the compositor lacks the protocol.
//! - Everywhere: files copied in a file manager paste as the first one that is art (SVG, PDF, a
//! metafile or a bitmap), before the clipboard's other formats (the files' paths as text).
//! - Everywhere: files copied in a file manager paste as the first one that is art (SVG, PDF, EPS,
//! a metafile or a bitmap), before the clipboard's other formats (the files' paths as text).
use std::io::{Cursor, Read};
use std::path::PathBuf;
use vectorcraft_engine::cmd::clipboard::{BITMAP, FILE_HEAD, Flavour, PNG, TEXT, file_flavour};
use vectorcraft_engine::cmd::clipboard::{BITMAP, EPS, FILE_HEAD, Flavour, PDF, PNG, SVG, TEXT, file_flavour};
use vectorcraft_ui_egui::SystemClipboard;
/// This platform's system clipboard.
@@ -27,6 +28,34 @@ pub fn system_clipboard() -> Box<dyn SystemClipboard> {
cb
}
/// The registered Windows clipboard formats a flavour is written under (and read from, first held
/// first).
#[cfg_attr(not(windows), allow(dead_code))]
fn windows_formats(mime: &str) -> &'static [&'static str] {
match mime {
SVG => &["image/svg+xml"],
PDF => &["Portable Document Format", "application/pdf"],
EPS => &["Encapsulated PostScript", "application/postscript"],
PNG => &["PNG"],
_ => &[],
}
}
/// The macOS pasteboard types Paste reads for `mime` (`image/*`: any bitmap), best first, each with
/// the MIME type of its data. They are the system's uniform type identifiers (the values of
/// `NSPasteboardTypePDF`, `NSPasteboardTypePNG` and `NSPasteboardTypeTIFF` among them) and the
/// older name some apps still give PDF. Text is read through arboard.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn pasteboard_types(mime: &str) -> &'static [(&'static str, &'static str)] {
match mime {
SVG => &[("public.svg-image", SVG)],
PDF => &[("com.adobe.pdf", PDF), ("Apple PDF pasteboard type", PDF)], // brand-ok: the system's type identifier
EPS => &[("com.adobe.encapsulated-postscript", EPS)], // brand-ok: the system's type identifier
BITMAP => &[("public.png", PNG), ("public.tiff", "image/tiff")],
_ => &[],
}
}
/// The text flavour among `flavours`.
fn text_of(flavours: &[Flavour]) -> Option<String> {
flavours.iter().find(|f| f.mime == TEXT).map(|f| String::from_utf8_lossy(&f.data).into_owned())
@@ -127,27 +156,15 @@ mod win {
use std::num::NonZeroU32;
use clipboard_win::{formats, options::NoClear, raw};
use vectorcraft_engine::cmd::clipboard::{PDF, SVG};
use super::*;
/// The registered clipboard formats a flavour is written under (and read from, first held
/// first).
fn names(mime: &str) -> &'static [&'static str] {
match mime {
SVG => &["image/svg+xml"],
PDF => &["Portable Document Format", "application/pdf"],
PNG => &["PNG"],
_ => &[],
}
}
/// The device-independent bitmaps, the one that carries alpha first. Each is there whenever the
/// other or a `CF_BITMAP` is: the system converts between them.
const DIBS: [u32; 2] = [formats::CF_DIBV5, formats::CF_DIB];
fn formats_of(mime: &str) -> impl Iterator<Item = u32> {
names(mime).iter().filter_map(|n| raw::register_format(n)).map(NonZeroU32::get)
windows_formats(mime).iter().filter_map(|n| raw::register_format(n)).map(NonZeroU32::get)
}
fn err(e: impl std::fmt::Display) -> String {
@@ -350,27 +367,30 @@ mod portable {
if let Some(f) = cb.get().file_list().ok().and_then(|paths| copied_file(paths, mimes)) {
return Some(f);
}
mimes.iter().find_map(|m| match *m {
TEXT => cb.get_text().ok().filter(|t| !t.is_empty()).map(|t| Flavour { mime: TEXT, data: t.into_bytes() }),
BITMAP => {
#[cfg(target_os = "macos")]
if let Some(f) = super::mac::bitmap() {
return Some(f);
mimes.iter().find_map(|m| {
#[cfg(target_os = "macos")]
if let Some(f) = super::mac::read(m) {
return Some(f);
}
match *m {
TEXT => cb.get_text().ok().filter(|t| !t.is_empty()).map(|t| 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 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)
_ => None,
}
_ => None,
})
}
/// Text, and on macOS a bitmap (by the pasteboard's types). Linux has no cheap way to ask
/// for a bitmap (reading one costs as much as pasting it): the paste keys take it, the
/// menu item waits for text.
/// Text, and on macOS vector art or a bitmap (by the pasteboard's types). Linux has no
/// cheap way to ask for a bitmap (reading one costs as much as pasting it): the paste keys
/// take it, the menu item waits for text.
fn has(&mut self, mimes: &[&'static str]) -> bool {
#[cfg(target_os = "macos")]
if mimes.contains(&BITMAP) && super::mac::has_bitmap() {
if super::mac::has(mimes) {
return true;
}
mimes.contains(&TEXT) && self.cb().is_ok_and(|cb| cb.get_text().is_ok_and(|t| !t.is_empty()))
@@ -378,7 +398,8 @@ mod portable {
}
}
/// The macOS pasteboard's bitmaps as they are (arboard reads TIFF only, and decodes it).
/// The macOS pasteboard's vector art and bitmaps as they are (arboard reads text, and of the
/// pictures TIFF only, decoded).
#[cfg(target_os = "macos")]
mod mac {
use objc2::rc::autoreleasepool;
@@ -387,24 +408,20 @@ mod mac {
use super::*;
/// The bitmap types Paste reads, best first (the values of `NSPasteboardTypePNG` and
/// `NSPasteboardTypeTIFF`), with their MIME types.
const TYPES: [(&str, &str); 2] = [("public.png", PNG), ("public.tiff", "image/tiff")];
/// Does the pasteboard hold a bitmap? Asks for its types only.
pub fn has_bitmap() -> bool {
/// Does the pasteboard hold one of `mimes` ([`pasteboard_types`])? Asks for its types only.
pub fn has(mimes: &[&'static str]) -> bool {
autoreleasepool(|_| {
let types = TYPES.map(|(t, _)| NSString::from_str(t));
let types: Vec<_> = mimes.iter().flat_map(|m| pasteboard_types(m)).map(|(t, _)| NSString::from_str(t)).collect();
let types: Vec<&NSString> = types.iter().map(|t| &**t).collect();
NSPasteboard::generalPasteboard().availableTypeFromArray(&NSArray::from_slice(&types)).is_some()
!types.is_empty() && NSPasteboard::generalPasteboard().availableTypeFromArray(&NSArray::from_slice(&types)).is_some()
})
}
/// The pasteboard's bitmap, PNG before TIFF.
pub fn bitmap() -> Option<Flavour> {
/// The pasteboard's data for `mime`, the first of its types held ([`pasteboard_types`]).
pub fn read(mime: &str) -> Option<Flavour> {
autoreleasepool(|_| {
let pasteboard = NSPasteboard::generalPasteboard();
TYPES.into_iter().find_map(|(t, mime)| {
pasteboard_types(mime).iter().find_map(|&(t, mime)| {
let data = pasteboard.dataForType(&NSString::from_str(t))?.to_vec();
(!data.is_empty()).then_some(Flavour { mime, data })
})
@@ -414,7 +431,7 @@ mod mac {
#[cfg(test)]
mod tests {
use vectorcraft_engine::cmd::clipboard::{PASTE_ORDER, SVG};
use vectorcraft_engine::cmd::clipboard::{EMF, PASTE_ORDER};
use super::*;
@@ -448,6 +465,36 @@ mod tests {
assert!(copied_file(vec![], &PASTE_ORDER).is_none());
}
/// What another app copied as vector art is asked for under the names the platforms give it,
/// and read as the flavour the paste imports (#1499).
#[test]
fn vector_art_is_read_under_each_platforms_names() {
let mac = |m| pasteboard_types(m).iter().map(|t| t.0).collect::<Vec<_>>();
assert_eq!(mac(PDF), ["com.adobe.pdf", "Apple PDF pasteboard type"]);
assert_eq!(mac(EPS), ["com.adobe.encapsulated-postscript"]);
assert_eq!(mac(SVG), ["public.svg-image"]);
assert_eq!(pasteboard_types(BITMAP), [("public.png", PNG), ("public.tiff", "image/tiff")]);
assert!(windows_formats(EPS).contains(&"Encapsulated PostScript") && windows_formats(PDF).contains(&"Portable Document Format"));
for asked in PASTE_ORDER {
// Each type's data is the flavour asked for (a bitmap its own type): what
// `clipboard.import*` then reads. Text and metafiles come another way.
assert!(pasteboard_types(asked).iter().all(|(_, mime)| *mime == asked || asked == BITMAP), "{asked}");
assert_eq!(pasteboard_types(asked).is_empty(), [TEXT, EMF].contains(&asked), "{asked}");
}
// Vector art before text, text before bitmaps.
let at = |m| PASTE_ORDER.iter().position(|x| *x == m).unwrap();
assert!(at(PDF) < at(EPS) && at(EPS) < at(TEXT) && at(TEXT) < at(BITMAP));
}
/// An .eps file copied in a file manager pastes as EPS.
#[test]
fn a_copied_eps_file_pastes() {
let eps = dir("eps").join("equation.eps");
std::fs::write(&eps, "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 10 10\n0 0 10 10 rectfill\n").unwrap();
assert_eq!(copied_file(vec![eps.clone()], &PASTE_ORDER).map(|f| f.mime), Some(EPS));
assert!(copied_file(vec![eps], &[BITMAP, TEXT]).is_none());
}
#[test]
fn a_crlf_left_by_the_uri_list_is_ignored() {
let red = dir("crlf").join("red.png");
+1 -1
View File
@@ -21,7 +21,7 @@ use vectorcraft_color::Swatch;
use vectorcraft_doc::{Document, GraphicStyle, ImageBlob, Node, NodeId, PatternDef, Symbol, TextStyleDef};
use vectorcraft_geom::Rect;
pub use flavours::{BITMAP, EMF, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address, names_a_picture};
pub use flavours::{BITMAP, EMF, EPS, FILE_HEAD, Flavour, PASTE_ORDER, PDF, PNG, SVG, TEXT, file_flavour, is_address, names_a_picture};
pub(crate) use resources::SwatchChoices;
use super::*;
+34 -5
View File
@@ -1,5 +1,5 @@
//! Clipboard flavours: what Copy offers other apps besides SVG (PNG, PDF and plain text) and what
//! Paste takes from them (bitmaps, plain text, PDF and Windows metafiles).
//! Paste takes from them (bitmaps, plain text, PDF, EPS and Windows metafiles).
//!
//! The UI owns the platform clipboard (`Services::system_clipboard` in the egui frontend): on Copy
//! it publishes [`Session::clipboard_flavours`]; before a Paste it turns what another app put on
@@ -27,9 +27,12 @@ pub const BITMAP: &str = "image/*";
/// An EMF (or WMF) picture: what Windows apps copy as vector art (the system clipboard reads it
/// where the platform offers it).
pub const EMF: &str = "image/emf";
/// An EPS (or other PostScript) picture: what drawing and typesetting apps copy as vector art
/// beside, or instead of, a PDF (#1499).
pub const EPS: &str = "application/postscript";
/// What Paste reads from other apps, best first: vector art before text, text before bitmaps
/// (word processors offer a picture of copied text too).
pub const PASTE_ORDER: [&str; 5] = [SVG, PDF, EMF, TEXT, BITMAP];
pub const PASTE_ORDER: [&str; 6] = [SVG, PDF, EPS, EMF, TEXT, BITMAP];
/// Is `text` only an address (a web page's, an image's, a file's)? A browser's Copy Image puts the
/// image's address as text next to the picture: then the picture is what Paste takes (#597). An
/// address alone pastes as text, and nothing is fetched.
@@ -67,13 +70,14 @@ fn only_images(html: &str) -> bool {
pub const FILE_HEAD: usize = 4096;
/// What a file copied in a file manager pastes as, if one of `mimes` (`image/*`: any bitmap):
/// SVG, PDF, a Windows metafile or a bitmap (its own `image/…` type), told from its first
/// SVG, PDF, EPS, a Windows metafile or a bitmap (its own `image/…` type), told from its first
/// [`FILE_HEAD`] bytes and its name. The file's bytes are that flavour's data.
pub fn file_flavour(name: &str, head: &[u8], mimes: &[&str]) -> Option<&'static str> {
let f = fileio::detect(name, head)?;
let (asked, mime) = match f.id {
"svg" => (SVG, SVG),
"pdf" => (PDF, PDF),
"eps" => (EPS, EPS),
"emf" | "wmf" => (EMF, EMF),
_ if f.raster => (BITMAP, f.mime),
_ => return None,
@@ -167,6 +171,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
import_emf
),
cmd!(
query "clipboard.importEps",
"Load EPS into Clipboard",
[],
None,
"{dataBase64 (EPS or other PostScript bytes), center?: [x, y]} replace the clipboard with the picture's objects as document.open reads an EPS (editable paths, type and images, with the images, patterns and swatches they use), centred on `center` (default: the first artboard) → {count, warnings}; then run edit.paste",
has_doc,
import_eps
),
]
}
@@ -345,10 +358,26 @@ fn import_emf(s: &mut Session, p: &Value) -> Result<Value> {
if vectorcraft_metafile::sniff(&bytes).is_none() {
return Err(bad(C, "the data isn't an EMF or WMF picture"));
}
let loaded = fileio::load("Clipboard.emf", &bytes).map_err(|e| bad(C, e.to_string()))?;
import_picture(s, p, C, "Clipboard.emf", &bytes)
}
fn import_eps(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "clipboard.importEps";
let bytes = data_param(p, C)?;
if !vectorcraft_pdf::is_postscript(&bytes) {
return Err(bad(C, "the data isn't an EPS or PostScript picture"));
}
import_picture(s, p, C, "Clipboard.eps", &bytes)
}
/// Replace the clipboard with the objects of the picture `bytes`, read as the file `name` is
/// opened (command `cmd`).
fn import_picture(s: &mut Session, p: &Value, cmd: &str, name: &str, bytes: &[u8]) -> Result<Value> {
let mut loaded = fileio::load(name, bytes).map_err(|e| bad(cmd, e.to_string()))?;
loaded.doc.drop_edit_modes();
let clip = Clipboard::from_document(&loaded.doc);
if clip.is_empty() {
return Err(bad(C, "the picture has no objects"));
return Err(bad(cmd, "the picture has no objects"));
}
let count = s.load_clipboard(clip, p)?;
Ok(json!({ "count": count, "warnings": loaded.warnings }))
+7
View File
@@ -1290,6 +1290,13 @@ proptest! {
survive("mutated EPS PostScript", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
}
/// A damaged EPS another app copied: Paste reads it as an error or as objects.
#[test]
fn pasted_eps_never_panics(cut in 0usize..400_000, edits in prop::collection::vec((0usize..400_000, any::<u8>()), 0..12)) {
static SAMPLE: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new();
paste_from_elsewhere("clipboard.importEps", json!({"dataBase64": mutated_b64(SAMPLE.get_or_init(foreign_eps_sample).clone(), cut, &edits)}))?;
}
/// Hostile PostScript programs: operators in any order with any operands, opened and placed;
/// as files in the legacy Illustrator format too (the creator line is what turns on its
/// documented `u` … `U` groups), the groups unbalanced, deep and among clips; in a layer, which
+1
View File
@@ -4683,3 +4683,4 @@ mark-style Japanese Japonské
@msg unknown parameter `{key}`; did you mean `{near}`? neznámý parametr `{key}`; mysleli jste `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters neznámý parametr `{key}`; příkaz nemá žádné parametry
@msg unknown parameter `{key}`; the command takes: {keys} neznámý parametr `{key}`; příkaz přijímá: {keys}
Load EPS into Clipboard Načíst EPS do schránky
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4206,3 +4206,4 @@ theme Appearance Erscheinungsbild
@msg unknown parameter `{key}`; did you mean `{near}`? unbekannter Parameter `{key}`; meinten Sie `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters unbekannter Parameter `{key}`; der Befehl hat keine Parameter
@msg unknown parameter `{key}`; the command takes: {keys} unbekannter Parameter `{key}`; der Befehl akzeptiert: {keys}
Load EPS into Clipboard EPS in die Zwischenablage laden
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4623,3 +4623,4 @@ mark-style Japanese Japonesas
@msg unknown parameter `{key}`; did you mean `{near}`? parámetro desconocido `{key}`; ¿quiso decir `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters parámetro desconocido `{key}`; el comando no tiene parámetros
@msg unknown parameter `{key}`; the command takes: {keys} parámetro desconocido `{key}`; el comando acepta: {keys}
Load EPS into Clipboard Cargar EPS en el portapapeles
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4682,3 +4682,4 @@ file-filter VectorCraft Template Modèle VectorCraft
gradient-kind Radial Radial
mark-style Japanese Japonais
theme Appearance Apparence
Load EPS into Clipboard Charger un EPS dans le presse-papiers
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4649,3 +4649,4 @@ theme Appearance Kinézet
@msg unknown parameter `{key}`; did you mean `{near}`? ismeretlen paraméter: `{key}`; erre gondolt: `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters ismeretlen paraméter: `{key}`; a parancsnak nincs paramétere
@msg unknown parameter `{key}`; the command takes: {keys} ismeretlen paraméter: `{key}`; a parancs ezeket fogadja el: {keys}
Load EPS into Clipboard EPS betöltése a vágólapra
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4251,3 +4251,4 @@ lighting Light Luce
@msg unknown parameter `{key}`; did you mean `{near}`? parametro sconosciuto `{key}`; forse intendevate `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters parametro sconosciuto `{key}`; il comando non ha parametri
@msg unknown parameter `{key}`; the command takes: {keys} parametro sconosciuto `{key}`; il comando accetta: {keys}
Load EPS into Clipboard Carica EPS negli appunti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4245,3 +4245,4 @@ lighting Light ライト
@msg unknown parameter `{key}`; did you mean `{near}`? `{key}` は不明なパラメーターです。`{near}` のことですか?
@msg unknown parameter `{key}`; the command takes no parameters `{key}` は不明なパラメーターです。このコマンドにパラメーターはありません
@msg unknown parameter `{key}`; the command takes: {keys} `{key}` は不明なパラメーターです。このコマンドで使えるパラメーター: {keys}
Load EPS into Clipboard EPS をクリップボードに読み込む
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4215,3 +4215,4 @@ lighting Light 광원
@msg unknown parameter `{key}`; did you mean `{near}`? 알 수 없는 매개변수 `{key}`: `{near}`을(를) 의미하셨습니까?
@msg unknown parameter `{key}`; the command takes no parameters 알 수 없는 매개변수 `{key}`: 이 명령에는 매개변수가 없습니다
@msg unknown parameter `{key}`; the command takes: {keys} 알 수 없는 매개변수 `{key}`: 이 명령이 받는 매개변수: {keys}
Load EPS into Clipboard EPS를 클립보드로 불러오기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4526,3 +4526,4 @@ mark-style Japanese Japońskie
@msg unknown parameter `{key}`; did you mean `{near}`? nieznany parametr `{key}`; czy chodziło o `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters nieznany parametr `{key}`; polecenie nie przyjmuje parametrów
@msg unknown parameter `{key}`; the command takes: {keys} nieznany parametr `{key}`; polecenie przyjmuje: {keys}
Load EPS into Clipboard Wczytaj EPS do schowka
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3692,3 +3692,4 @@ axis Both Ambos
@msg unknown parameter `{key}`; did you mean `{near}`? parâmetro desconhecido `{key}`; você quis dizer `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters parâmetro desconhecido `{key}`; o comando não tem parâmetros
@msg unknown parameter `{key}`; the command takes: {keys} parâmetro desconhecido `{key}`; o comando aceita: {keys}
Load EPS into Clipboard Carregar EPS na Área de Transferência
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4247,3 +4247,4 @@ lighting Light Свет
@msg unknown parameter `{key}`; did you mean `{near}`? неизвестный параметр `{key}`; возможно, имелся в виду `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters неизвестный параметр `{key}`; у команды нет параметров
@msg unknown parameter `{key}`; the command takes: {keys} неизвестный параметр `{key}`; команда принимает: {keys}
Load EPS into Clipboard Загрузить EPS в буфер обмена
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4231,3 +4231,4 @@ lighting Light Світло
@msg unknown parameter `{key}`; did you mean `{near}`? невідомий параметр `{key}`; можливо, ви мали на увазі `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters невідомий параметр `{key}`; команда не має параметрів
@msg unknown parameter `{key}`; the command takes: {keys} невідомий параметр `{key}`; команда приймає: {keys}
Load EPS into Clipboard Завантажити EPS у буфер обміну
Can't render this file because it contains an unexpected character in line 659 and column 40.
+1
View File
@@ -4233,3 +4233,4 @@ theme Appearance Giao diện
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg {tool} isn't in a toolbar group of several tools {tool} không nằm trong nhóm thanh công cụ có nhiều công cụ
Load EPS into Clipboard Tải EPS vào bảng tạm
Can't render this file because it contains an unexpected character in line 3584 and column 10.
+1
View File
@@ -3842,3 +3842,4 @@ lighting Light 光源
@msg unknown parameter `{key}`; did you mean `{near}`? 未知参数 `{key}`;您是否想输入 `{near}`?
@msg unknown parameter `{key}`; the command takes no parameters 未知参数 `{key}`;该命令没有参数
@msg unknown parameter `{key}`; the command takes: {keys} 未知参数 `{key}`;该命令接受:{keys}
Load EPS into Clipboard 将 EPS 载入剪贴板
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3623,3 +3623,4 @@ lighting Light 光源
@msg unknown parameter `{key}`; did you mean `{near}`? 未知的參數 `{key}`;您是指 `{near}` 嗎?
@msg unknown parameter `{key}`; the command takes no parameters 未知的參數 `{key}`;此指令沒有參數
@msg unknown parameter `{key}`; the command takes: {keys} 未知的參數 `{key}`;此指令接受:{keys}
Load EPS into Clipboard 將 EPS 載入剪貼簿
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -2
View File
@@ -1,6 +1,6 @@
//! The system clipboard's formats: Copy and Cut publish every flavour the engine makes
//! ([`Session::clipboard_flavours`](vectorcraft_engine::Session::clipboard_flavours): text, SVG,
//! PDF, PNG), and a Paste first turns what another app copied (SVG, PDF, an EMF, text, a bitmap,
//! PDF, PNG), and a Paste first turns what another app copied (SVG, PDF, EPS, an EMF, text, a bitmap,
//! or a copied file that is one of them) into the internal clipboard with the `clipboard.import*`
//! commands. The host installs the platform side as
//! [`Services::system_clipboard`](crate::Services::system_clipboard); without it (the web) Copy
@@ -8,7 +8,7 @@
//! reads from a paste itself ([`VectorcraftApp::paste_from_host`]).
use serde_json::{Value, json};
use vectorcraft_engine::cmd::clipboard::{BITMAP, EMF, Flavour, PASTE_ORDER, PDF, SVG, TEXT, looks_like_svg, names_a_picture};
use vectorcraft_engine::cmd::clipboard::{BITMAP, EMF, EPS, Flavour, PASTE_ORDER, PDF, SVG, TEXT, looks_like_svg, names_a_picture};
use crate::VectorcraftApp;
@@ -43,6 +43,7 @@ pub(crate) fn import_command(f: &Flavour, center: Option<[f64; 2]>) -> (&'static
}
SVG => ("clipboard.importSvg", json!({ "dataBase64": b64(), "center": center })),
PDF => ("clipboard.importPdf", json!({ "dataBase64": b64(), "center": center })),
EPS => ("clipboard.importEps", json!({ "dataBase64": b64(), "center": center })),
EMF => ("clipboard.importEmf", json!({ "dataBase64": b64(), "center": center })),
mime => ("clipboard.importImage", json!({ "dataBase64": b64(), "mime": mime, "center": center })),
}
+34 -2
View File
@@ -4,7 +4,7 @@
use serde_json::json;
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::Session;
use vectorcraft_engine::cmd::clipboard::{EMF, PNG};
use vectorcraft_engine::cmd::clipboard::{EMF, EPS, Flavour, PASTE_ORDER, PDF, PNG, TEXT, file_flavour};
use crate::sysclip::import_command;
use crate::tests_sysclip::{app, blue_png, copy_elsewhere, pasted, run};
@@ -17,7 +17,7 @@ fn an_emf_from_another_app_pastes_as_vectors_ahead_of_its_bitmap() {
other.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 30, "height": 30})).unwrap();
let emf = other.execute("document.export", &json!({"format": "emf"})).unwrap();
let emf = vectorcraft_format::base64_decode(emf["dataBase64"].as_str().unwrap()).unwrap();
let flavour = vectorcraft_engine::cmd::clipboard::Flavour { mime: EMF, data: emf.clone() };
let flavour = Flavour { mime: EMF, data: emf.clone() };
assert_eq!(import_command(&flavour, None).0, "clipboard.importEmf");
copy_elsewhere(&board, vec![(PNG, blue_png()), (EMF, emf)]);
run(&mut app, "edit.paste", json!({}));
@@ -26,3 +26,35 @@ fn an_emf_from_another_app_pastes_as_vectors_ahead_of_its_bitmap() {
let b = p[0].geometric_bounds().unwrap();
assert!((b.width() - 30.0).abs() < 0.05, "{b:?}");
}
/// A 30 pt red square, as an EPS written by hand from the EPSF specification.
const SQUARE_EPS: &str = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n1 0 0 setrgbcolor\nnewpath 10 10 moveto 40 10 lineto 40 40 lineto 10 40 lineto closepath fill\nshowpage\n%%EOF\n";
/// An EPS another app copied (an equation editor's, a drawing app's) pastes as editable paths, not
/// as the bitmap or the text beside it, and not as what was copied in the app before (#1499).
#[test]
fn an_eps_from_another_app_pastes_as_vectors() {
let (mut app, board) = app();
crate::tests_sysclip::copy_rect(&mut app);
let eps = SQUARE_EPS.as_bytes().to_vec();
assert_eq!(import_command(&Flavour { mime: EPS, data: eps.clone() }, None).0, "clipboard.importEps");
copy_elsewhere(&board, vec![(PNG, blue_png()), (TEXT, b"x = 1".to_vec()), (EPS, eps.clone())]);
run(&mut app, "edit.paste", json!({}));
let p = pasted(&app);
assert!(!p.is_empty(), "something pasted");
let path = |n: &vectorcraft_doc::Node| matches!(n.kind, NodeKind::Path { .. });
let mut paths = vec![];
p.iter().for_each(|n| n.walk(&mut |c| paths.extend(path(c).then(|| c.geometric_bounds().unwrap()))));
assert_eq!(paths.len(), 1, "{p:?}");
assert!((paths[0].width() - 30.0).abs() < 0.05 && (paths[0].height() - 30.0).abs() < 0.05, "{paths:?}");
assert!(p.iter().all(|n| !matches!(n.kind, NodeKind::Image(_) | NodeKind::Text(_))), "{p:?}");
// A PDF beside it is the better copy: the apps that offer both mean the same picture.
assert!(PASTE_ORDER.iter().position(|m| *m == PDF) < PASTE_ORDER.iter().position(|m| *m == EPS));
// Not PostScript: refused, nothing pasted over.
let pdf = vectorcraft_format::base64_encode(b"%PDF-1.7");
assert!(app.session.execute("clipboard.importEps", &json!({"dataBase64": pdf})).is_err());
// An .eps file copied in a file manager is that flavour too (told by its bytes, not its name).
assert_eq!(file_flavour("equation.eps", &eps, &PASTE_ORDER), Some(EPS));
assert_eq!(file_flavour("equation.dat", &eps, &PASTE_ORDER), Some(EPS));
assert_eq!(file_flavour("equation.eps", &eps, &[TEXT]), None);
}
+2 -1
View File
@@ -507,7 +507,8 @@ type with those options (`file.place {text: {…}}`).
System clipboard formats: through the desktop app, `edit.copy` and `edit.cut` publish what `clipboard.flavours`
lists to the system clipboard, and every Paste command first loads what another app copied (`clipboard.importSvg`,
`importPdf`, `importText` or `importImage`, centred in the view); an import error answers the Paste with that error
`importPdf`, `importEps`, `importText` or `importImage`, centred in the view; the desktop app reads PDF and EPS from
the Windows clipboard and, with SVG, from the macOS pasteboard, and an `.eps` file copied in a file manager anywhere); an import error answers the Paste with that error
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).
+2 -1
View File
@@ -2115,7 +2115,8 @@ area). Each one has a query command:
transparent around them).
What other apps copied goes into the clipboard with `clipboard.importSvg`, `clipboard.importPdf {dataBase64, page?,
password?}` (the page's objects), `clipboard.importImage {dataBase64, mime?}` (an embedded image at 100% of its
password?}` (the page's objects), `clipboard.importEps {dataBase64}` (an EPS or other PostScript picture's objects,
as `document.open` reads one), `clipboard.importImage {dataBase64, mime?}` (an embedded image at 100% of its
physical size) or `clipboard.importText {text}` (point text in the default type style), each centred on `center`
(default: the first artboard); then any `edit.paste*` command pastes it. The desktop app does this itself: Copy and
Cut publish the formats to the system clipboard, and a Paste outside text editing first reads what another app