mirror of
https://github.com/penpot/penpot.git
synced 2026-09-02 10:09:15 +00:00
* 🔧 Support background blur on PDF render for strokes and text shapes * ✨ Add LRU eviction to the wasm image store * ✨ Add RasterFormat to encode png, jpeg and webp from wasm * 🐛 Fix client-side wasm export encoding jpeg and webp as png
186 lines
7.9 KiB
Clojure
186 lines
7.9 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace.thumbnails-wasm
|
|
"WASM-based component thumbnail rendering.
|
|
Renders component previews using the existing workspace WASM context
|
|
via render-shape-pixels (SurfaceId::Export), avoiding a separate
|
|
WASM module in the worker.
|
|
|
|
Two-phase design:
|
|
- render-thumbnail: immediate WASM render → UI update (no server)
|
|
- persist-thumbnail: pushes current data-uri to the server (debounced)"
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.logging :as l]
|
|
[app.common.math :as mth]
|
|
[app.common.thumbnails :as thc]
|
|
[app.common.time :as ct]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.workspace.thumbnails :as dwt]
|
|
[app.main.repo :as rp]
|
|
[app.main.store :as st]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.util.timers :as timers]
|
|
[app.util.webapi :as wapi]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(l/set-level! :warn)
|
|
|
|
(defn- png-bytes->data-uri
|
|
"Converts a Uint8Array of PNG bytes to a data:image/png;base64 URI."
|
|
[png-bytes]
|
|
(let [blob (wapi/create-blob png-bytes "image/png")
|
|
reader (js/FileReader.)]
|
|
(js/Promise.
|
|
(fn [resolve reject]
|
|
(set! (.-onload reader)
|
|
(fn [] (resolve (.-result reader))))
|
|
(set! (.-onerror reader)
|
|
(fn [e] (reject e)))
|
|
(.readAsDataURL reader blob)))))
|
|
|
|
;; This constant stores the target thumbnail minimum max-size so
|
|
;; the images doesn't lose quality when rendered
|
|
(def ^:private ^:const target-size 200)
|
|
|
|
(defn- render-component-pixels
|
|
"Renders a component frame using the workspace WASM context.
|
|
Returns an observable that emits a data-uri string.
|
|
Deferred by one animation frame so that process-shape-changes!
|
|
has time to sync all child shapes to WASM memory first."
|
|
[file-id page-id frame-id]
|
|
(rx/create
|
|
(fn [subs]
|
|
(let [req-id
|
|
(timers/raf
|
|
(fn [_]
|
|
(try
|
|
(let [objects (dsh/lookup-page-objects @st/state file-id page-id)]
|
|
(if-let [frame (get objects frame-id)]
|
|
(let [{ext-w :width ext-h :height} (wasm.api/get-shape-extrect frame-id)
|
|
{sel-w :width sel-h :height} (:selrect frame)
|
|
max-size (mth/max (or ext-w sel-w) (or ext-h sel-h))
|
|
scale (mth/max 1 (/ target-size max-size))
|
|
;; Thumbnails are always PNG: they need the alpha
|
|
;; channel and are consumed as a `image/png` data uri.
|
|
png-bytes (wasm.api/render-shape-pixels frame-id scale :png)]
|
|
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
|
|
(do
|
|
(l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id))
|
|
(rx/end! subs))
|
|
(.then
|
|
(png-bytes->data-uri png-bytes)
|
|
(fn [data-uri]
|
|
(rx/push! subs data-uri)
|
|
(rx/end! subs))
|
|
(fn [err]
|
|
(rx/error! subs err)))))
|
|
|
|
;; A missing frame. Nothing to render, so end quietly.
|
|
(rx/end! subs)))
|
|
(catch :default err
|
|
(rx/error! subs err)))))]
|
|
#(timers/cancel-af! req-id)))))
|
|
|
|
(defn render-thumbnail
|
|
"Renders a component thumbnail via WASM and updates the UI immediately.
|
|
When `persist?` is true, also persists the rendered thumbnail to the
|
|
server in the same observable chain (guaranteeing correct ordering)."
|
|
[file-id page-id frame-id & {:keys [persist?] :or {persist? false}}]
|
|
|
|
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
|
|
(ptk/reify ::render-thumbnail
|
|
cljs.core/IDeref
|
|
(-deref [_] object-id)
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
;; When the component is removed it can arrived a render
|
|
;; request with frame-id=null
|
|
(when (some? frame-id)
|
|
(letfn [(load-objects-stream
|
|
[]
|
|
(rx/create
|
|
(fn [subs]
|
|
(let [objects (dsh/lookup-page-objects state file-id page-id)
|
|
|
|
;; retrieves a subtree with only the id and its children
|
|
;; to be loaded before rendering the thumbnail
|
|
subtree
|
|
(into {}
|
|
(map #(vector (:id %) %))
|
|
(cfh/get-children-with-self objects frame-id))]
|
|
(try
|
|
(wasm.api/set-objects subtree #(rx/push! subs %))
|
|
(catch :default err
|
|
(rx/error! subs err)))))))
|
|
|
|
(persist-to-server
|
|
[data-uri]
|
|
(let [blob (wapi/data-uri->blob data-uri)]
|
|
(->> (rp/cmd! :create-file-object-thumbnail
|
|
{:file-id file-id
|
|
:object-id object-id
|
|
:media blob
|
|
:tag "component"})
|
|
(rx/catch rx/empty)
|
|
(rx/ignore))))
|
|
|
|
(do-render-thumbnail
|
|
[]
|
|
(let [tp (ct/tpoint-ms)]
|
|
(->> (render-component-pixels file-id page-id frame-id)
|
|
(rx/mapcat
|
|
(fn [data-uri]
|
|
(l/dbg :hint "component thumbnail rendered (wasm)"
|
|
:elapsed (dm/str (tp) "ms"))
|
|
(if persist?
|
|
(rx/merge
|
|
(rx/of (dwt/assoc-thumbnail object-id data-uri))
|
|
(persist-to-server data-uri))
|
|
(rx/of (dwt/assoc-thumbnail object-id data-uri)))))
|
|
|
|
(rx/catch (fn [err]
|
|
(js/console.error err)
|
|
(l/error :hint "error rendering component thumbnail" :frame-id (str frame-id))
|
|
(rx/empty)))
|
|
|
|
(rx/take-until
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dwt/clear-thumbnail))
|
|
(rx/filter #(= (deref %) object-id)))))))]
|
|
|
|
(if (not= page-id (:current-page-id state))
|
|
(->> (load-objects-stream)
|
|
(rx/mapcat do-render-thumbnail))
|
|
(do-render-thumbnail))))))))
|
|
|
|
(defn persist-thumbnail
|
|
"Persists the current component thumbnail data-uri to the server.
|
|
Expects that `render-thumbnail` has already been called so the
|
|
data-uri is present in app state. If not, this is a no-op."
|
|
[file-id page-id frame-id]
|
|
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
|
|
(ptk/reify ::persist-thumbnail
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [data-uri (dm/get-in state [:thumbnails object-id :uri])]
|
|
(if (and (some? data-uri)
|
|
(str/starts-with? data-uri "data:"))
|
|
(let [blob (wapi/data-uri->blob data-uri)]
|
|
(->> (rp/cmd! :create-file-object-thumbnail
|
|
{:file-id file-id
|
|
:object-id object-id
|
|
:media blob
|
|
:tag "component"})
|
|
(rx/catch rx/empty)
|
|
(rx/ignore)))
|
|
(rx/empty)))))))
|