mirror of
https://github.com/penpot/penpot.git
synced 2025-05-28 13:56:11 +02:00
⚡ Add thumbnail renderer
And integrate the dashboard thumbnails to use that service
This commit is contained in:
parent
64ddfa0c31
commit
d11b007795
19 changed files with 579 additions and 183 deletions
|
@ -84,7 +84,6 @@
|
|||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
|
@ -110,7 +109,14 @@
|
|||
(def public-uri
|
||||
(atom
|
||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||
(.-origin ^js location)))))
|
||||
(obj/get location "origin")))))
|
||||
|
||||
(def thumbnail-renderer-uri
|
||||
(or (some-> (obj/get global "penpotThumbnailRendererURI") normalize-uri)
|
||||
(deref public-uri)))
|
||||
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[app.main.errors]
|
||||
[app.main.features :as feat]
|
||||
[app.main.store :as st]
|
||||
[app.main.thumbnail-renderer :as tr]
|
||||
[app.main.ui :as ui]
|
||||
[app.main.ui.alert]
|
||||
[app.main.ui.confirm]
|
||||
|
@ -80,6 +81,7 @@
|
|||
(i18n/init! cf/translations)
|
||||
(theme/init! cf/themes)
|
||||
(cur/init-styles)
|
||||
(tr/init!)
|
||||
(init-ui)
|
||||
(st/emit! (initialize)))
|
||||
|
||||
|
|
|
@ -782,6 +782,15 @@
|
|||
(->> (rp/cmd! :set-file-shared params)
|
||||
(rx/ignore))))))
|
||||
|
||||
(defn set-file-thumbnail
|
||||
[file-id thumbnail-uri]
|
||||
(ptk/reify ::set-file-thumbnail
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri)
|
||||
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri)))))
|
||||
|
||||
;; --- EVENT: create-file
|
||||
|
||||
(declare file-created)
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.text :as txt]
|
||||
[app.config :as cf]
|
||||
|
@ -148,15 +149,13 @@
|
|||
|
||||
;; --- LOADER: CUSTOM
|
||||
|
||||
(def font-css-template
|
||||
(def font-face-template
|
||||
"@font-face {
|
||||
font-family: '%(family)s';
|
||||
font-style: %(style)s;
|
||||
font-weight: %(weight)s;
|
||||
font-display: block;
|
||||
src: url(%(woff1-uri)s) format('woff'),
|
||||
url(%(ttf-uri)s) format('ttf'),
|
||||
url(%(otf-uri)s) format('otf');
|
||||
src: url(%(uri)s) format('woff');
|
||||
}")
|
||||
|
||||
(defn- asset-id->uri
|
||||
|
@ -165,14 +164,11 @@
|
|||
|
||||
(defn generate-custom-font-variant-css
|
||||
[family variant]
|
||||
(str/fmt font-css-template
|
||||
(str/fmt font-face-template
|
||||
{:family family
|
||||
:style (:style variant)
|
||||
:weight (:weight variant)
|
||||
:woff2-uri (asset-id->uri (::woff2-file-id variant))
|
||||
:woff1-uri (asset-id->uri (::woff1-file-id variant))
|
||||
:ttf-uri (asset-id->uri (::ttf-file-id variant))
|
||||
:otf-uri (asset-id->uri (::otf-file-id variant))}))
|
||||
:uri (asset-id->uri (::woff1-file-id variant))}))
|
||||
|
||||
(defn- generate-custom-font-css
|
||||
[{:keys [family variants] :as font}]
|
||||
|
@ -237,26 +233,19 @@
|
|||
(-> (obj/get-in js/document ["fonts" "ready"])
|
||||
(p/then cb)))
|
||||
|
||||
(defn get-default-variant [{:keys [variants]}]
|
||||
(or
|
||||
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
|
||||
(first variants)))
|
||||
(defn get-default-variant
|
||||
[{:keys [variants]}]
|
||||
(or (d/seek #(or (= (:id %) "regular")
|
||||
(= (:name %) "regular")) variants)
|
||||
(first variants)))
|
||||
|
||||
(defn get-variant
|
||||
[{:keys [variants] :as font} font-variant-id]
|
||||
(or (d/seek #(= (:id %) font-variant-id) variants)
|
||||
(get-default-variant font)))
|
||||
|
||||
;; Font embedding functions
|
||||
|
||||
;; Template for a CSS font face
|
||||
|
||||
(def font-face-template "
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: '%(family)s';
|
||||
font-style: %(style)s;
|
||||
font-weight: %(weight)s;
|
||||
font-display: block;
|
||||
src: url(%(baseurl)sfonts/%(family)s-%(suffix)s.woff) format('woff');
|
||||
}
|
||||
")
|
||||
|
||||
(defn get-content-fonts
|
||||
"Extracts the fonts used by the content of a text shape"
|
||||
[{font-id :font-id children :children :as content}]
|
||||
|
@ -267,38 +256,52 @@
|
|||
children-font (->> children (mapv get-content-fonts))]
|
||||
(reduce set/union (conj children-font current-font))))
|
||||
|
||||
|
||||
(defn fetch-font-css
|
||||
"Given a font and the variant-id, retrieves the fontface CSS"
|
||||
[{:keys [font-id font-variant-id]
|
||||
:or {font-variant-id "regular"}}]
|
||||
|
||||
(let [{:keys [backend family variants]} (get @fontsdb font-id)]
|
||||
(let [{:keys [backend family] :as font} (get @fontsdb font-id)]
|
||||
(cond
|
||||
(nil? font)
|
||||
(rx/empty)
|
||||
|
||||
(= :google backend)
|
||||
(let [variant (d/seek #(= (:id %) font-variant-id) variants)]
|
||||
(let [variant (get-variant font font-variant-id)]
|
||||
(-> (generate-gfonts-url
|
||||
{:family family
|
||||
:variants [variant]})
|
||||
(http/fetch-text)))
|
||||
|
||||
(= :custom backend)
|
||||
(let [variant (d/seek #(= (:id %) font-variant-id) variants)
|
||||
(let [variant (get-variant font font-variant-id)
|
||||
result (generate-custom-font-variant-css family variant)]
|
||||
(p/resolved result))
|
||||
(rx/of result))
|
||||
|
||||
:else
|
||||
(let [{:keys [weight style suffix] :as variant}
|
||||
(d/seek #(= (:id %) font-variant-id) variants)
|
||||
font-data {:baseurl (str @cf/public-uri)
|
||||
:family family
|
||||
:style style
|
||||
:suffix (or suffix font-variant-id)
|
||||
:weight weight}]
|
||||
(rx/of (str/fmt font-face-template font-data))))))
|
||||
(let [{:keys [weight style suffix]} (get-variant font font-variant-id)
|
||||
suffix (or suffix font-variant-id)
|
||||
params {:uri (dm/str @cf/public-uri "fonts/" family "-" suffix ".woff")
|
||||
:family family
|
||||
:style style
|
||||
:weight weight}]
|
||||
(rx/of (str/fmt font-face-template params))))))
|
||||
|
||||
(defn extract-fontface-urls
|
||||
"Parses the CSS and retrieves the font urls"
|
||||
[^string css]
|
||||
(->> (re-seq #"url\(([^)]+)\)" css)
|
||||
(mapv second)))
|
||||
|
||||
(defn render-font-styles
|
||||
[ids]
|
||||
(->> (rx/from ids)
|
||||
(rx/mapcat (fn [font-id]
|
||||
(let [font (get @fontsdb font-id)]
|
||||
(->> (:variants font [])
|
||||
(map :id)
|
||||
(map (fn [variant-id]
|
||||
{:font-id font-id
|
||||
:font-variant-id variant-id}))))))
|
||||
(rx/mapcat fetch-font-css)
|
||||
(rx/reduce (fn [acc css] (dm/str acc "\n" css)) "")))
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
:upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
|
||||
:create-file-object-thumbnail {:query-params [:file-id :object-id]
|
||||
:form-data? true}
|
||||
|
||||
:create-file-thumbnail
|
||||
{:query-params [:file-id :revn]
|
||||
:form-data? true}
|
||||
|
||||
:export-binfile {:response-type :blob}
|
||||
:import-binfile {:form-data? true}
|
||||
:retrieve-list-of-builtin-templates {:query-params :all}
|
||||
|
|
93
frontend/src/app/main/thumbnail_renderer.cljs
Normal file
93
frontend/src/app/main/thumbnail_renderer.cljs
Normal file
|
@ -0,0 +1,93 @@
|
|||
;; 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.thumbnail-renderer
|
||||
"A main entry point for the thumbnail renderer API interface.
|
||||
|
||||
This ns is responsible to provide an API for create thumbnail
|
||||
renderer iframes and interact with them using asyncrhonous
|
||||
messages."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defonce ready? false)
|
||||
(defonce queue #js [])
|
||||
(defonce instance nil)
|
||||
(defonce msgbus (rx/subject))
|
||||
(defonce origin
|
||||
(dm/str (assoc cf/thumbnail-renderer-uri :path "/thumbnail-renderer.html")))
|
||||
|
||||
(declare send-message!)
|
||||
|
||||
(defn- process-queued-messages!
|
||||
[]
|
||||
(loop [message (.shift ^js queue)]
|
||||
(when (some? message)
|
||||
(send-message! message)
|
||||
(recur (.shift ^js queue)))))
|
||||
|
||||
(defn- on-message
|
||||
"Handles a message from the thumbnail renderer."
|
||||
[event]
|
||||
(let [evorigin (unchecked-get event "origin")
|
||||
evdata (unchecked-get event "data")]
|
||||
|
||||
(when (and (object? evdata) (str/starts-with? origin evorigin))
|
||||
(let [scope (unchecked-get evdata "scope")
|
||||
type (unchecked-get evdata "type")]
|
||||
(when (= "penpot/thumbnail-renderer" scope)
|
||||
(when (= type "ready")
|
||||
(set! ready? true)
|
||||
(process-queued-messages!))
|
||||
(rx/push! msgbus evdata))))))
|
||||
|
||||
(defn- send-message!
|
||||
"Sends a message to the thumbnail renderer."
|
||||
[message]
|
||||
(let [window (.-contentWindow ^js instance)]
|
||||
(.postMessage ^js window message origin)))
|
||||
|
||||
(defn- queue-message!
|
||||
"Queues a message to be sent to the thumbnail renderer when it's ready."
|
||||
[message]
|
||||
(.push ^js queue message))
|
||||
|
||||
(defn render
|
||||
"Renders a thumbnail."
|
||||
[{:keys [data styles] :as params}]
|
||||
(let [id (dm/str (uuid/next))
|
||||
payload #js {:data data :styles styles}
|
||||
message #js {:id id
|
||||
:scope "penpot/thumbnail-renderer"
|
||||
:payload payload}]
|
||||
|
||||
(if ^boolean ready?
|
||||
(send-message! message)
|
||||
(queue-message! message))
|
||||
|
||||
(->> msgbus
|
||||
(rx/filter #(= id (unchecked-get % "id")))
|
||||
(rx/mapcat (fn [msg]
|
||||
(case (unchecked-get msg "type")
|
||||
"success" (rx/of (unchecked-get msg "payload"))
|
||||
"failure" (rx/throw (unchecked-get msg "payload")))))
|
||||
(rx/take 1))))
|
||||
|
||||
(defn init!
|
||||
"Initializes the thumbnail renderer."
|
||||
[]
|
||||
(let [iframe (dom/create-element "iframe")]
|
||||
(dom/set-attribute! iframe "src" origin)
|
||||
(dom/set-attribute! iframe "hidden" true)
|
||||
(dom/append-child! js/document.body iframe)
|
||||
|
||||
(set! instance iframe)
|
||||
(.addEventListener js/window "message" on-message)))
|
|
@ -16,7 +16,9 @@
|
|||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :refer [component-svg]]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.thumbnail-renderer :as thr]
|
||||
[app.main.ui.components.color-bullet :as bc]
|
||||
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||
|
@ -30,7 +32,6 @@
|
|||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.perf :as perf]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
|
@ -41,44 +42,49 @@
|
|||
|
||||
;; --- Grid Item Thumbnail
|
||||
|
||||
(defn ask-for-thumbnail
|
||||
(defn- persist-thumbnail
|
||||
[file-id revn blob]
|
||||
(let [params {:file-id file-id :revn revn :media blob}]
|
||||
(->> (rp/cmd! :create-file-thumbnail params)
|
||||
(rx/map :uri))))
|
||||
|
||||
(defn- ask-for-thumbnail
|
||||
"Creates some hooks to handle the files thumbnails cache"
|
||||
[file]
|
||||
[file-id revn]
|
||||
(let [features (cond-> ffeat/enabled
|
||||
(features/active-feature? :components-v2)
|
||||
(conj "components/v2"))]
|
||||
|
||||
(wrk/ask! {:cmd :thumbnails/generate-for-file
|
||||
:revn (:revn file)
|
||||
:file-id (:id file)
|
||||
:file-name (:name file)
|
||||
:features features})))
|
||||
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:features features})
|
||||
(rx/mapcat (fn [{:keys [fonts] :as result}]
|
||||
(->> (fonts/render-font-styles fonts)
|
||||
(rx/map (fn [styles]
|
||||
(assoc result :styles styles))))))
|
||||
(rx/mapcat thr/render)
|
||||
(rx/mapcat (partial persist-thumbnail file-id revn)))))
|
||||
|
||||
(mf/defc grid-item-thumbnail
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [file] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [file-id revn thumbnail-uri background-color]}]
|
||||
(let [container (mf/use-ref)
|
||||
bgcolor (dm/get-in file [:data :options :background])
|
||||
visible? (h/use-visible container :once? true)]
|
||||
|
||||
(mf/with-effect [file visible?]
|
||||
(when visible?
|
||||
(let [tp (perf/tpoint)]
|
||||
(->> (ask-for-thumbnail file)
|
||||
(rx/subscribe-on :af)
|
||||
(rx/subs (fn [{:keys [data fonts] :as params}]
|
||||
(run! fonts/ensure-loaded! fonts)
|
||||
(log/debug :hint "loaded thumbnail"
|
||||
:file-id (dm/str (:id file))
|
||||
:file-name (:name file)
|
||||
:elapsed (str/ffmt "%ms" (tp)))
|
||||
(when-let [node (mf/ref-val container)]
|
||||
(dom/set-html! node data))))))))
|
||||
(mf/with-effect [file-id revn visible? thumbnail-uri]
|
||||
(when (and visible? (not thumbnail-uri))
|
||||
(->> (ask-for-thumbnail file-id revn)
|
||||
(rx/subs (fn [url]
|
||||
(st/emit! (dd/set-file-thumbnail file-id url)))))))
|
||||
|
||||
[:div.grid-item-th
|
||||
{:style {:background-color bgcolor}
|
||||
{:style {:background-color background-color}
|
||||
:ref container}
|
||||
i/loader-pencil]))
|
||||
(when visible?
|
||||
(if thumbnail-uri
|
||||
[:img.grid-item-thumbnail-image {:src thumbnail-uri}]
|
||||
i/loader-pencil))]))
|
||||
|
||||
;; --- Grid Item Library
|
||||
|
||||
|
@ -312,7 +318,12 @@
|
|||
[:div.overlay]
|
||||
(if library-view?
|
||||
[:& grid-item-library {:file file}]
|
||||
[:& grid-item-thumbnail {:file file}])
|
||||
[:& grid-item-thumbnail
|
||||
{:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:thumbnail-uri (:thumbnail-uri file)
|
||||
:background-color (dm/get-in file [:data :options :background])}])
|
||||
|
||||
(when (and (:is-shared file) (not library-view?))
|
||||
[:div.item-badge i/library])
|
||||
[:div.info-wrapper
|
||||
|
|
245
frontend/src/app/thumbnail_renderer.cljs
Normal file
245
frontend/src/app/thumbnail_renderer.cljs
Normal file
|
@ -0,0 +1,245 @@
|
|||
;; 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.thumbnail-renderer
|
||||
"A main entry point for the thumbnail renderer process that is
|
||||
executed on a separated iframe."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.object :as obj]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(log/set-level! :trace)
|
||||
|
||||
(declare send-success!)
|
||||
(declare send-failure!)
|
||||
|
||||
(defonce parent-origin
|
||||
(dm/str @cf/public-uri))
|
||||
|
||||
(defn- get-document-element
|
||||
[^js svg]
|
||||
(.-documentElement svg))
|
||||
|
||||
(defn- create-image
|
||||
[uri]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(let [image (js/Image.)]
|
||||
(obj/set! image "onload" #(do
|
||||
(rx/push! subs image)
|
||||
(rx/end! subs)))
|
||||
|
||||
(obj/set! image "crossOrigin" "anonymous")
|
||||
(obj/set! image "onerror" #(rx/error! subs %))
|
||||
(obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal
|
||||
:code :abort
|
||||
:hint "operation aborted")))
|
||||
(obj/set! image "src" uri)
|
||||
(fn []
|
||||
(obj/set! image "src" "")
|
||||
(obj/set! image "onload" nil)
|
||||
(obj/set! image "onerror" nil)
|
||||
(obj/set! image "onabort" nil))))))
|
||||
|
||||
(defn- svg-get-size
|
||||
[svg max]
|
||||
(let [doc (get-document-element svg)
|
||||
vbox (dom/get-attribute doc "viewBox")]
|
||||
(when (string? vbox)
|
||||
(let [[_ _ width height] (str/split vbox #"\s+")
|
||||
width (d/parse-integer width 0)
|
||||
height (d/parse-integer height 0)
|
||||
ratio (/ width height)]
|
||||
(if (> width height)
|
||||
[max (* max (/ 1 ratio))]
|
||||
[(* max ratio) max])))))
|
||||
|
||||
(defn- svg-has-intrinsic-size?
|
||||
"Returns true if the SVG has an intrinsic size."
|
||||
[svg]
|
||||
(let [doc (get-document-element svg)
|
||||
width (dom/get-attribute doc "width")
|
||||
height (dom/get-attribute doc "height")]
|
||||
(d/num? width height)))
|
||||
|
||||
(defn- svg-set-intrinsic-size!
|
||||
"Sets the intrinsic size of an SVG to the given max size."
|
||||
[^js svg max]
|
||||
(when-not (svg-has-intrinsic-size? svg)
|
||||
(let [doc (get-document-element svg)
|
||||
[w h] (svg-get-size svg max)]
|
||||
(dom/set-attribute! doc "width" (dm/str w))
|
||||
(dom/set-attribute! doc "height" (dm/str h))))
|
||||
svg)
|
||||
|
||||
(defn- fetch-as-data-uri
|
||||
"Fetches a URL as a Data URI."
|
||||
[uri]
|
||||
(->> (http/send! {:uri uri
|
||||
:response-type :blob
|
||||
:method :get
|
||||
:mode :cors
|
||||
:omit-default-headers true})
|
||||
(rx/map :body)
|
||||
(rx/mapcat wapi/read-file-as-data-url)))
|
||||
|
||||
(defn- svg-update-image!
|
||||
"Updates an image in an SVG to a Data URI."
|
||||
[image]
|
||||
(when-let [href (dom/get-attribute image "href")]
|
||||
(->> (fetch-as-data-uri href)
|
||||
(rx/map (fn [url]
|
||||
(dom/set-attribute! image "href" url)
|
||||
image)))))
|
||||
|
||||
(defn- svg-resolve-images!
|
||||
"Resolves all images in an SVG to Data URIs."
|
||||
[svg]
|
||||
(->> (rx/from (dom/query-all svg "image"))
|
||||
(rx/mapcat svg-update-image!)
|
||||
(rx/ignore)))
|
||||
|
||||
(defn- svg-add-style!
|
||||
"Adds a <style> node to an SVG."
|
||||
[svg styles]
|
||||
(let [doc (get-document-element svg)
|
||||
style (dom/create-element svg "http://www.w3.org/2000/svg" "style")]
|
||||
(dom/append-child! style (dom/create-text svg styles))
|
||||
(dom/append-child! doc style)))
|
||||
|
||||
(defn- svg-resolve-styles!
|
||||
"Resolves all fonts in an SVG to Data URIs."
|
||||
[svg styles]
|
||||
(->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles))
|
||||
(rx/map second)
|
||||
(rx/mapcat (fn [url]
|
||||
(->> (fetch-as-data-uri url)
|
||||
(rx/map (fn [uri] [url uri])))))
|
||||
|
||||
(rx/reduce (fn [styles [url uri]]
|
||||
(str/replace styles url uri))
|
||||
styles)
|
||||
(rx/tap (partial svg-add-style! svg))
|
||||
(rx/ignore)))
|
||||
|
||||
(defn- svg-resolve-all!
|
||||
"Resolves all images and fonts in an SVG to Data URIs."
|
||||
[svg styles]
|
||||
(rx/concat
|
||||
(svg-resolve-images! svg)
|
||||
(svg-resolve-styles! svg styles)
|
||||
(rx/of svg)))
|
||||
|
||||
(defn- svg-parse
|
||||
"Parses an SVG string into an SVG DOM."
|
||||
[data]
|
||||
(let [parser (js/DOMParser.)]
|
||||
(.parseFromString ^js parser data "image/svg+xml")))
|
||||
|
||||
(defn- svg-stringify
|
||||
"Converts an SVG to a string."
|
||||
[svg]
|
||||
(let [doc (get-document-element svg)
|
||||
serializer (js/XMLSerializer.)]
|
||||
(.serializeToString ^js serializer doc)))
|
||||
|
||||
(defn- svg-prepare
|
||||
"Prepares an SVG for rendering (resolves images to Data URIs and adds intrinsic size)."
|
||||
[data styles]
|
||||
(let [svg (svg-parse data)]
|
||||
(->> (svg-resolve-all! svg styles)
|
||||
(rx/map #(svg-set-intrinsic-size! % 300))
|
||||
(rx/map svg-stringify))))
|
||||
|
||||
(defn- bitmap->blob
|
||||
"Converts an ImageBitmap to a Blob."
|
||||
[bitmap]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(let [canvas (dom/create-element "canvas")]
|
||||
(set! (.-width ^js canvas) (.-width ^js bitmap))
|
||||
(set! (.-height ^js canvas) (.-height ^js bitmap))
|
||||
(let [context (.getContext ^js canvas "bitmaprenderer")]
|
||||
(.transferFromImageBitmap ^js context bitmap)
|
||||
(.toBlob canvas #(do (rx/push! subs %)
|
||||
(rx/end! subs))))
|
||||
|
||||
(constantly nil)))))
|
||||
|
||||
(defn- render
|
||||
"Renders a thumbnail using it's SVG and returns an ArrayBuffer of the image."
|
||||
[payload]
|
||||
(let [data (unchecked-get payload "data")
|
||||
styles (unchecked-get payload "styles")]
|
||||
(->> (svg-prepare data styles)
|
||||
(rx/map #(wapi/create-blob % "image/svg+xml"))
|
||||
(rx/map wapi/create-uri)
|
||||
(rx/mapcat (fn [uri]
|
||||
(->> (create-image uri)
|
||||
(rx/mapcat wapi/create-image-bitmap)
|
||||
(rx/tap #(wapi/revoke-uri uri)))))
|
||||
(rx/mapcat bitmap->blob))))
|
||||
|
||||
(defn- on-message
|
||||
"Handles messages from the main thread."
|
||||
[event]
|
||||
(let [evdata (unchecked-get event "data")
|
||||
evorigin (unchecked-get event "origin")]
|
||||
(when (str/starts-with? parent-origin evorigin)
|
||||
(let [id (unchecked-get evdata "id")
|
||||
payload (unchecked-get evdata "payload")
|
||||
scope (unchecked-get evdata "scope")]
|
||||
(when (and (some? payload)
|
||||
(= scope "penpot/thumbnail-renderer"))
|
||||
(->> (render payload)
|
||||
(rx/subs (partial send-success! id)
|
||||
(partial send-failure! id))))))))
|
||||
|
||||
(defn- listen
|
||||
"Initializes the listener for messages from the main thread."
|
||||
[]
|
||||
(.addEventListener js/window "message" on-message))
|
||||
|
||||
(defn- send-answer!
|
||||
"Sends an answer message."
|
||||
[id type payload]
|
||||
(let [message #js {:id id
|
||||
:type type
|
||||
:scope "penpot/thumbnail-renderer"
|
||||
:payload payload}]
|
||||
(when-not (identical? js/window js/parent)
|
||||
(.postMessage js/parent message parent-origin))))
|
||||
|
||||
(defn- send-success!
|
||||
"Sends a success message."
|
||||
[id payload]
|
||||
(send-answer! id "success" payload))
|
||||
|
||||
(defn- send-failure!
|
||||
"Sends a failure message."
|
||||
[id payload]
|
||||
(send-answer! id "failure" payload))
|
||||
|
||||
(defn- send-ready!
|
||||
"Sends a ready message."
|
||||
[]
|
||||
(send-answer! nil "ready" nil))
|
||||
|
||||
;; Initializes worker
|
||||
(defn ^:export init
|
||||
[]
|
||||
(listen)
|
||||
(send-ready!)
|
||||
(log/info :hint "initialized" :public-uri @cf/public-uri))
|
|
@ -254,7 +254,15 @@
|
|||
([tag]
|
||||
(.createElement globals/document tag))
|
||||
([ns tag]
|
||||
(.createElementNS globals/document ns tag)))
|
||||
(.createElementNS globals/document ns tag))
|
||||
([document ns tag]
|
||||
(.createElementNS document ns tag)))
|
||||
|
||||
(defn create-text
|
||||
([^js text]
|
||||
(create-text globals/document text))
|
||||
([document ^js text]
|
||||
(.createTextNode document text)))
|
||||
|
||||
(defn set-html!
|
||||
[^js el html]
|
||||
|
|
|
@ -130,6 +130,10 @@
|
|||
(map #(.item file-list %))
|
||||
(filter #(str/starts-with? (.-type %) "image/"))))))
|
||||
|
||||
(defn create-image-bitmap
|
||||
[image]
|
||||
(js/createImageBitmap image))
|
||||
|
||||
(defn request-fullscreen
|
||||
[el]
|
||||
(cond
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
[app.util.webapi :as wapi]
|
||||
[app.worker.impl :as impl]
|
||||
[beicon.core :as rx]
|
||||
[debug :refer [debug?]]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
@ -45,15 +44,6 @@
|
|||
:http-status status
|
||||
:http-body body})))
|
||||
|
||||
(defn- not-found?
|
||||
[{:keys [type]}]
|
||||
(= :not-found type))
|
||||
|
||||
(defn- body-too-large?
|
||||
[{:keys [type code]}]
|
||||
(and (= :validation type)
|
||||
(= :request-body-too-large code)))
|
||||
|
||||
(defn- request-data-for-thumbnail
|
||||
[file-id revn features]
|
||||
(let [path "api/rpc/command/get-file-data-for-thumbnail"
|
||||
|
@ -69,70 +59,25 @@
|
|||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
(defn- request-thumbnail
|
||||
[file-id revn]
|
||||
(let [path "api/rpc/command/get-file-thumbnail"
|
||||
params {:file-id file-id
|
||||
:revn revn}
|
||||
request {:method :get
|
||||
:uri (u/join @cf/public-uri path)
|
||||
:credentials "include"
|
||||
:query params}]
|
||||
(->> (http/send! request)
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
(defn- render-thumbnail
|
||||
[{:keys [page file-id revn] :as params}]
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
element (if frame
|
||||
(mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true})
|
||||
(mf/element render/page-svg #js {:data page :thumbnails? true}))
|
||||
data (rds/renderToStaticMarkup element)]
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
element (if frame
|
||||
(mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true})
|
||||
(mf/element render/page-svg #js {:data page :thumbnails? true :render-embed? true}))
|
||||
data (rds/renderToStaticMarkup element)
|
||||
font-ids (into @fonts/loaded (map first) @fonts/loading)]
|
||||
|
||||
{:data data
|
||||
:fonts (into @fonts/loaded (map first) @fonts/loading)
|
||||
:fonts font-ids
|
||||
:file-id file-id
|
||||
:revn revn}))
|
||||
|
||||
(defn- persist-thumbnail
|
||||
[{:keys [file-id data revn fonts]}]
|
||||
(let [path "api/rpc/command/upsert-file-thumbnail"
|
||||
params {:file-id file-id
|
||||
:revn revn
|
||||
:props {:fonts fonts}
|
||||
:data data}
|
||||
request {:method :post
|
||||
:uri (u/join @cf/public-uri path)
|
||||
:credentials "include"
|
||||
:body (http/transit-data params)}]
|
||||
|
||||
(->> (http/send! request)
|
||||
(rx/map http/conditional-decode-transit)
|
||||
(rx/mapcat handle-response)
|
||||
(rx/catch body-too-large? (constantly (rx/of nil)))
|
||||
(rx/map (constantly params)))))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate-for-file
|
||||
[{:keys [file-id revn features] :as message} _]
|
||||
(letfn [(on-result [{:keys [data props]}]
|
||||
{:data data
|
||||
:fonts (:fonts props)})
|
||||
|
||||
(on-cache-miss [_]
|
||||
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "miss")
|
||||
(->> (request-data-for-thumbnail file-id revn features)
|
||||
(rx/map render-thumbnail)
|
||||
(rx/mapcat persist-thumbnail)))]
|
||||
|
||||
(if (debug? :disable-thumbnail-cache)
|
||||
(->> (request-data-for-thumbnail file-id revn features)
|
||||
(rx/map render-thumbnail))
|
||||
(->> (request-thumbnail file-id revn)
|
||||
(rx/tap (fn [_]
|
||||
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit")))
|
||||
(rx/catch not-found? on-cache-miss)
|
||||
(rx/map on-result)))))
|
||||
(->> (request-data-for-thumbnail file-id revn features)
|
||||
(rx/map render-thumbnail)))
|
||||
|
||||
(defmethod impl/handler :thumbnails/render-offscreen-canvas
|
||||
[_ ibpm]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue