diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj
index d1da43c976..ff24bed624 100644
--- a/backend/src/app/rpc/commands/files.clj
+++ b/backend/src/app/rpc/commands/files.clj
@@ -38,6 +38,11 @@
;; --- FEATURES
+(defn resolve-public-uri
+ [media-id]
+ (when media-id
+ (str (cf/get :public-uri) "/assets/by-id/" media-id)))
+
(def supported-features
#{"storage/objects-map"
"storage/pointer-map"
@@ -413,15 +418,23 @@
f.modified_at,
f.name,
f.revn,
- f.is_shared
+ f.is_shared,
+ ft.media_id
from file as f
+ left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where f.project_id = ?
and f.deleted_at is null
order by f.modified_at desc")
(defn get-project-files
[conn project-id]
- (db/exec! conn [sql:project-files project-id]))
+ (->> (db/exec! conn [sql:project-files project-id])
+ (mapv (fn [row]
+ (if-let [media-id (:media-id row)]
+ (-> row
+ (dissoc :media-id)
+ (assoc :thumbnail-uri (resolve-public-uri media-id)))
+ (dissoc row :media-id))))))
(sv/defmethod ::get-project-files
"Get all files for the specified project."
@@ -668,9 +681,11 @@
f.modified_at,
f.name,
f.is_shared,
+ ft.media_id,
row_number() over w as row_num
from file as f
- join project as p on (p.id = f.project_id)
+ inner join project as p on (p.id = f.project_id)
+ left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where p.team_id = ?
and p.deleted_at is null
and f.deleted_at is null
@@ -681,7 +696,13 @@
(defn get-team-recent-files
[conn team-id]
- (db/exec! conn [sql:team-recent-files team-id]))
+ (->> (db/exec! conn [sql:team-recent-files team-id])
+ (mapv (fn [row]
+ (if-let [media-id (:media-id row)]
+ (-> row
+ (dissoc :media-id)
+ (assoc :thumbnail-uri (resolve-public-uri media-id)))
+ (dissoc row :media-id))))))
(s/def ::get-team-recent-files
(s/keys :req [::rpc/profile-id]
diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj
index 8a1f5cb72f..19233969ef 100644
--- a/backend/src/app/rpc/commands/files_thumbnails.clj
+++ b/backend/src/app/rpc/commands/files_thumbnails.clj
@@ -14,7 +14,6 @@
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.shape-tree :as ctt]
- [app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
@@ -39,10 +38,6 @@
;; --- COMMAND QUERY: get-file-object-thumbnails
-(defn- get-public-uri
- [media-id]
- (str (cf/get :public-uri) "/assets/by-id/" media-id))
-
(defn- get-object-thumbnails
([conn file-id]
(let [sql (str/concat
@@ -52,7 +47,7 @@
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
- (or (some-> row :media-id get-public-uri)
+ (or (some-> row :media-id files/resolve-public-uri)
(:data row))))
(d/without-nils))))
@@ -65,7 +60,7 @@
res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id
(fn [row]
- (or (some-> row :media-id get-public-uri)
+ (or (some-> row :media-id files/resolve-public-uri)
(:data row)))
res))))
@@ -85,8 +80,6 @@
;; --- COMMAND QUERY: get-file-thumbnail
-;; FIXME: refactor to support uploading data to storage
-
(defn get-file-thumbnail
[conn file-id revn]
(let [sql (sql/select :file-thumbnail
@@ -95,10 +88,15 @@
{:limit 1
:order-by [[:revn :desc]]})
row (db/exec-one! conn sql)]
+
(when-not row
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
+ (when-not (:data row)
+ (ex/raise :type :not-found
+ :code :file-thumbnail-not-found))
+
{:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row)
@@ -113,20 +111,16 @@
:opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail
- "Method used in frontend for obtain the file thumbnail (used in the
- dashboard)."
- {::doc/added "1.17"}
+ {::doc/added "1.17"
+ ::doc/deprecated "1.19"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration))))
-
;; --- COMMAND QUERY: get-file-data-for-thumbnail
-;; FIXME: performance issue, handle new media_id
-;;
;; We need to improve how we set frame for thumbnail in order to avoid
;; loading all pages into memory for find the frame set for thumbnail.
@@ -427,24 +421,27 @@
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props
- (:id media) props])))
-
-(s/def ::media ::media/upload)
-(s/def ::create-file-thumbnail
- (s/keys :req [::rpc/profile-id]
- :req-un [::file-id ::revn ::props ::media]))
+ (:id media) props])
+ media))
(sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.19"
- ::audit/skip true}
+ ::audit/skip true
+ ::sm/params [:map {:title "create-file-thumbnail"}
+ [:file-id ::sm/uuid]
+ [:revn :int]
+ [:media ::media/upload]]
+ }
+
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
- (-> cfg
- (update ::sto/storage media/configure-assets-storage)
- (assoc ::db/conn conn)
- (create-file-thumbnail! params))
- nil)))
+ (let [media (-> cfg
+ (update ::sto/storage media/configure-assets-storage)
+ (assoc ::db/conn conn)
+ (create-file-thumbnail! params))]
+
+ {:uri (files/resolve-public-uri (:id media))}))))
diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj
index 9eceac13d2..9b9c2134a9 100644
--- a/backend/src/app/tasks/file_gc.clj
+++ b/backend/src/app/tasks/file_gc.clj
@@ -184,7 +184,7 @@
(when (seq res)
(doseq [media-id (into #{} (keep :media-id) res)]
;; Mark as deleted the storage object related with the
- ;; photo-id field.
+ ;; media-id field.
(l/trace :hint "mark storage object as deleted" :id media-id)
(sto/del-object! storage media-id))
diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj
index 84687d04f0..14b0f72da7 100644
--- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj
+++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj
@@ -141,7 +141,7 @@
)))
-(t/deftest upsert-file-thumbnail
+(t/deftest create-file-thumbnail
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
@@ -159,7 +159,6 @@
data2 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
- :props {}
:revn 2
:media {:filename "sample.jpg"
:size 7923
@@ -169,7 +168,6 @@
data3 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
- :props {}
:revn 3
:media {:filename "sample.jpg"
:size 312043
@@ -183,11 +181,11 @@
(let [out (th/command! data2)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
- (t/is (nil? (:result out))))
+ (t/is (contains? (:result out) :uri)))
(let [out (th/command! data3)]
(t/is (nil? (:error out)))
- (t/is (nil? (:result out))))
+ (t/is (contains? (:result out) :uri)))
(let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail
{:file-id (:id file)}
diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js
index f07eba915e..2083b09dff 100644
--- a/frontend/gulpfile.js
+++ b/frontend/gulpfile.js
@@ -131,7 +131,8 @@ function readManifest() {
"polyfills": "js/polyfills.js",
"main": "js/main.js",
"shared": "js/shared.js",
- "worker": "js/worker.js"
+ "worker": "js/worker.js",
+ "thumbnail-renderer": "js/thumbnail-renderer.js"
};
}
}
@@ -242,7 +243,17 @@ gulp.task("template:render", templatePipeline({
output: paths.output
}));
-gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main", "template:render"));
+gulp.task("template:thumbnail-renderer", templatePipeline({
+ name: "thumbnail-renderer.html",
+ input: paths.resources + "templates/thumbnail-renderer.mustache",
+ output: paths.output
+}));
+
+gulp.task("templates", gulp.series("svg:sprite:icons",
+ "svg:sprite:cursors",
+ "template:main",
+ "template:render",
+ "template:thumbnail-renderer"));
gulp.task("polyfills", function() {
return gulp.src(paths.resources + "polyfills/*.js")
diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss
index d2c9b8fe28..2108b42d35 100644
--- a/frontend/resources/styles/main/partials/dashboard-grid.scss
+++ b/frontend/resources/styles/main/partials/dashboard-grid.scss
@@ -51,6 +51,10 @@
border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%);
text-align: initial;
+
+ img {
+ object-fit: contain;
+ }
}
&.dragged {
diff --git a/frontend/resources/templates/thumbnail-renderer.mustache b/frontend/resources/templates/thumbnail-renderer.mustache
new file mode 100644
index 0000000000..261cd05c08
--- /dev/null
+++ b/frontend/resources/templates/thumbnail-renderer.mustache
@@ -0,0 +1,26 @@
+
+
+
+
+ Penpot - Thumbnail Renderer
+
+
+
+
+ {{# manifest}}
+
+
+
+ {{/manifest}}
+
+
+
+ {{# manifest}}
+
+
+ {{/manifest}}
+
+
diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn
index bb641ad0ce..ea6faa935f 100644
--- a/frontend/shadow-cljs.edn
+++ b/frontend/shadow-cljs.edn
@@ -16,17 +16,25 @@
:modules
{:shared {:entries []}
- :main {:entries [app.main]
- :depends-on #{:shared}
- :init-fn app.main/init}
+ :main
+ {:entries [app.main]
+ :depends-on #{:shared}
+ :init-fn app.main/init}
- :render {:entries [app.render]
- :depends-on #{:shared}
- :init-fn app.render/init}
+ :render
+ {:entries [app.render]
+ :depends-on #{:shared}
+ :init-fn app.render/init}
- :worker {:entries [app.worker]
- :web-worker true
- :depends-on #{:shared}}}
+ :worker
+ {:entries [app.worker]
+ :web-worker true
+ :depends-on #{:shared}}
+
+ :thumbnail-renderer
+ {:entries [app.thumbnail-renderer]
+ :depends-on #{:shared}
+ :init-fn app.thumbnail-renderer/init}}
:compiler-options
{:output-feature-set :es2020
diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs
index 4f5191ab08..72c22713b9 100644
--- a/frontend/src/app/config.cljs
+++ b/frontend/src/app/config.cljs
@@ -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
diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs
index 362ca0a589..ae4fa5fddd 100644
--- a/frontend/src/app/main.cljs
+++ b/frontend/src/app/main.cljs
@@ -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)))
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index fdf4224cfe..ce07c32894 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -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)
diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs
index a9386ca2f6..4a4dc8b6f3 100644
--- a/frontend/src/app/main/fonts.cljs
+++ b/frontend/src/app/main/fonts.cljs
@@ -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)) "")))
diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs
index ca834ce207..2221eae293 100644
--- a/frontend/src/app/main/repo.cljs
+++ b/frontend/src/app/main/repo.cljs
@@ -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}
diff --git a/frontend/src/app/main/thumbnail_renderer.cljs b/frontend/src/app/main/thumbnail_renderer.cljs
new file mode 100644
index 0000000000..54c668af77
--- /dev/null
+++ b/frontend/src/app/main/thumbnail_renderer.cljs
@@ -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)))
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 4384e97b5b..27fc812ada 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -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
diff --git a/frontend/src/app/thumbnail_renderer.cljs b/frontend/src/app/thumbnail_renderer.cljs
new file mode 100644
index 0000000000..ad05d322fd
--- /dev/null
+++ b/frontend/src/app/thumbnail_renderer.cljs
@@ -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