🎉 Add copy as image to clipboard menu option (#8364)

*  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 <dalai@blender.org>

*  Add minor adjustments on promise creation

* 🔥 Remove prn from obj/reify macros

---------

Signed-off-by: Dalai Felinto <dalai@blender.org>
This commit is contained in:
Andrey Antukh 2026-02-16 11:17:02 +01:00 committed by GitHub
parent a1cc016727
commit a278d54429
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 90 additions and 2 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)))))))))

View File

@ -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)

View File

@ -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"

View File

@ -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"