♻️ Refactor exportation process, make it considerably faster

This commit is contained in:
Andrey Antukh 2022-03-29 12:34:11 +02:00 committed by Andrés Moya
parent d6abd2202c
commit 9140fc71b9
33 changed files with 1096 additions and 1090 deletions

View file

@ -10,10 +10,12 @@
funcool/beicon {:mvn/version "2021.07.05-1"}
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2021.09.20-0"}
funcool/rumext {:mvn/version "2022.01.20.128"}
funcool/rumext {:mvn/version "2022.03.28-131"}
funcool/tubax {:mvn/version "2021.05.20-0"}
instaparse/instaparse {:mvn/version "1.4.10"}
garden/garden {:mvn/version "1.3.10"}
}
:aliases

View file

@ -6,7 +6,6 @@
(ns app.main.data.exports
(:require
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.workspace.persistence :as dwp]
@ -47,6 +46,7 @@
state
(dissoc state :export))))))
(defn show-workspace-export-dialog
([] (show-workspace-export-dialog nil))
([{:keys [selected]}]
@ -55,8 +55,6 @@
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
filename (-> (wsh/lookup-page state page-id) :name)
selected (or selected (wsh/lookup-selected state page-id {}))
shapes (if (seq selected)
@ -74,11 +72,10 @@
(assoc :name (:name shape))))]
(rx/of (modal/show :export-shapes
{:exports (vec exports)
:filename filename})))))))
{:exports (vec exports)})))))))
(defn show-viewer-export-dialog
[{:keys [shapes filename page-id file-id exports]}]
[{:keys [shapes page-id file-id exports]}]
(ptk/reify ::show-viewer-export-dialog
ptk/WatchEvent
(watch [_ _ _]
@ -91,51 +88,44 @@
(assoc :object-id (:id shape))
(assoc :shape (dissoc shape :exports))
(assoc :name (:name shape))))]
(rx/of (modal/show :export-shapes {:exports (vec exports)
:filename filename}))))))
(rx/of (modal/show :export-shapes {:exports (vec exports)}))))))
(defn show-workspace-export-frames-dialog
([frames]
(ptk/reify ::show-workspace-export-frames-dialog
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
filename (-> (wsh/lookup-page state page-id)
:name
(dm/str ".pdf"))
[frames]
(ptk/reify ::show-workspace-export-frames-dialog
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
exports (for [frame frames]
{:enabled true
:page-id page-id
:file-id file-id
:object-id (:id frame)
:shape frame
:name (:name frame)})]
exports (for [frame frames]
{:enabled true
:page-id page-id
:file-id file-id
:frame-id (:id frame)
:shape frame
:name (:name frame)})]
(rx/of (modal/show :export-frames
{:exports (vec exports)
:filename filename})))))))
(rx/of (modal/show :export-frames
{:exports (vec exports)}))))))
(defn- initialize-export-status
[exports filename resource-id query-name]
[exports cmd resource]
(ptk/reify ::initialize-export-status
ptk/UpdateEvent
(update [_ state]
(assoc state :export {:in-progress true
:resource-id resource-id
:resource-id (:id resource)
:healthy? true
:error false
:progress 0
:widget-visible true
:detail-visible true
:exports exports
:filename filename
:last-update (dt/now)
:query-name query-name}))))
:cmd cmd}))))
(defn- update-export-status
[{:keys [progress status resource-id name] :as data}]
[{:keys [done status resource-id filename] :as data}]
(ptk/reify ::update-export-status
ptk/UpdateEvent
(update [_ state]
@ -144,7 +134,7 @@
healthy? (< time-diff (dt/duration {:seconds 6}))]
(cond-> state
(= status "running")
(update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?)
(update :export assoc :progress done :last-update (dt/now) :healthy? healthy?)
(= status "error")
(update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?)
@ -155,12 +145,12 @@
ptk/WatchEvent
(watch [_ _ _]
(when (= status "ended")
(->> (rp/query! :download-export-resource resource-id)
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id})
(rx/delay 500)
(rx/map #(dom/trigger-download name %)))))))
(rx/map #(dom/trigger-download filename %)))))))
(defn request-simple-export
[{:keys [export filename]}]
[{:keys [export]}]
(ptk/reify ::request-simple-export
ptk/UpdateEvent
(update [_ state]
@ -170,22 +160,26 @@
(watch [_ state _]
(let [profile-id (:profile-id state)
params {:exports [export]
:profile-id profile-id}]
:profile-id profile-id
:cmd :export-shapes
:wait true}]
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rp/query! :export-shapes-simple params)
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero)))
(->> (rp/query! :export-shapes params)
(rx/mapcat (fn [{:keys [id filename]}]
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id})
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero))))))
(rx/catch (fn [cause]
(prn "KKKK" cause)
(rx/concat
(rx/of (clear-export-state uuid/zero))
(rx/throw cause))))))))))
(defn request-multiple-export
[{:keys [filename exports query-name]
:or {query-name :export-shapes-multiple}
[{:keys [exports cmd]
:or {cmd :export-shapes}
:as params}]
(ptk/reify ::request-multiple-export
ptk/WatchEvent
@ -194,7 +188,7 @@
profile-id (:profile-id state)
ws-conn (:ws-conn state)
params {:exports exports
:name filename
:cmd cmd
:profile-id profile-id
:wait false}
@ -219,11 +213,10 @@
;; Launch the exportation process and stores the resource id
;; locally.
(->> (rp/query! query-name params)
(rx/tap (fn [{:keys [id]}]
(vreset! resource-id id)))
(rx/map (fn [{:keys [id]}]
(initialize-export-status exports filename id query-name))))
(->> (rp/query! :exporter params)
(rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id)
(initialize-export-status exports cmd resource))))
;; We proceed to update the export state with incoming
;; progress updates. We delay the stoper for give some time
@ -246,13 +239,12 @@
(rx/map #(clear-export-state @resource-id))
(rx/take-until (rx/delay 6000 stoper))))))))
(defn retry-last-export
[]
(ptk/reify ::retry-last-export
ptk/WatchEvent
(watch [_ state _]
(let [params (select-keys (:export state) [:filename :exports :query-name])]
(let [params (select-keys (:export state) [:exports :cmd])]
(when (seq params)
(rx/of (request-multiple-export params)))))))

View file

@ -17,7 +17,6 @@
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[expound.alpha :as expound]
[fipp.edn :as fpp]
[potok.core :as ptk]))
@ -113,13 +112,12 @@
(ts/schedule
(st/emitf
(msg/show {:content "Internal error: assertion."
:type :error
:timeout 3000})))
:type :error
:timeout 3000})))
;; Print to the console some debugging info
(js/console.group message)
(js/console.info context)
(js/console.error (with-out-str (expound/printer error)))
(js/console.groupEnd message)))
;; That are special case server-errors that should be treated

View file

@ -14,7 +14,8 @@
(:require
["react-dom/server" :as rds]
[app.common.colors :as clr]
[app.common.geom.align :as gal]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@ -22,10 +23,12 @@
[app.common.pages.helpers :as cph]
[app.config :as cfg]
[app.main.fonts :as fonts]
[app.main.ui.context :as muc]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.export :as export]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.image :as image]
@ -57,11 +60,9 @@
:fill color}])
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
(let [shapes (cph/get-immediate-children objects)
rect (cond->> (gsh/selection-rect shapes)
(some? vport)
(gal/adjust-to-viewport vport))]
[objects]
(let [shapes (cph/get-immediate-children objects)
rect (gsh/selection-rect shapes)]
(-> rect
(update :x mth/finite 0)
(update :y mth/finite 0)
@ -156,24 +157,63 @@
(->> [x y width height]
(map #(ust/format-precision % viewbox-decimal-precision)))))
(defn adapt-root-frame
[objects object]
(let [shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (gsh/transform-shape object)]
(assoc object :fill-color "#f0f0f0")))
(defn adapt-objects-for-shape
[objects object-id]
(let [object (get objects object-id)
object (cond->> object
(cph/root-frame? object)
(adapt-root-frame objects))
;; Replace the previous object with the new one
objects (assoc objects object-id object)
modifier (-> (gpt/point (:x object) (:y object))
(gpt/negate)
(gmt/translate-matrix))
mod-ids (cons object-id (cph/get-children-ids objects object-id))
updt-fn #(-> %1
(assoc-in [%2 :modifiers :displacement] modifier)
(update %2 gsh/transform-shape))]
(reduce updt-fn objects mod-ids)))
(defn get-object-bounds
[objects object-id]
(let [object (get objects object-id)
padding (filters/calculate-padding object)
bounds (-> (filters/get-filters-bounds object)
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
(update :width + (* 2 (:horizontal padding)))
(update :height + (* 2 (:vertical padding))))]
(if (cph/group-shape? object)
(if (:masked-group? object)
(get-object-bounds objects (-> object :shapes first))
(->> (:shapes object)
(into [bounds] (map (partial get-object-bounds objects)))
(gsh/join-rects)))
bounds)))
(mf/defc page-svg
{::mf/wrap [mf/memo]}
[{:keys [data width height thumbnails? embed? include-metadata?] :as props
:or {embed? false include-metadata? false}}]
[{:keys [data thumbnails? render-embed? include-metadata?] :as props
:or {render-embed? false include-metadata? false}}]
(let [objects (:objects data)
shapes (cph/get-immediate-children objects)
root-children
(->> shapes
(remove cph/frame-shape?)
(mapcat #(cph/get-children-with-self objects (:id %))))
vport (when (and (some? width) (some? height))
{:width width :height height})
dim (calculate-dimensions data vport)
dim (calculate-dimensions objects)
vbox (format-viewbox dim)
background-color (get-in data [:options :background] default-color)
bgcolor (dm/get-in data [:options :background] default-color)
frame-wrapper
(mf/use-memo
@ -185,7 +225,7 @@
(mf/deps objects)
#(shape-wrapper-factory objects))]
[:& (mf/provider embed/context) {:value embed?}
[:& (mf/provider embed/context) {:value render-embed?}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
[:svg {:view-box vbox
:version "1.1"
@ -194,12 +234,17 @@
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100%"
:height "100%"
:background background-color}}
:background bgcolor}}
(when include-metadata?
[:& export/export-page {:options (:options data)}])
[:& ff/fontfaces-style {:shapes root-children}]
(let [shapes (->> shapes
(remove cph/frame-shape?)
(mapcat #(cph/get-children-with-self objects (:id %))))]
[:& ff/fontfaces-style {:shapes shapes}])
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
@ -214,6 +259,10 @@
[:& shape-wrapper {:shape item
:key (:id item)}])))]]]))
;; Component that serves for render frame thumbnails, mainly used in
;; the viewer and handoff
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
[{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}]
@ -260,6 +309,10 @@
[:> shape-container {:shape frame}
[:& frame/frame-thumbnail {:shape frame}]]))]))
;; Component for rendering a thumbnail of a single componenent. Mainly
;; used to render thumbnails on assets panel.
(mf/defc component-svg
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
[{:keys [objects group zoom] :or {zoom 1} :as props}]
@ -304,81 +357,122 @@
[:> shape-container {:shape group}
[:& group-wrapper {:shape group :view-box vbox}]]]))
(mf/defc object-svg
{::mf/wrap [mf/memo]}
[{:keys [objects object zoom render-texts? render-embed?]
:or {zoom 1 render-embed? false}
:as props}]
(let [object (cond-> object
(:hide-fill-on-export object)
(assoc :fills []))
obj-id (:id object)
x (* (:x object) zoom)
y (* (:y object) zoom)
width (* (:width object) zoom)
height (* (:height object) zoom)
vbox (dm/str x " " y " " width " " height)
frame-wrapper
(mf/with-memo [objects]
(frame-wrapper-factory objects))
group-wrapper
(mf/with-memo [objects]
(group-wrapper-factory objects))
shape-wrapper
(mf/with-memo [objects]
(shape-wrapper-factory objects))
text-shapes (sequence (filter cph/text-shape?) (vals objects))
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
[:& (mf/provider embed/context) {:value render-embed?}
[:svg {:id (dm/str "screenshot-" obj-id)
:view-box vbox
:width width
:height height
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
;; Fix Chromium bug about color of html texts
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
:style {:-webkit-print-color-adjust :exact}}
(let [shapes (cph/get-children objects obj-id)]
[:& ff/fontfaces-style {:shapes shapes}])
(case (:type object)
:frame [:& frame-wrapper {:shape object :view-box vbox}]
:group [:> shape-container {:shape object}
[:& group-wrapper {:shape object}]]
[:& shape-wrapper {:shape object}])]
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
[:svg
{:id (dm/str "screenshot-text-" (:id object))
:view-box (dm/str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SPRITES (DEBUG)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc component-symbol
{::mf/wrap-props false}
[props]
(let [id (obj/get props "id")
data (obj/get props "data")
name (:name data)
path (:path data)
objects (:objects data)
root (get objects id)
selrect (:selrect root)
[{:keys [id data] :as props}]
(let [name (:name data)
objects (-> (:objects data)
(adapt-objects-for-shape id))
object (get objects id)
selrect (:selrect object)
vbox
(format-viewbox
{:width (:width selrect)
:height (:height selrect)})
modifier
(mf/use-memo
(mf/deps (:x root) (:y root))
(fn []
(-> (gpt/point (:x root) (:y root))
(gpt/negate)
(gmt/translate-matrix))))
objects
(mf/use-memo
(mf/deps modifier id objects)
(fn []
(let [modifier-ids (cons id (cph/get-children-ids objects id))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)]
(reduce update-fn objects modifier-ids))))
root
(mf/use-memo
(mf/deps modifier root)
(fn [] (assoc-in root [:modifiers :displacement] modifier)))
group-wrapper
(mf/use-memo
(mf/deps objects)
(fn [] (group-wrapper-factory objects)))]
[:> "symbol" #js {:id (str id)
:viewBox vbox
"penpot:path" path}
[:> "symbol" #js {:id (str id) :viewBox vbox}
[:title name]
[:> shape-container {:shape root}
[:& group-wrapper {:shape root :view-box vbox}]]]))
[:> shape-container {:shape object}
[:& group-wrapper {:shape object :view-box vbox}]]]))
(mf/defc components-sprite-svg
{::mf/wrap-props false}
[props]
(let [data (obj/get props "data")
children (obj/get props "children")
embed? (obj/get props "embed?")
render-embed? (obj/get props "render-embed?")
include-metadata? (obj/get props "include-metadata?")]
[:& (mf/provider embed/context) {:value embed?}
[:& (mf/provider embed/context) {:value render-embed?}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata?}
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100vw"
:height "100vh"
:display (when-not (some? children) "none")}}
:style {:display (when-not (some? children) "none")}}
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
(for [[id data] (:components data)]
[:& component-symbol {:id id :key (dm/str id) :data data}])]
children]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; RENDERING
;; RENDER FOR DOWNLOAD (wrongly called exportation)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- get-image-data [shape]
@ -426,7 +520,7 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element page-svg #js {:data data :embed? true :include-metadata? true})]
(let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem)))))))
(defn render-components
@ -445,5 +539,6 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element components-sprite-svg #js {:data data :embed? true :include-metadata? true})]
(let [elem (mf/element components-sprite-svg
#js {:data data :render-embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem))))))))

View file

@ -105,34 +105,22 @@
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defn- send-export-command
[& {:keys [cmd params blob?]}]
(defn- send-export
[{:keys [blob?] :as params}]
(->> (http/send! {:method :post
:uri (u/join base-uri "api/export")
:body (http/transit-data (assoc params :cmd cmd))
:body (http/transit-data (dissoc params :blob?))
:credentials "include"
:response-type (if blob? :blob :text)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defmethod query :export-shapes-simple
(defmethod query :exporter
[_ params]
(let [params (merge {:wait true} params)]
(->> (rx/of params)
(rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false))
(rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true)))))
(defmethod query :export-shapes-multiple
[_ params]
(send-export-command :cmd :export-shapes :params params :blob? false))
(defmethod query :export-frames-multiple
[_ params]
(send-export-command :cmd :export-frames :params (assoc params :uri (str base-uri)) :blob? false))
(defmethod query :download-export-resource
[_ id]
(send-export-command :cmd :get-resource :params {:id id} :blob? true))
(let [default {:wait false
:blob? false
:uri (str base-uri)}]
(send-export (merge default params))))
(derive :upload-file-media-object ::multipart-upload)
(derive :update-profile-photo ::multipart-upload)

View file

@ -19,7 +19,6 @@
[app.main.ui.onboarding]
[app.main.ui.onboarding.questions]
[app.main.ui.releases]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings]
[app.main.ui.static :as static]
[app.main.ui.viewer :as viewer]
@ -110,15 +109,6 @@
:index index
:share-id share-id}]))
;; TODO: maybe move to `app.render` entrypoint (handled by render.html)
:render-sprite
(do
(let [file-id (uuid (get-in route [:path-params :file-id]))
component-id (get-in route [:query-params :component-id])
component-id (when (some? component-id) (uuid component-id))]
[:& render/render-sprite {:file-id file-id
:component-id component-id}]))
:workspace
(let [project-id (some-> params :path :project-id uuid)
file-id (some-> params :path :file-id uuid)

View file

@ -23,7 +23,7 @@
[rumext.alpha :as mf]))
(mf/defc export-multiple-dialog
[{:keys [exports filename title query-name no-selection]}]
[{:keys [exports title cmd no-selection]}]
(let [lstate (mf/deref refs/export)
in-progress? (:in-progress lstate)
@ -33,7 +33,10 @@
all-checked? (every? :enabled all-exports)
all-unchecked? (every? (complement :enabled) all-exports)
enabled-exports (into [] (filter :enabled) all-exports)
enabled-exports (into []
(comp (filter :enabled)
(map #(dissoc % :shape :enabled)))
all-exports)
cancel-fn
(fn [event]
@ -45,9 +48,8 @@
(dom/prevent-default event)
(st/emit! (modal/hide)
(de/request-multiple-export
{:filename filename
:exports enabled-exports
:query-name query-name})))
{:exports enabled-exports
:cmd cmd})))
on-toggle-enabled
(fn [index]
@ -145,25 +147,23 @@
(mf/defc export-shapes-dialog
{::mf/register modal/components
::mf/register-as :export-shapes}
[{:keys [exports filename]}]
[{:keys [exports]}]
(let [title (tr "dashboard.export-shapes.title")]
[:& export-multiple-dialog
{:exports exports
:filename filename
:title title
:query-name :export-shapes-multiple
:cmd :export-shapes
:no-selection shapes-no-selection}]))
(mf/defc export-frames
{::mf/register modal/components
::mf/register-as :export-frames}
[{:keys [exports filename]}]
[{:keys [exports]}]
(let [title (tr "dashboard.export-frames.title")]
[:& export-multiple-dialog
{:exports exports
:filename filename
:title title
:query-name :export-frames-multiple}]))
:cmd :export-frames}]))
(mf/defc export-progress-widget
{::mf/wrap [mf/memo]}

View file

@ -1,203 +0,0 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.render
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.fonts :as df]
[app.main.render :as render]
[app.main.repo :as repo]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.util.dom :as dom]
[beicon.core :as rx]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn calc-bounds
[object objects]
(let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects)))
padding (filters/calculate-padding object)
obj-bounds (-> (filters/get-filters-bounds object)
(update :x - (:horizontal padding))
(update :y - (:vertical padding))
(update :width + (* 2 (:horizontal padding)))
(update :height + (* 2 (:vertical padding))))]
(cond
(and (= :group (:type object))
(:masked-group? object))
(calc-bounds (get objects (first (:shapes object))) objects)
(= :group (:type object))
(->> (:shapes object)
(into [obj-bounds] xf-get-bounds)
(gsh/join-rects))
:else
obj-bounds)))
(mf/defc object-svg
{::mf/wrap [mf/memo]}
[{:keys [objects object-id zoom render-texts? embed?]
:or {zoom 1 embed? false}
:as props}]
(let [object (get objects object-id)
frame-id (if (= :frame (:type object))
(:id object)
(:frame-id object))
modifier (-> (gpt/point (:x object) (:y object))
(gpt/negate)
(gmt/translate-matrix))
mod-ids (cons frame-id (cph/get-children-ids objects frame-id))
updt-fn #(-> %1
(assoc-in [%2 :modifiers :displacement] modifier)
(update %2 gsh/transform-shape))
objects (reduce updt-fn objects mod-ids)
object (get objects object-id)
object (cond-> object
(:hide-fill-on-export object)
(assoc :fills []))
all-children (cph/get-children objects object-id)
{:keys [x y width height] :as bs} (calc-bounds object objects)
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))
vbox (str/join " " coords)
frame-wrapper
(mf/with-memo [objects]
(render/frame-wrapper-factory objects))
group-wrapper
(mf/with-memo [objects]
(render/group-wrapper-factory objects))
shape-wrapper
(mf/with-memo [objects]
(render/shape-wrapper-factory objects))
is-text? (fn [shape] (= :text (:type shape)))
text-shapes (sequence (comp (map second) (filter is-text?)) objects)
render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))]
(mf/with-effect [width height]
(dom/set-page-style!
{:size (dm/str (mth/ceil width) "px "
(mth/ceil height) "px")}))
[:& (mf/provider embed/context) {:value embed?}
[:svg {:id "screenshot"
:view-box vbox
:width width
:height height
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
;; Fix Chromium bug about color of html texts
;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5
:style {:-webkit-print-color-adjust :exact}}
[:& ff/fontfaces-style {:shapes all-children}]
(case (:type object)
:frame [:& frame-wrapper {:shape object :view-box vbox}]
:group [:> shape-container {:shape object}
[:& group-wrapper {:shape object}]]
[:& shape-wrapper {:shape object}])]
;; Auxiliary SVG for rendering text-shapes
(when render-texts?
(for [object text-shapes]
[:& (mf/provider muc/text-plain-colors-ctx) {:value true}
[:svg {:id (str "screenshot-text-" (:id object))
:view-box (str "0 0 " (:width object) " " (:height object))
:width (:width object)
:height (:height object)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"}
[:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]))
(defn- adapt-root-frame
[objects object-id]
(if (uuid/zero? object-id)
(let [object (get objects object-id)
shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (gsh/transform-shape object)
object (assoc object :fill-color "#f0f0f0")]
(assoc objects (:id object) object))
objects))
(mf/defc render-object
[{:keys [file-id page-id object-id render-texts? embed?] :as props}]
(let [objects (mf/use-state nil)]
(mf/with-effect [file-id page-id object-id]
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id}))
(rx/subs
(fn [[fonts {:keys [data]}]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))
(let [objs (get-in data [:pages-index page-id :objects])
objs (adapt-root-frame objs object-id)]
(reset! objects objs)))))
(constantly nil))
(when @objects
[:& object-svg {:objects @objects
:object-id object-id
:embed? embed?
:render-texts? render-texts?
:zoom 1}])))
(mf/defc render-sprite
[{:keys [file-id component-id] :as props}]
(let [file (mf/use-state nil)]
(mf/with-effect [file-id]
(->> (repo/query! :file {:id file-id})
(rx/subs
(fn [result]
(reset! file result))))
(constantly nil))
(when @file
[:*
[:& render/components-sprite-svg {:data (:data @file) :embed true}
(when (some? component-id)
[:use {:x 0 :y 0
:xlinkHref (str "#" component-id)}])]
(when-not (some? component-id)
[:ul
(for [[id data] (get-in @file [:data :components])]
(let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)]
[:li [:a {:href url} (:name data)]]))])])))

View file

@ -61,7 +61,6 @@
["/debug/icons-preview" :debug-icons-preview])
;; Used for export
["/render-object/:file-id/:page-id/:object-id" :render-object]
["/render-sprite/:file-id" :render-sprite]
["/dashboard/team/:team-id"

View file

@ -11,6 +11,7 @@
[app.util.code-gen :as cg]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn has-image? [shape]
@ -34,12 +35,10 @@
[:div.attributes-value (-> shape :metadata :height) "px"]
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
extension (dom/mtype->extension mtype)]
[:a.download-button {:target "_blank"
:download (if extension
(str name "." extension)
name)
:download (cond-> name extension (str/concat extension))
:href (cfg/resolve-file-media (-> shape :metadata))}
(tr "handoff.attributes.image.download")])])))

View file

@ -390,34 +390,3 @@
:bool [:> bool-container {:shape shape :frame frame :objects objects}]
:svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}])))))))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
[{:keys [objects frame zoom] :or {zoom 1} :as props}]
(let [modifier (-> (gpt/point (:x frame) (:y frame))
(gpt/negate)
(gmt/translate-matrix))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
frame-id (:id frame)
modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id))
objects (reduce update-fn objects modifier-ids)
frame (assoc-in frame [:modifiers :displacement] modifier)
width (* (:width frame) zoom)
height (* (:height frame) zoom)
vbox (str "0 0 " (:width frame 0)
" " (:height frame 0))
wrapper (mf/use-memo
(mf/deps objects)
#(frame-container-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"}
[:& wrapper {:shape frame
:view-box vbox}]]))

View file

@ -30,7 +30,7 @@
state (mf/deref refs/export)
in-progress? (:in-progress state)
filename (when (seqable? exports)
sname (when (seqable? exports)
(let [shapes (wsh/lookup-shapes @st/state ids)
sname (-> shapes first :name)
suffix (-> exports first :suffix)]
@ -56,13 +56,13 @@
;; separatelly by the export-modal.
(let [defaults {:page-id page-id
:file-id file-id
:name filename
:name sname
:object-id (first ids)}
exports (mapv #(merge % defaults) exports)]
(if (= 1 (count exports))
(let [export (first exports)]
(st/emit! (de/request-simple-export {:export export :filename (:name export)})))
(st/emit! (de/request-multiple-export {:exports exports :filename filename})))))))
(st/emit! (de/request-simple-export {:export export})))
(st/emit! (de/request-multiple-export {:exports exports})))))))
;; TODO: maybe move to specific events for avoid to have this logic here?
add-export

View file

@ -7,27 +7,38 @@
(ns app.render
"The main entry point for UI part needed by the exporter."
(:require
[app.common.logging :as log]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.spec :as us]
[app.common.uri :as u]
[app.config :as cf]
[app.main.ui.render :as render]
[app.main.data.fonts :as df]
[app.main.render :as render]
[app.main.repo :as repo]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.globals :as glob]
[beicon.core :as rx]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[garden.core :refer [css]]
[rumext.alpha :as mf]))
(log/initialize!)
(log/set-level! :root :warn)
(log/set-level! :app :info)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETUP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare reinit)
(l/initialize!)
(l/set-level! :root :warn)
(l/set-level! :app :info)
(declare ^:private render-object)
(declare ^:private render-single-object)
(declare ^:private render-components)
(declare ^:private render-objects)
(log/info :hint "Welcome to penpot (Export)"
:version (:full @cf/version)
:public-uri (str cf/public-uri))
(l/info :hint "Welcome to penpot (Export)"
:version (:full @cf/version)
:public-uri (str cf/public-uri))
(defn- parse-params
[loc]
@ -38,7 +49,8 @@
[]
(when-let [params (parse-params glob/location)]
(when-let [component (case (:route params)
"render-object" (render-object params)
"objects" (render-objects params)
"components" (render-components params)
nil)]
(mf/mount component (dom/get-element "app")))))
@ -55,23 +67,225 @@
[]
(reinit))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ---- SINGLE OBJECT
(defn use-resource
"A general purpose hook for retrieve or subscribe to remote changes
using the reactive-streams mechanism mechanism.
It receives a function to execute for retrieve the stream that will
be used for creating the subscription. The function should be
stable, so is the responsability of the user of this hook to
properly memoize it.
TODO: this should be placed in some generic hooks namespace but his
right now is pending of refactor and it will be done later."
[f]
(let [[state ^js update-state!] (mf/useState {:loaded? false})]
(mf/with-effect [f]
(update-state! (fn [prev] (assoc prev :refreshing? true)))
(let [on-value (fn [data]
(update-state! #(-> %
(assoc :refreshing? false)
(assoc :loaded? true)
(merge data))))
subs (rx/subscribe (f) on-value)]
#(rx/dispose! subs)))
state))
(mf/defc object-svg
[{:keys [page-id file-id object-id render-embed? render-texts?]}]
(let [fetch-state (mf/use-fn
(mf/deps file-id page-id object-id)
(fn []
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :page {:file-id file-id
:page-id page-id
:object-id object-id
:prune-thumbnails true}))
(rx/tap (fn [[fonts]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))))
(rx/map (comp :objects second))
(rx/map (fn [objects]
(let [objects (render/adapt-objects-for-shape objects object-id)
bounds (render/get-object-bounds objects object-id)
object (get objects object-id)]
{:objects objects
:object (merge object bounds)}))))))
{:keys [objects object]} (use-resource fetch-state)]
;; Set the globa CSS to assign the page size, needed for PDF
;; exportation process.
(mf/with-effect [object]
(when object
(dom/set-page-style!
{:size (str/concat
(mth/ceil (:width object)) "px "
(mth/ceil (:height object)) "px")})))
(when objects
[:& render/object-svg
{:objects objects
:object object
:render-embed? render-embed?
:render-texts? render-texts?
:zoom 1}])))
(mf/defc objects-svg
[{:keys [page-id file-id object-ids render-embed? render-texts?]}]
(let [fetch-state (mf/use-fn
(mf/deps file-id page-id)
(fn []
(->> (rx/zip
(repo/query! :font-variants {:file-id file-id})
(repo/query! :page {:file-id file-id
:page-id page-id
:prune-thumbnails true}))
(rx/tap (fn [[fonts]]
(when (seq fonts)
(st/emit! (df/fonts-fetched fonts)))))
(rx/map (comp :objects second)))))
objects (use-resource fetch-state)]
(when objects
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)
bounds (render/get-object-bounds objects object-id)
object (merge (get objects object-id) bounds)]
[:& render/object-svg
{:objects objects
:key (str object-id)
:object object
:render-embed? render-embed?
:render-texts? render-texts?
:zoom 1}])))))
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::object-id
(s/or :single ::us/uuid
:multiple (s/coll-of ::us/uuid)))
(s/def ::render-text ::us/boolean)
(s/def ::embed ::us/boolean)
(s/def ::render-object-params
(s/def ::render-objects
(s/keys :req-un [::file-id ::page-id ::object-id]
:opt-un [::render-text ::embed]))
:opt-un [::render-text ::render-embed]))
(defn- render-object
(defn- render-objects
[params]
(let [{:keys [page-id file-id object-id render-texts embed]} (us/conform ::render-object-params params)]
(let [{:keys [file-id
page-id
render-embed
render-texts]
:as params}
(us/conform ::render-objects params)
[type object-id] (:object-id params)]
(case type
:single
(mf/html
[:& object-svg
{:file-id file-id
:page-id page-id
:object-id object-id
:render-embed? render-embed
:render-texts? render-texts}])
:multiple
(mf/html
[:& objects-svg
{:file-id file-id
:page-id page-id
:object-ids (into #{} object-id)
:render-embed? render-embed
:render-texts? render-texts}]))))
;; ---- COMPONENTS SPRITE
(mf/defc components-sprite-svg
[{:keys [file-id embed] :as props}]
(let [fetch (mf/use-fn
(mf/deps file-id)
(fn [] (repo/query! :file {:id file-id})))
file (use-resource fetch)
state (mf/use-state nil)]
(when file
[:*
[:style
(css [[:body
{:margin 0
:overflow "hidden"
:width "100vw"
:height "100vh"}]
[:main
{:overflow "auto"
:display "flex"
:justify-content "center"
:align-items "center"
:height "calc(100vh - 200px)"}
[:svg {:width "50%"
:height "50%"}]]
[:.nav
{:display "flex"
:margin 0
:padding "10px"
:flex-direction "column"
:flex-wrap "wrap"
:height "200px"
:list-style "none"
:overflow-x "scroll"
:border-bottom "1px dotted #e6e6e6"}
[:a {:cursor :pointer
:text-overflow "ellipsis"
:white-space "nowrap"
:overflow "hidden"
:text-decoration "underline"}]
[:li {:display "flex"
:width "150px"
:padding "5px"
:border "0px solid black"}]]])]
[:ul.nav
(for [[id data] (get-in file [:data :components])]
(let [on-click (fn [event]
(dom/prevent-default event)
(swap! state assoc :component-id id))]
[:li {:key (str id)}
[:a {:on-click on-click} (:name data)]]))]
[:main
[:& render/components-sprite-svg
{:data (:data file)
:embed embed}
(when-let [component-id (:component-id @state)]
[:use {:x 0 :y 0 :xlinkHref (str "#" component-id)}])]]
])))
(s/def ::component-id ::us/uuid)
(s/def ::render-components
(s/keys :req-un [::file-id]
:opt-un [::embed ::component-id]))
(defn render-components
[params]
(let [{:keys [file-id component-id embed]} (us/conform ::render-components params)]
(mf/html
[:& render/render-object
[:& components-sprite-svg
{:file-id file-id
:page-id page-id
:object-id object-id
:embed? embed
:render-texts? render-texts}])))
:component-id component-id
:embed embed}])))

View file

@ -403,16 +403,16 @@
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype
"image/apng" "apng"
"image/avif" "avif"
"image/gif" "gif"
"image/jpeg" "jpg"
"image/png" "png"
"image/svg+xml" "svg"
"image/webp" "webp"
"application/zip" "zip"
"application/penpot" "penpot"
"application/pdf" "pdf"
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
nil))
(defn set-attribute! [^js node ^string attr value]
@ -464,11 +464,11 @@
(defn trigger-download-uri
[filename mtype uri]
(let [link (create-element "a")
(let [link (create-element "a")
extension (mtype->extension mtype)
filename (if extension
(str filename "." extension)
filename)]
filename (if (and extension (not (str/ends-with? filename extension)))
(str/concat filename "." extension)
filename)]
(obj/set! link "href" uri)
(obj/set! link "download" filename)
(obj/set! (.-style ^js link) "display" "none")

View file

@ -135,7 +135,7 @@
(rx/map #(assoc % :file-id file-id))
(rx/flat-map
(fn [media]
(let [file-path (str file-id "/media/" (:id media) "." (dom/mtype->extension (:mtype media)))]
(let [file-path (str/concat file-id "/media/" (:id media) (dom/mtype->extension (:mtype media)))]
(->> (http/send!
{:uri (cfg/resolve-file-media media)
:response-type :blob

View file

@ -48,7 +48,7 @@
:typographies (str file-id "/typographies.json")
:media-list (str file-id "/media.json")
:media (let [ext (dom/mtype->extension (:mtype media))]
(str file-id "/media/" id "." ext))
(str/concat file-id "/media/" id ext))
:components (str file-id "/components.svg"))
parse-svg? (and (not= type :media) (str/ends-with? path "svg"))

View file

@ -56,15 +56,16 @@
:uri (u/join (cfg/get-public-uri) path)
:credentials "include"
:query params}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
(defn- render-thumbnail
[{:keys [data file-id revn] :as params}]
(let [elem (if-let [frame (:thumbnail-frame data)]
(mf/element render/frame-svg #js {:objects (:objects data) :frame frame})
(mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}))]
[{:keys [page file-id revn] :as params}]
(let [elem (if-let [frame (:thumbnail-frame page)]
(mf/element render/frame-svg #js {:objects (:objects page) :frame frame})
(mf/element render/page-svg #js {:data page :thumbnails? true}))]
{:data (rds/renderToStaticMarkup elem)
:fonts @fonts/loaded
:file-id file-id
@ -81,6 +82,7 @@
:uri (u/join (cfg/get-public-uri) path)
:credentials "include"
:body (http/transit-data params)}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)