From a278d54429677906927e20b8e1a8c492358ad248 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Feb 2026 11:17:02 +0100 Subject: [PATCH] :tada: Add copy as image to clipboard menu option (#8364) * :sparkles: Copy as image Function to copy a board directly to the clipboard. This is exposed on the Copy/Paste as... context menu. The image is always copied at 2x to work well with wireframes. I tried with and without Retina display and it is better in both scenarios. Signed-off-by: Dalai Felinto * :sparkles: Add minor adjustments on promise creation * :fire: Remove prn from obj/reify macros --------- Signed-off-by: Dalai Felinto --- CHANGES.md | 1 + frontend/src/app/main/data/workspace.cljs | 1 + .../app/main/data/workspace/clipboard.cljs | 52 +++++++++++++++++++ .../app/main/ui/workspace/context_menu.cljs | 14 ++++- frontend/translations/en.po | 12 +++++ frontend/translations/es.po | 12 +++++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 259a2b68c6..9c306b5e36 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ ### :heart: Community contributions (Thank you!) - Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) +- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) ### :sparkles: New features & Enhancements diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 9116c99024..24eac2aa94 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1434,6 +1434,7 @@ (dm/export dwcp/paste-shapes) (dm/export dwcp/paste-data-valid?) (dm/export dwcp/copy-link-to-clipboard) +(dm/export dwcp/copy-as-image) ;; Drawing (dm/export dwd/select-for-drawing) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 2692eef103..9c90643842 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -1039,3 +1039,55 @@ ptk/WatchEvent (watch [_ _ _] (clipboard/to-clipboard (rt/get-current-href))))) + +(defn copy-as-image + [] + (ptk/reify ::copy-as-image + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + selected (first (dsh/lookup-selected state)) + + export {:file-id file-id + :page-id page-id + :object-id selected + ;; webp would be preferrable, but PNG is the most supported image MIME type by clipboard APIs. + :type :png + ;; Always use 2 to ensure good enough quality for wireframes. + :scale 2 + :suffix "" + :enabled true + :name ""} + + params {:exports [export] + :profile-id (:profile-id state) + :cmd :export-shapes + :wait true}] + + (rx/concat + ;; Ensure current state persisted before exporting. + (rx/of ::dps/force-persist) + (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) + (rx/filter #(or (nil? %) (= :saved %))) + (rx/first) + (rx/timeout 400 (rx/empty))) + + ;; Exporting itself can time its time, better to notify that we are busy. + (rx/of (ntf/info (tr "workspace.clipboard.copying"))) + + ;; Call exporter to get image URI, then fetch and copy blob. + (->> (rp/cmd! :export params) + (rx/mapcat (fn [{:keys [uri]}] + (http/send! {:method :get + :uri uri + :response-type :blob}))) + (rx/map :body) + (rx/tap (fn [blob] + (clipboard/to-clipboard-promise "image/png" (p/resolved blob)))) + (rx/map (fn [_] + (ntf/success (tr "workspace.clipboard.image-copied")))) + (rx/catch (fn [e] + (js/console.error "clipboard blocked:" e) + (ntf/error (tr "workspace.clipboard.image-copy-failed")) + (rx/empty))))))))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index b2a69eca34..edc090f426 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -150,7 +150,9 @@ {::mf/props :obj ::mf/private true} [{:keys [shapes]}] - (let [do-copy #(st/emit! (dw/copy-selected)) + (let [multiple? (> (count shapes) 1) + + do-copy #(st/emit! (dw/copy-selected)) do-copy-link #(st/emit! (dw/copy-link-to-clipboard)) do-cut #(st/emit! (dw/copy-selected) @@ -178,6 +180,9 @@ handle-copy-text (mf/use-callback #(st/emit! (dw/copy-selected-text))) + handle-copy-as-image + (mf/use-callback #(st/emit! (dw/copy-as-image))) + handle-hover-copy-paste (mf/use-callback (fn [] @@ -222,6 +227,11 @@ [:> menu-entry* {:title (tr "workspace.shape.menu.copy-svg") :on-click handle-copy-svg}] + (when (some cfh/frame-shape? shapes) + [:> menu-entry* {:title (tr "workspace.shape.menu.copy-as-image") + :disabled multiple? + :on-click handle-copy-as-image}]) + [:> menu-separator* {}] [:> menu-entry* {:title (tr "workspace.shape.menu.copy-text") @@ -229,7 +239,7 @@ [:> menu-entry* {:title (tr "workspace.shape.menu.copy-props") :shortcut (sc/get-tooltip :copy-props) - :disabled (> (count shapes) 1) + :disabled multiple? :on-click handle-copy-props}] [:> menu-entry* {:title (tr "workspace.shape.menu.paste-props") :shortcut (sc/get-tooltip :paste-props) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 95e5906dbe..9000ce16da 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7600,6 +7600,18 @@ msgstr "Paste" msgid "workspace.shape.menu.paste-props" msgstr "Paste properties" +msgid "workspace.shape.menu.copy-as-image" +msgstr "Copy as image" + +msgid "workspace.clipboard.copying" +msgstr "Copying image…" + +msgid "workspace.clipboard.image-copied" +msgstr "Image copied to the clipboard" + +msgid "workspace.clipboard.image-copy-failed" +msgstr "Error copying image" + #: src/app/main/ui/workspace/context_menu.cljs:443 msgid "workspace.shape.menu.path" msgstr "Path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9e543b7f4f..b65172caed 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7544,6 +7544,18 @@ msgstr "Pegar" msgid "workspace.shape.menu.paste-props" msgstr "Pegar propiedades" +msgid "workspace.shape.menu.copy-as-image" +msgstr "Copiar como imagen" + +msgid "workspace.clipboard.copying" +msgstr "Copiando imagen…" + +msgid "workspace.clipboard.image-copied" +msgstr "Imagen copiada al portapapeles" + +msgid "workspace.clipboard.image-copy-failed" +msgstr "Error al copiar la imagen" + #: src/app/main/ui/workspace/context_menu.cljs:443 msgid "workspace.shape.menu.path" msgstr "Ruta"