;; 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.render "Rendering utilities and components for penpot SVG. NOTE: This namespace is used from worker and from many parts of the workspace; we need to be careful when adding new requires because this can cause to import too many deps on worker bundle." (:require ["react-dom/server" :as rds] [app.common.colors :as clr] [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.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] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] [app.main.ui.shapes.text.fontfaces :as ff] [app.util.http :as http] [app.util.object :as obj] [app.util.strings :as ust] [app.util.timers :as ts] [beicon.core :as rx] [clojure.set :as set] [cuerdas.core :as str] [rumext.alpha :as mf])) (def ^:const viewbox-decimal-precision 3) (def ^:private default-color clr/canvas) (mf/defc background [{:keys [vbox color]}] [:rect {:x (:x vbox) :y (:y vbox) :width (:width vbox) :height (:height vbox) :fill color}]) (defn- calculate-dimensions [objects] (let [shapes (cph/get-immediate-children objects) rect (gsh/selection-rect shapes)] (-> rect (update :x mth/finite 0) (update :y mth/finite 0) (update :width mth/finite 100000) (update :height mth/finite 100000)))) (declare shape-wrapper-factory) (defn frame-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape)) shape (gsh/transform-shape shape)] [:> shape-container {:shape shape} [:& frame-shape {:shape shape :childs childs}]])))) (defn group-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] [:& group-shape {:shape shape :is-child-selected? true :childs childs}])))) (defn bool-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) bool-shape (bool/bool-shape shape-wrapper)] (mf/fnc bool-wrapper [{:keys [shape] :as props}] (let [childs (mf/with-memo [(:id shape) objects] (->> (cph/get-children-ids objects (:id shape)) (select-keys objects)))] [:& bool-shape {:shape shape :childs childs}])))) (defn svg-raw-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] (if (and (map? (:content shape)) (or (= :svg (get-in shape [:content :tag])) (contains? shape :svg-attrs))) [:> shape-container {:shape shape} [:& svg-raw-shape {:shape shape :childs childs}]] [:& svg-raw-shape {:shape shape :childs childs}]))))) (defn shape-wrapper-factory [objects] (mf/fnc shape-wrapper [{:keys [frame shape] :as props}] (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects)) bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (gsh/transform-shape shape) opts #js {:shape shape} svg-raw? (= :svg-raw (:type shape))] (if-not svg-raw? [:> shape-container {:shape shape} (case (:type shape) :text [:> text/text-shape opts] :rect [:> rect/rect-shape opts] :path [:> path/path-shape opts] :image [:> image/image-shape opts] :circle [:> circle/circle-shape opts] :frame [:> frame-wrapper {:shape shape}] :group [:> group-wrapper {:shape shape :frame frame}] :bool [:> bool-wrapper {:shape shape :frame frame}] nil)] ;; Don't wrap svg elements inside a otherwise some can break [:> svg-raw-wrapper {:shape shape :frame frame}])))))) (defn format-viewbox "Format a viewbox given a rectangle" [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}] (str/join " " (->> [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 thumbnails? render-embed? include-metadata?] :as props :or {render-embed? false include-metadata? false}}] (let [objects (:objects data) shapes (cph/get-immediate-children objects) dim (calculate-dimensions objects) vbox (format-viewbox dim) bgcolor (dm/get-in data [:options :background] default-color) frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects)) shape-wrapper (mf/use-memo (mf/deps objects) #(shape-wrapper-factory objects))] [:& (mf/provider embed/context) {:value render-embed?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} [:svg {:view-box vbox :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 "100%" :height "100%" :background bgcolor} :fill "none"} (when include-metadata? [:& export/export-page {:options (:options data)}]) (let [shapes (->> shapes (remove cph/frame-shape?) (mapcat #(cph/get-children-with-self objects (:id %)))) fonts (ff/shapes->fonts shapes)] [:& ff/fontfaces-style {:fonts fonts}]) (for [item shapes] (let [frame? (= (:type item) :frame)] (cond (and frame? thumbnails? (some? (:thumbnail item))) [:> shape-container {:shape item} [:& frame/frame-thumbnail {:shape item}]] frame? [:& frame-wrapper {:shape item :key (:id item)}] :else [:& 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}] (let [frame-id (:id frame) include-metadata? (mf/use-ctx export/include-metadata-ctx) modifier (mf/with-memo [(:x frame) (:y frame)] (-> (gpt/point (:x frame) (:y frame)) (gpt/negate) (gmt/translate-matrix))) objects (mf/with-memo [frame-id objects modifier] (let [update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)] (->> (cph/get-children-ids objects frame-id) (into [frame-id]) (reduce update-fn objects)))) frame (mf/with-memo [modifier] (assoc-in frame [:modifiers :displacement] modifier)) wrapper (mf/with-memo [objects] (frame-wrapper-factory objects)) width (* (:width frame) zoom) height (* (:height frame) zoom) vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)})] [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) :height (ust/format-precision height viewbox-decimal-precision) :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") :fill "none"} (if (or (not show-thumbnails?) (nil? (:thumbnail frame))) [:& wrapper {:shape frame :view-box vbox}] ;; Render the frame thumbnail (let [frame (gsh/transform-shape frame)] [:> 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}] (let [group-id (:id group) include-metadata? (mf/use-ctx export/include-metadata-ctx) modifier (mf/use-memo (mf/deps (:x group) (:y group)) (fn [] (-> (gpt/point (:x group) (:y group)) (gpt/negate) (gmt/translate-matrix)))) objects (mf/use-memo (mf/deps modifier objects group-id) (fn [] (let [modifier-ids (cons group-id (cph/get-children-ids objects group-id)) update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) modifiers (reduce update-fn {} modifier-ids)] (gsh/merge-modifiers objects modifiers)))) group (get objects group-id) width (* (:width group) zoom) height (* (:height group) zoom) vbox (format-viewbox {:width (:width group 0) :height (:height group 0)}) group-wrapper (mf/use-memo (mf/deps objects) (fn [] (group-wrapper-factory objects)))] [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) :height (ust/format-precision height viewbox-decimal-precision) :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") :fill "none"} [:> shape-container {:shape group} [:& group-wrapper {:shape group :view-box vbox}]]])) (mf/defc object-svg {::mf/wrap [mf/memo]} [{:keys [objects object-id render-texts? render-embed?] :or {render-embed? false} :as props}] (let [object (get objects object-id) object (cond-> object (:hide-fill-on-export object) (assoc :fills [])) {:keys [x y width height]} (get-object-bounds objects object-id) 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-" object-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} :fill "none"} (let [fonts (ff/frame->fonts object objects)] [:& ff/fontfaces-style {:fonts fonts}]) (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" :fill "none"} [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SPRITES (DEBUG) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc component-symbol [{: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)}) group-wrapper (mf/use-memo (mf/deps objects) (fn [] (group-wrapper-factory objects)))] [:> "symbol" #js {:id (str id) :viewBox vbox} [:title name] [:> 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") render-embed? (obj/get props "render-embed?") include-metadata? (obj/get props "include-metadata?")] [:& (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 {:display (when-not (some? children) "none")} :fill "none"} [:defs (for [[id data] (:components data)] [:& component-symbol {:id id :key (dm/str id) :data data}])] children]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; RENDER FOR DOWNLOAD (wrongly called exportation) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- get-image-data [shape] (cond (= :image (:type shape)) [(:metadata shape)] (some? (:fill-image shape)) [(:fill-image shape)] :else [])) (defn- populate-images-cache [objects] (let [images (->> objects (vals) (mapcat get-image-data))] (->> (rx/from images) (rx/map #(cfg/resolve-file-media %)) (rx/flat-map http/fetch-data-uri)))) (defn populate-fonts-cache [objects] (let [texts (->> objects (vals) (filterv #(= (:type %) :text)) (mapv :content)) ] (->> (rx/from texts) (rx/map fonts/get-content-fonts) (rx/reduce set/union #{}) (rx/flat-map identity) (rx/flat-map fonts/fetch-font-css) (rx/flat-map fonts/extract-fontface-urls) (rx/flat-map http/fetch-data-uri)))) (defn render-page [data] (rx/concat (->> (rx/merge (populate-images-cache (:objects data)) (populate-fonts-cache (:objects data))) (rx/ignore)) (->> (rx/of data) (rx/map (fn [data] (let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})] (rds/renderToStaticMarkup elem))))))) (defn render-components [data] (let [;; Join all components objects into a single map objects (->> (:components data) (vals) (map :objects) (reduce conj))] (rx/concat (->> (rx/merge (populate-images-cache objects) (populate-fonts-cache objects)) (rx/ignore)) (->> (rx/of data) (rx/map (fn [data] (let [elem (mf/element components-sprite-svg #js {:data data :render-embed? true :include-metadata? true})] (rds/renderToStaticMarkup elem))))))))