0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-08 07:50:43 -05:00

Merge pull request #3272 from penpot/azazeln28-thumbnail-renderer

🎉 Add thumbnail renderer service
This commit is contained in:
Alejandro 2023-06-22 13:45:07 +02:00 committed by GitHub
commit 74e8081574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 704 additions and 305 deletions

View file

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

View file

@ -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.
@ -310,14 +304,17 @@
(:id media) (:id media)])))
(s/def ::media (s/nilable ::media/upload))
(s/def ::create-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id ::media]))
(def schema:create-file-object-thumbnail
[:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid]
[:object-id :string]
[:media ::media/upload]])
(sv/defmethod ::create-file-object-thumbnail
{:doc/added "1.19"
::audit/skip true}
::audit/skip true
::sm/params schema:create-file-object-thumbnail}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
@ -380,7 +377,6 @@
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])))
(s/def ::revn ::us/integer)
(s/def ::props map?)
@ -427,24 +423,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))}))))

View file

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

View file

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

View file

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

View file

@ -51,6 +51,10 @@
border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%);
text-align: initial;
img {
object-fit: contain;
}
}
&.dragged {

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Penpot - Thumbnail Renderer</title>
<link rel="icon" href="images/favicon.png" />
<script>
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
</script>
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker}}"</script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
{{/manifest}}
</head>
<body>
{{# manifest}}
<script src="{{& shared}}"></script>
<script src="{{& thumbnail-renderer}}"></script>
{{/manifest}}
</body>
</html>

View file

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

View file

@ -79,21 +79,21 @@
"unknown"
date)))
;; --- Globar Config Vars
(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"))
(def build-date (parse-build-date global))
(def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))
(def target (atom (parse-target global)))
(def browser (atom (parse-browser)))
(def platform (atom (parse-platform)))
(def flags (parse-flags global))
(def version (parse-version global))
(def target (parse-target global))
(def browser (parse-browser))
(def platform (parse-platform))
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
@ -108,36 +108,43 @@
(update :path #(str % "/")))))
(def public-uri
(atom
(normalize-uri (or (obj/get global "penpotPublicURI")
(.-origin ^js location)))))
(normalize-uri (or (obj/get global "penpotPublicURI")
(obj/get location "origin"))))
(def thumbnail-renderer-uri
(or (some-> (obj/get global "penpotThumbnailRendererURI") normalize-uri)
public-uri))
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]
(dm/assert! (contains? valid-browsers candidate))
(= candidate @browser))
(= candidate browser))
(defn ^boolean check-platform? [candidate]
(dm/assert! (contains? valid-platforms candidate))
(= candidate @platform))
(= candidate platform))
(defn resolve-profile-photo-url
[{:keys [photo-id fullname name] :as profile}]
(if (nil? photo-id)
(avatars/generate {:name (or fullname name)})
(str (u/join @public-uri "assets/by-id/" photo-id))))
(dm/str (u/join public-uri "assets/by-id/" photo-id))))
(defn resolve-team-photo-url
[{:keys [photo-id name] :as team}]
(if (nil? photo-id)
(avatars/generate {:name name})
(str (u/join @public-uri "assets/by-id/" photo-id))))
(dm/str (u/join public-uri "assets/by-id/" photo-id))))
(defn resolve-file-media
([media]
(resolve-file-media media false))
([{:keys [id] :as media} thumbnail?]
(str (cond-> (u/join @public-uri "assets/by-file-media-id/")
(true? thumbnail?) (u/join (str id "/thumbnail"))
(false? thumbnail?) (u/join (str id))))))
(dm/str
(cond-> (u/join public-uri "assets/by-file-media-id/")
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
(false? thumbnail?) (u/join (dm/str id))))))

View file

@ -6,6 +6,7 @@
(ns app.main
(:require
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -15,6 +16,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]
@ -34,12 +36,12 @@
(log/setup! {:app :info})
(when (= :browser @cf/target)
(when (= :browser cf/target)
(log/info :message "Welcome to penpot"
:version (:full @cf/version)
:version (:full cf/version)
:asserts *assert*
:build-date cf/build-date
:public-uri (str @cf/public-uri)))
:public-uri (dm/str cf/public-uri)))
(declare reinit)
@ -80,6 +82,7 @@
(i18n/init! cf/translations)
(theme/init! cf/themes)
(cur/init-styles)
(tr/init!)
(init-ui)
(st/emit! (initialize)))

View file

@ -475,7 +475,7 @@
(rx/map (fn [params]
(rt/resolve router :auth-verify-token {} params)))
(rx/map (fn [fragment]
(assoc @cf/public-uri :fragment fragment)))
(assoc cf/public-uri :fragment fragment)))
(rx/tap (fn [uri]
(wapi/write-to-clipboard (str uri))))
(rx/tap on-success)
@ -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)

View file

@ -39,7 +39,7 @@
[]
(let [uagent (UAParser.)]
(merge
{:app-version (:full @cf/version)
{:app-version (:full cf/version)
:locale @i18n/locale}
(let [browser (.getBrowser uagent)]
{:browser (obj/get browser "name")
@ -215,7 +215,7 @@
(defn- persist-events
[events]
(if (seq events)
(let [uri (u/join @cf/public-uri "api/rpc/command/push-audit-events")
(let [uri (u/join cf/public-uri "api/rpc/command/push-audit-events")
params {:uri uri
:method :post
:credentials "include"
@ -230,7 +230,7 @@
(defn initialize
[]
(when (contains? @cf/flags :audit-log)
(when (contains? cf/flags :audit-log)
(ptk/reify ::initialize
ptk/EffectEvent
(effect [_ _ stream]

View file

@ -377,7 +377,7 @@
(ptk/reify ::mark-onboarding-as-viewed
ptk/WatchEvent
(watch [_ _ _]
(let [version (or version (:main @cf/version))
(let [version (or version (:main cf/version))
props {:onboarding-viewed true
:release-notes-viewed version}]
(->> (rp/cmd! :update-profile-props {:props props})

View file

@ -22,7 +22,7 @@
(defn- prepare-uri
[params]
(let [base (-> @cf/public-uri
(let [base (-> cf/public-uri
(u/join "ws/notifications")
(assoc :query (u/map->query-string params)))]
(cond-> base

View file

@ -87,7 +87,7 @@
(log/trace :hint "event:initialize" :fn "features")
(rx/concat
;; Enable all features set on the configuration
(->> (rx/from @cf/flags)
(->> (rx/from cf/flags)
(rx/map name)
(rx/map (fn [flag]
(when (str/starts-with? flag "frontend-feature-")

View file

@ -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]
@ -81,8 +82,12 @@
;; FONTS LOADING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce loaded (l/atom #{}))
(defonce loading (l/atom {}))
(defonce ^:dynamic loaded (l/atom #{}))
(defonce ^:dynamic loading (l/atom {}))
;; NOTE: mainly used on worker, when you don't really need load font
;; only know if the font is needed or not
(defonce ^:dynamic loaded-hints (l/atom #{}))
(defn- create-link-element
[uri]
@ -148,31 +153,26 @@
;; --- 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
[asset-id]
(str (u/join @cf/public-uri "assets/by-id/" asset-id)))
(str (u/join cf/public-uri "assets/by-id/" asset-id)))
(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}]
@ -194,34 +194,35 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ensure-loaded!
[id]
(log/debug :action "try-ensure-loaded!" :font-id id)
(if-not (exists? js/window)
([font-id] (ensure-loaded! font-id nil))
([font-id variant-id]
(log/debug :action "try-ensure-loaded!" :font-id font-id :variant-id variant-id)
(if-not (exists? js/window)
;; If we are in the worker environment, we just mark it as loaded
;; without really loading it.
(do
(swap! loaded conj id)
(p/resolved id))
(swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id})
(p/resolved font-id))
(let [font (get @fontsdb id)]
(let [font (get @fontsdb font-id)]
(cond
(nil? font)
(p/resolved id)
(p/resolved font-id)
;; Font already loaded, we just continue
(contains? @loaded id)
(p/resolved id)
(contains? @loaded font-id)
(p/resolved font-id)
;; Font is currently downloading. We attach the caller to the promise
(contains? @loading id)
(p/resolved (get @loading id))
(contains? @loading font-id)
(p/resolved (get @loading font-id))
;; First caller, we create the promise and then wait
:else
(let [on-load (fn [resolve]
(swap! loaded conj id)
(swap! loading dissoc id)
(resolve id))
(swap! loaded conj font-id)
(swap! loading dissoc font-id)
(resolve font-id))
load-p (p/create
(fn [resolve _]
@ -229,34 +230,27 @@
(assoc ::on-loaded (partial on-load resolve))
(load-font))))]
(swap! loading assoc id load-p)
load-p)))))
(swap! loading assoc font-id load-p)
load-p))))))
(defn ready
[cb]
(-> (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 +261,45 @@
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
[font-refs]
(->> (rx/from font-refs)
(rx/mapcat fetch-font-css)
(rx/reduce (fn [acc css] (dm/str acc "\n" css)) "")))

View file

@ -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}
@ -79,7 +84,7 @@
:else :post)
request {:method method
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:headers {"accept" "application/transit+json"}
:body (when (= method :post)
@ -105,7 +110,7 @@
(defmethod cmd! :login-with-oidc
[_ {:keys [provider] :as params}]
(let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider))
(let [uri (u/join cf/public-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)]
(->> (http/send! {:method :post
:uri uri
@ -117,7 +122,7 @@
(defn- send-export
[{:keys [blob?] :as params}]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/export")
:uri (u/join cf/public-uri "api/export")
:body (http/transit-data (dissoc params :blob?))
:credentials "include"
:response-type (if blob? :blob :text)})
@ -136,7 +141,7 @@
(defmethod cmd! ::multipart-upload
[id params]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:body (http/form-data params)})
(rx/map http/conditional-decode-transit)

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

View file

@ -98,9 +98,9 @@
[:& app.main.ui.onboarding/onboarding-modal {}]
(and (:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main @cf/version))
(not= "0.0" (:main @cf/version)))
[:& app.main.ui.releases/release-notes-modal {:version (:main @cf/version)}]))
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))
[:& app.main.ui.releases/release-notes-modal {:version (:main cf/version)}]))
[:& dashboard {:route route :profile profile}]]

View file

@ -28,7 +28,7 @@
[rumext.v2 :as mf]))
(def show-alt-login-buttons?
(some (partial contains? @cf/flags)
(some (partial contains? cf/flags)
[:login-with-google
:login-with-github
:login-with-gitlab
@ -175,13 +175,13 @@
:label (tr "auth.password")}]]
[:div.buttons-stack
(when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password))
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password))
[:& fm/submit-button
{:label (tr "auth.login-submit")
:data-test "login-submit"}])
(when (contains? @cf/flags :login-with-ldap)
(when (contains? cf/flags :login-with-ldap)
[:& fm/submit-button
{:label (tr "auth.login-with-ldap-submit")
:on-click on-submit-ldap}])]]]))
@ -189,25 +189,25 @@
(mf/defc login-buttons
[{:keys [params] :as props}]
[:div.auth-buttons
(when (contains? @cf/flags :login-with-google)
(when (contains? cf/flags :login-with-google)
[:& bl/button-link {:action #(login-with-oidc % :google params)
:icon i/brand-google
:name (tr "auth.login-with-google-submit")
:klass "btn-google-auth"}])
(when (contains? @cf/flags :login-with-github)
(when (contains? cf/flags :login-with-github)
[:& bl/button-link {:action #(login-with-oidc % :github params)
:icon i/brand-github
:name (tr "auth.login-with-github-submit")
:klass "btn-github-auth"}])
(when (contains? @cf/flags :login-with-gitlab)
(when (contains? cf/flags :login-with-gitlab)
[:& bl/button-link {:action #(login-with-oidc % :gitlab params)
:icon i/brand-gitlab
:name (tr "auth.login-with-gitlab-submit")
:klass "btn-gitlab-auth"}])
(when (contains? @cf/flags :login-with-oidc)
(when (contains? cf/flags :login-with-oidc)
[:& bl/button-link {:action #(login-with-oidc % :oidc params)
:icon i/brand-openid
:name (tr "auth.login-with-oidc-submit")
@ -215,7 +215,7 @@
(mf/defc login-button-oidc
[{:keys [params] :as props}]
(when (contains? @cf/flags :login-with-oidc)
(when (contains? cf/flags :login-with-oidc)
[:div.link-entry.link-oidc
[:a {:tab-index "0"
:on-key-down (fn [event]
@ -236,17 +236,17 @@
[:& login-buttons {:params params}]
(when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap))
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password)
(contains? cf/flags :login-with-ldap))
[:span.separator
[:span.line]
[:span.text (tr "labels.or")]
[:span.line]])])
(when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap))
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password)
(contains? cf/flags :login-with-ldap))
[:& login-form {:params params :on-success-callback on-success-callback}])])
(mf/defc login-page
@ -258,21 +258,21 @@
[:& login-methods {:params params}]
[:div.links
(when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password))
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password))
[:div.link-entry
[:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request))
:data-test "forgot-password"}
(tr "auth.forgot-password")]])
(when (contains? @cf/flags :registration)
(when (contains? cf/flags :registration)
[:div.link-entry
[:span (tr "auth.register") " "]
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params))
:data-test "register-submit"}
(tr "auth.register-submit")]])]
(when (contains? @cf/flags :demo-users)
(when (contains? cf/flags :demo-users)
[:div.links.demo
[:div.link-entry
[:span (tr "auth.create-demo-profile") " "]

View file

@ -141,8 +141,8 @@
[:& login/login-buttons {:params params}]
(when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-ldap))
(when (or (contains? cf/flags :login)
(contains? cf/flags :login-with-ldap))
[:span.separator
[:span.line]
[:span.text (tr "labels.or")]
@ -157,7 +157,7 @@
[:h1 {:data-test "registration-title"} (tr "auth.register-title")]
[:div.subtitle (tr "auth.register-subtitle")]
(when (contains? @cf/flags :demo-warning)
(when (contains? cf/flags :demo-warning)
[:& demo-warning])
[:& register-methods {:params params}]
@ -170,7 +170,7 @@
:data-test "login-here-link"}
(tr "auth.login-here")]]
(when (contains? @cf/flags :demo-users)
(when (contains? cf/flags :demo-users)
[:div.link-entry
[:span (tr "auth.create-demo-profile") " "]
[:& lk/link {:action #(st/emit! (du/create-demo-profile))}
@ -207,7 +207,7 @@
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean)
(if (contains? @cf/flags :terms-and-privacy-checkbox)
(if (contains? cf/flags :terms-and-privacy-checkbox)
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
:opt-un [::accept-newsletter-subscription]))
@ -244,7 +244,7 @@
:label (tr "auth.fullname")
:type "text"}]]
(when (contains? @cf/flags :terms-and-privacy-checkbox)
(when (contains? cf/flags :terms-and-privacy-checkbox)
[:div.fields-row.input-visible.accept-terms-and-privacy-wrapper
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"

View file

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

View file

@ -376,7 +376,7 @@
:data-test "team-invitations"}
(tr "labels.invitations")]
(when (contains? @cf/flags :webhooks)
(when (contains? cf/flags :webhooks)
[:& dropdown-menu-item {:on-click go-webhooks
:on-key-down (fn [event]
(when (kbd/enter? event)
@ -459,7 +459,7 @@
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
options-ids ["teams-options-members"
"teams-options-invitations"
(when (contains? @cf/flags :webhooks)
(when (contains? cf/flags :webhooks)
"teams-options-webhooks")
"teams-options-settings"
(when can-rename?
@ -680,7 +680,7 @@
show-release-notes
(mf/use-callback
(fn [event]
(let [version (:main @cf/version)]
(let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding}))
@ -769,7 +769,7 @@
(dom/open-new-window "https://penpot.app/terms")))}
[:span (tr "auth.terms-of-service")]]
(when (contains? @cf/flags :user-feedback)
(when (contains? cf/flags :user-feedback)
[:li.separator {:tab-index (if show
"0"
"-1")

View file

@ -38,7 +38,7 @@
[:div.modal-left.welcome
[:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
[:div.modal-right
[:div.release-container [:span.release "Version " (:main @cf/version)]]
[:div.release-container [:span.release "Version " (:main cf/version)]]
[:div.right-content
[:div.modal-title
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]]
@ -73,7 +73,7 @@
[:div.modal-left.welcome
[:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
[:div.modal-right
[:div.release-container [:span.release "Version " (:main @cf/version)]]
[:div.release-container [:span.release "Version " (:main cf/version)]]
[:div.right-content
[:div.modal-title
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]]
@ -112,7 +112,7 @@
skip
(mf/use-fn
#(st/emit! (modal/hide)
(if (contains? @cf/flags :newsletter-subscription)
(if (contains? cf/flags :newsletter-subscription)
(modal/show {:type :onboarding-newsletter-modal})
(modal/show {:type :onboarding-team}))
(du/mark-onboarding-as-viewed)))]

View file

@ -6,6 +6,7 @@
(ns app.main.ui.onboarding.templates
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
@ -21,7 +22,7 @@
(mf/defc template-item
[{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false)
link (str (assoc @cf/public-uri :path path))
link (dm/str (assoc cf/public-uri :path path))
on-finish-import
(fn []

View file

@ -56,11 +56,11 @@
(mf/use-callback
(mf/deps profile)
#(st/emit! (rt/nav :settings-access-tokens)))
show-release-notes
(mf/use-callback
(fn [event]
(let [version (:main @cf/version)]
(let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding}))
@ -91,7 +91,7 @@
i/tree
[:span.element-title (tr "labels.settings")]]
(when (contains? @cf/flags :access-tokens)
(when (contains? cf/flags :access-tokens)
[:li {:class (when access-tokens? "current")
:on-click go-settings-access-tokens
:data-test "settings-access-tokens"}
@ -104,7 +104,7 @@
i/pencil
[:span.element-title (tr "labels.release-notes")]]
(when (contains? @cf/flags :user-feedback)
(when (contains? cf/flags :user-feedback)
[:li {:class (when feedback? "current")
:on-click go-settings-feedback}
i/msg-info

View file

@ -16,10 +16,13 @@
(defn- load-fonts!
[content]
(let [default (:font-id txt/default-text-attrs)]
(let [extract-fn (juxt :font-id :font-variant-id)
default (extract-fn txt/default-text-attrs)]
(->> (tree-seq map? :children content)
(into #{default} (keep :font-id))
(run! fonts/ensure-loaded!))))
(into #{default} (keep extract-fn))
(run! (fn [[font-id variant-id]]
(when (some? font-id)
(fonts/ensure-loaded! font-id variant-id)))))))
(mf/defc text-shape
{::mf/wrap-props false}

View file

@ -145,7 +145,7 @@
(assoc qparams :zoom zoom-type))
href (rt/resolve router :viewer pparams qparams)]
(assoc @cf/public-uri :fragment href)))]
(assoc cf/public-uri :fragment href)))]
(reset! link (some-> href str)))))
[:div.modal-overlay.transparent.share-modal

View file

@ -158,7 +158,7 @@
show-release-notes
(mf/use-fn
(fn [event]
(let [version (:main @cf/version)]
(let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding}))
@ -186,7 +186,7 @@
[:span (tr "label.shortcuts")]
[:span.shortcut (sc/get-tooltip :show-shortcuts)]]
(when (contains? @cf/flags :user-feedback)
(when (contains? cf/flags :user-feedback)
[:*
[:li.feedback {:on-click nav-to-feedback}
[:span (tr "labels.give-feedback")]]])]]))

View file

@ -10,35 +10,30 @@
[app.main.errors :as err]
[app.util.worker :as uw]))
(defonce instance (atom nil))
(defn- update-public-uri!
[instance val]
(uw/ask! instance {:cmd :configure
:key :public-uri
:val val}))
(defonce instance nil)
(defn init!
[]
(let [worker (uw/init cf/worker-uri err/on-error)]
(update-public-uri! worker @cf/public-uri)
(add-watch cf/public-uri ::worker-public-uri (fn [_ _ _ val] (update-public-uri! worker val)))
(reset! instance worker)))
(uw/ask! worker {:cmd :configure
:key :public-uri
:val cf/public-uri})
(set! instance worker)))
(defn ask!
([message]
(when @instance (uw/ask! @instance message)))
(when instance (uw/ask! instance message)))
([message transfer]
(when @instance (uw/ask! @instance message transfer))))
(when instance (uw/ask! instance message transfer))))
(defn ask-buffered!
([message]
(when @instance (uw/ask-buffered! @instance message)))
(when instance (uw/ask-buffered! instance message)))
([message transfer]
(when @instance (uw/ask-buffered! @instance message transfer))))
(when instance (uw/ask-buffered! instance message transfer))))
(defn ask-many!
([message]
(when @instance (uw/ask-many! @instance message)))
(when instance (uw/ask-many! instance message)))
([message transfer]
(when @instance (uw/ask-many! @instance message transfer))))
(when instance (uw/ask-many! instance message transfer))))

View file

@ -0,0 +1,246 @@
;; 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))
(defn ^:export init
[]
(listen)
(send-ready!)
(log/info :hint "initialized"
:public-uri (dm/str cf/public-uri)
:parent-uri (dm/str parent-origin)))

View file

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

View file

@ -51,7 +51,7 @@
(into {} (map vec) (seq (.entries ^js headers))))
(def default-headers
{"x-frontend-version" (:full @cfg/version)})
{"x-frontend-version" (:full cfg/version)})
(defn fetch
[{:keys [method uri query headers body mode omit-default-headers credentials]

View file

@ -117,7 +117,7 @@
(let [router (:router state)
path (resolve router rname path-params query-params)
name (or name "_blank")
uri (assoc @cf/public-uri :fragment path)]
uri (assoc cf/public-uri :fragment path)]
(dom/open-new-window uri name nil)))))
(defn nav-back

View file

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

View file

@ -51,7 +51,7 @@
(defmethod handler :configure
[{:keys [key val]}]
(log/info :hint "configure worker" :key key :val val)
(log/info :hint "configure worker" :key key :val (dm/str val))
(case key
:public-uri
(reset! cf/public-uri val)))
(set! cf/public-uri val)))

View file

@ -18,7 +18,7 @@
[app.util.webapi :as wapi]
[app.worker.impl :as impl]
[beicon.core :as rx]
[debug :refer [debug?]]
[okulary.core :as l]
[promesa.core :as p]
[rumext.v2 :as mf]))
@ -45,15 +45,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"
@ -62,77 +53,32 @@
:strip-frames-with-thumbnails true
:features features}
request {:method :get
:uri (u/join @cf/public-uri path)
: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- 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)]
{:data data
:fonts (into @fonts/loaded (map first) @fonts/loading)
:file-id file-id
:revn revn}))
(binding [fonts/loaded-hints (l/atom #{})]
(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)]
(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)))))
{:data data
:fonts @fonts/loaded-hints
:file-id file-id
:revn revn})))
(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]