From d11b007795fb9841251f304db225553be84fbe6b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Jun 2023 17:14:50 +0200 Subject: [PATCH 1/5] :zap: Add thumbnail renderer And integrate the dashboard thumbnails to use that service --- backend/src/app/rpc/commands/files.clj | 29 ++- .../src/app/rpc/commands/files_thumbnails.clj | 51 ++-- backend/src/app/tasks/file_gc.clj | 2 +- .../rpc_file_thumbnails_test.clj | 8 +- frontend/gulpfile.js | 15 +- .../styles/main/partials/dashboard-grid.scss | 4 + .../templates/thumbnail-renderer.mustache | 26 ++ frontend/shadow-cljs.edn | 26 +- frontend/src/app/config.cljs | 10 +- frontend/src/app/main.cljs | 2 + frontend/src/app/main/data/dashboard.cljs | 9 + frontend/src/app/main/fonts.cljs | 81 +++--- frontend/src/app/main/repo.cljs | 5 + frontend/src/app/main/thumbnail_renderer.cljs | 93 +++++++ frontend/src/app/main/ui/dashboard/grid.cljs | 65 +++-- frontend/src/app/thumbnail_renderer.cljs | 245 ++++++++++++++++++ frontend/src/app/util/dom.cljs | 10 +- frontend/src/app/util/webapi.cljs | 4 + frontend/src/app/worker/thumbnails.cljs | 77 +----- 19 files changed, 579 insertions(+), 183 deletions(-) create mode 100644 frontend/resources/templates/thumbnail-renderer.mustache create mode 100644 frontend/src/app/main/thumbnail_renderer.cljs create mode 100644 frontend/src/app/thumbnail_renderer.cljs diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index d1da43c97..ff24bed62 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 8a1f5cb72..19233969e 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 9eceac13d..9b9c2134a 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 84687d04f..14b0f72da 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 f07eba915..2083b09df 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 d2c9b8fe2..2108b42d3 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 000000000..261cd05c0 --- /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 bb641ad0c..ea6faa935 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 4f5191ab0..72c22713b 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 362ca0a58..ae4fa5fdd 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 fdf4224cf..ce07c3289 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 a9386ca2f..4a4dc8b6f 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 ca834ce20..2221eae29 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 000000000..54c668af7 --- /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 4384e97b5..27fc812ad 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 000000000..ad05d322f --- /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