mirror of
https://github.com/penpot/penpot.git
synced 2025-05-15 12:26:38 +02:00
♻️ Refactor embed resouces
This commit is contained in:
parent
42072f2584
commit
6a68e9c118
15 changed files with 312 additions and 260 deletions
|
@ -26,7 +26,8 @@
|
||||||
[app.main.ui.shapes.text :as text]
|
[app.main.ui.shapes.text :as text]
|
||||||
[app.main.ui.shapes.group :as group]
|
[app.main.ui.shapes.group :as group]
|
||||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]))
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
|
[app.main.ui.shapes.embed :as embed]))
|
||||||
|
|
||||||
(def ^:private default-color "#E8E9EA") ;; $color-canvas
|
(def ^:private default-color "#E8E9EA") ;; $color-canvas
|
||||||
|
|
||||||
|
@ -43,7 +44,8 @@
|
||||||
[{:keys [objects] :as data} vport]
|
[{:keys [objects] :as data} vport]
|
||||||
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true})
|
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true})
|
||||||
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
|
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
|
||||||
rect (->> (gsh/selection-rect shapes)
|
rect (cond->> (gsh/selection-rect shapes)
|
||||||
|
(some? vport)
|
||||||
(gal/adjust-to-viewport vport))]
|
(gal/adjust-to-viewport vport))]
|
||||||
(-> rect
|
(-> rect
|
||||||
(update :x to-finite 0)
|
(update :x to-finite 0)
|
||||||
|
@ -121,13 +123,14 @@
|
||||||
|
|
||||||
(mf/defc page-svg
|
(mf/defc page-svg
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [data width height thumbnails?] :as props}]
|
[{:keys [data width height thumbnails? embed?] :as props}]
|
||||||
(let [objects (:objects data)
|
(let [objects (:objects data)
|
||||||
root (get objects uuid/zero)
|
root (get objects uuid/zero)
|
||||||
shapes (->> (:shapes root)
|
shapes (->> (:shapes root)
|
||||||
(map #(get objects %)))
|
(map #(get objects %)))
|
||||||
|
|
||||||
vport {:width width :height height}
|
vport (when (and (some? width) (some? height))
|
||||||
|
{:width width :height height})
|
||||||
dim (calculate-dimensions data vport)
|
dim (calculate-dimensions data vport)
|
||||||
vbox (get-viewbox dim)
|
vbox (get-viewbox dim)
|
||||||
background-color (get-in data [:options :background] default-color)
|
background-color (get-in data [:options :background] default-color)
|
||||||
|
@ -140,10 +143,12 @@
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps objects)
|
(mf/deps objects)
|
||||||
#(shape-wrapper-factory objects))]
|
#(shape-wrapper-factory objects))]
|
||||||
|
[:& (mf/provider embed/context) {:value embed?}
|
||||||
[:svg {:view-box vbox
|
[:svg {:view-box vbox
|
||||||
:version "1.1"
|
:version "1.1"
|
||||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||||
:xmlns "http://www.w3.org/2000/svg"}
|
:xmlns "http://www.w3.org/2000/svg"
|
||||||
|
:xmlns:penpot "https://penpot.app/xmlns"}
|
||||||
[:& background {:vbox dim :color background-color}]
|
[:& background {:vbox dim :color background-color}]
|
||||||
(for [item shapes]
|
(for [item shapes]
|
||||||
(let [frame? (= (:type item) :frame)]
|
(let [frame? (= (:type item) :frame)]
|
||||||
|
@ -162,7 +167,7 @@
|
||||||
:key (:id item)}]
|
:key (:id item)}]
|
||||||
:else
|
:else
|
||||||
[:& shape-wrapper {:shape item
|
[:& shape-wrapper {:shape item
|
||||||
:key (:id item)}])))]))
|
:key (:id item)}])))]]))
|
||||||
|
|
||||||
(mf/defc frame-svg
|
(mf/defc frame-svg
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
|
|
|
@ -8,17 +8,19 @@
|
||||||
"Fonts management and loading logic."
|
"Fonts management and loading logic."
|
||||||
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
||||||
(:require
|
(:require
|
||||||
[app.config :as cf]
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.text :as txt]
|
||||||
|
[app.config :as cf]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[app.util.logging :as log]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[app.util.logging :as log]
|
|
||||||
[lambdaisland.uri :as u]
|
|
||||||
[goog.events :as gev]
|
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
[goog.events :as gev]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
@ -216,3 +218,55 @@
|
||||||
(or
|
(or
|
||||||
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
|
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
|
||||||
(first variants)))
|
(first variants)))
|
||||||
|
|
||||||
|
;; 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(/fonts/%(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}]
|
||||||
|
(let [current-font
|
||||||
|
(if (some? font-id)
|
||||||
|
#{(select-keys content [:font-id :font-variant-id])}
|
||||||
|
#{(select-keys txt/default-text-attrs [:font-id :font-variant-id])})
|
||||||
|
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)]
|
||||||
|
(if (= :google backend)
|
||||||
|
(-> (generate-gfonts-url
|
||||||
|
{:family family
|
||||||
|
:variants [{:id font-variant-id}]})
|
||||||
|
(http/fetch-text))
|
||||||
|
|
||||||
|
(let [{:keys [weight style suffix] :as variant}
|
||||||
|
(d/seek #(= (:id %) font-variant-id) variants)
|
||||||
|
font-data {:family family
|
||||||
|
:style style
|
||||||
|
:suffix (or suffix font-variant-id)
|
||||||
|
:weight weight}]
|
||||||
|
(rx/of (str/fmt font-face-template font-data))))))
|
||||||
|
|
||||||
|
(defn extract-fontface-urls
|
||||||
|
"Parses the CSS and retrieves the font urls"
|
||||||
|
[^string css]
|
||||||
|
(->> (re-seq #"url\(([^)]+)\)" css)
|
||||||
|
(mapv second)))
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
(:require
|
(:require
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def embed-ctx (mf/create-context false))
|
|
||||||
(def render-ctx (mf/create-context nil))
|
(def render-ctx (mf/create-context nil))
|
||||||
(def def-ctx (mf/create-context false))
|
(def def-ctx (mf/create-context false))
|
||||||
|
|
||||||
|
|
|
@ -225,3 +225,28 @@
|
||||||
(fn []
|
(fn []
|
||||||
(mf/set-ref-val! ref value)))
|
(mf/set-ref-val! ref value)))
|
||||||
(mf/ref-val ref)))
|
(mf/ref-val ref)))
|
||||||
|
|
||||||
|
(defn use-equal-memo
|
||||||
|
[val]
|
||||||
|
(let [ref (mf/use-ref nil)]
|
||||||
|
(when-not (= (mf/ref-val ref) val)
|
||||||
|
(mf/set-ref-val! ref val))
|
||||||
|
(mf/ref-val ref)))
|
||||||
|
|
||||||
|
(defn- ssr?
|
||||||
|
"Checks if the current environment is run under a SSR context"
|
||||||
|
[]
|
||||||
|
(try
|
||||||
|
(not js/window)
|
||||||
|
(catch :default e
|
||||||
|
;; When exception accessing window we're in ssr
|
||||||
|
true)))
|
||||||
|
|
||||||
|
(defn use-effect-ssr
|
||||||
|
"Use effect that handles SSR"
|
||||||
|
[deps effect-fn]
|
||||||
|
|
||||||
|
(if (ssr?)
|
||||||
|
(let [ret (effect-fn)]
|
||||||
|
(when (fn? ret) (ret)))
|
||||||
|
(mf/use-effect deps effect-fn)))
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.exports :as exports]
|
[app.main.exports :as exports]
|
||||||
[app.main.repo :as repo]
|
[app.main.repo :as repo]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.shapes.embed :as embed]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
#(exports/shape-wrapper-factory objects))
|
#(exports/shape-wrapper-factory objects))
|
||||||
]
|
]
|
||||||
|
|
||||||
[:& (mf/provider muc/embed-ctx) {:value true}
|
[:& (mf/provider embed/context) {:value true}
|
||||||
[:svg {:id "screenshot"
|
[:svg {:id "screenshot"
|
||||||
:view-box vbox
|
:view-box vbox
|
||||||
:width width
|
:width width
|
||||||
|
|
39
frontend/src/app/main/ui/shapes/embed.cljs
Normal file
39
frontend/src/app/main/ui/shapes/embed.cljs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
;; 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.shapes.embed
|
||||||
|
(:require
|
||||||
|
[app.main.ui.hooks :as hooks]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(def context (mf/create-context false))
|
||||||
|
|
||||||
|
(defn use-data-uris [urls]
|
||||||
|
(let [embed? (mf/use-ctx context)
|
||||||
|
urls (hooks/use-equal-memo urls)
|
||||||
|
uri-data (mf/use-ref {})
|
||||||
|
state (mf/use-state 0)]
|
||||||
|
|
||||||
|
(hooks/use-effect-ssr
|
||||||
|
(mf/deps embed? urls)
|
||||||
|
(fn []
|
||||||
|
(let [sub (when embed?
|
||||||
|
(->> (rx/from urls)
|
||||||
|
(rx/merge-map http/fetch-data-uri)
|
||||||
|
(rx/reduce conj {})
|
||||||
|
(rx/subs (fn [data]
|
||||||
|
(when-not (= data (mf/ref-val uri-data))
|
||||||
|
(mf/set-ref-val! uri-data data)
|
||||||
|
(reset! state inc))))))]
|
||||||
|
#(when sub
|
||||||
|
(rx/dispose! sub)))))
|
||||||
|
|
||||||
|
;; Use ref so if the urls are cached will return inmediately instead of the
|
||||||
|
;; next render
|
||||||
|
(when embed?
|
||||||
|
(mf/ref-val uri-data))))
|
|
@ -8,10 +8,9 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
|
[app.main.ui.shapes.embed :as embed]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]))
|
||||||
[app.common.geom.point :as gpt]
|
|
||||||
[app.main.ui.shapes.image :as image]))
|
|
||||||
|
|
||||||
(mf/defc fill-image-pattern
|
(mf/defc fill-image-pattern
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
|
@ -22,8 +21,8 @@
|
||||||
(when (contains? shape :fill-image)
|
(when (contains? shape :fill-image)
|
||||||
(let [{:keys [x y width height]} (:selrect shape)
|
(let [{:keys [x y width height]} (:selrect shape)
|
||||||
fill-image-id (str "fill-image-" render-id)
|
fill-image-id (str "fill-image-" render-id)
|
||||||
media (:fill-image shape)
|
uri (cfg/resolve-file-media (:fill-image shape))
|
||||||
uri (image/use-image-uri media)
|
embed (embed/use-data-uris [uri])
|
||||||
transform (gsh/transform-matrix shape)]
|
transform (gsh/transform-matrix shape)]
|
||||||
|
|
||||||
[:pattern {:id fill-image-id
|
[:pattern {:id fill-image-id
|
||||||
|
@ -33,6 +32,6 @@
|
||||||
:height height
|
:height height
|
||||||
:width width
|
:width width
|
||||||
:patternTransform transform}
|
:patternTransform transform}
|
||||||
[:image {:xlinkHref uri
|
[:image {:xlinkHref (get embed uri uri)
|
||||||
:width width
|
:width width
|
||||||
:height height}]]))))
|
:height height}]]))))
|
||||||
|
|
|
@ -7,13 +7,16 @@
|
||||||
(ns app.main.ui.shapes.frame
|
(ns app.main.ui.shapes.frame
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as geom]
|
|
||||||
[app.main.ui.shapes.attrs :as attrs]
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
|
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def frame-default-props {:fill-color "#ffffff"})
|
(def frame-default-props {:fill-color "#ffffff"})
|
||||||
|
|
||||||
|
(defn is-text? [{type :type}]
|
||||||
|
(= :text type))
|
||||||
|
|
||||||
(defn frame-shape
|
(defn frame-shape
|
||||||
[shape-wrapper]
|
[shape-wrapper]
|
||||||
(mf/fnc frame-shape
|
(mf/fnc frame-shape
|
||||||
|
@ -23,6 +26,8 @@
|
||||||
shape (unchecked-get props "shape")
|
shape (unchecked-get props "shape")
|
||||||
{:keys [id width height]} shape
|
{:keys [id width height]} shape
|
||||||
|
|
||||||
|
text-childs (->> childs (filterv is-text?))
|
||||||
|
|
||||||
props (-> (merge frame-default-props shape)
|
props (-> (merge frame-default-props shape)
|
||||||
(attrs/extract-style-attrs)
|
(attrs/extract-style-attrs)
|
||||||
(obj/merge!
|
(obj/merge!
|
||||||
|
@ -32,6 +37,7 @@
|
||||||
:height height
|
:height height
|
||||||
:className "frame-background"}))]
|
:className "frame-background"}))]
|
||||||
[:*
|
[:*
|
||||||
|
[:& ff/fontfaces-style {:shapes text-childs}]
|
||||||
[:> :rect props]
|
[:> :rect props]
|
||||||
(for [[i item] (d/enumerate childs)]
|
(for [[i item] (d/enumerate childs)]
|
||||||
[:& shape-wrapper {:frame shape
|
[:& shape-wrapper {:frame shape
|
||||||
|
|
|
@ -7,35 +7,13 @@
|
||||||
(ns app.main.ui.shapes.image
|
(ns app.main.ui.shapes.image
|
||||||
(:require
|
(:require
|
||||||
[app.common.geom.shapes :as geom]
|
[app.common.geom.shapes :as geom]
|
||||||
[app.config :as cfg]
|
|
||||||
[app.main.ui.context :as muc]
|
|
||||||
[app.main.ui.shapes.attrs :as attrs]
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
|
[app.main.ui.shapes.embed :as se]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.http :as http]
|
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.webapi :as wapi]
|
[rumext.alpha :as mf]
|
||||||
[beicon.core :as rx]
|
[app.config :as cfg]
|
||||||
[rumext.alpha :as mf]))
|
[app.main.ui.shapes.embed :as embed]))
|
||||||
|
|
||||||
(defn use-image-uri
|
|
||||||
[media]
|
|
||||||
(let [uri (mf/use-memo (mf/deps (:id media))
|
|
||||||
#(cfg/resolve-file-media media))
|
|
||||||
embed-resources? (mf/use-ctx muc/embed-ctx)
|
|
||||||
data-uri (mf/use-state (when (not embed-resources?) uri))]
|
|
||||||
|
|
||||||
(mf/use-effect
|
|
||||||
(mf/deps uri)
|
|
||||||
(fn []
|
|
||||||
(if embed-resources?
|
|
||||||
(->> (http/send! {:method :get
|
|
||||||
:uri uri
|
|
||||||
:response-type :blob})
|
|
||||||
(rx/map :body)
|
|
||||||
(rx/mapcat wapi/read-file-as-data-url)
|
|
||||||
(rx/subs #(reset! data-uri %))))))
|
|
||||||
|
|
||||||
(or @data-uri uri)))
|
|
||||||
|
|
||||||
(mf/defc image-shape
|
(mf/defc image-shape
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
|
@ -43,7 +21,8 @@
|
||||||
|
|
||||||
(let [shape (unchecked-get props "shape")
|
(let [shape (unchecked-get props "shape")
|
||||||
{:keys [id x y width height rotation metadata]} shape
|
{:keys [id x y width height rotation metadata]} shape
|
||||||
uri (use-image-uri metadata)]
|
uri (cfg/resolve-file-media metadata)
|
||||||
|
embed (embed/use-data-uris [uri])]
|
||||||
|
|
||||||
(let [transform (geom/transform-matrix shape)
|
(let [transform (geom/transform-matrix shape)
|
||||||
props (-> (attrs/extract-style-attrs shape)
|
props (-> (attrs/extract-style-attrs shape)
|
||||||
|
@ -60,5 +39,5 @@
|
||||||
|
|
||||||
[:> "image" (obj/merge!
|
[:> "image" (obj/merge!
|
||||||
props
|
props
|
||||||
#js {:xlinkHref uri
|
#js {:xlinkHref (get embed uri uri)
|
||||||
:onDragStart on-drag-start})])))
|
:onDragStart on-drag-start})])))
|
||||||
|
|
|
@ -1,144 +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.shapes.text.embed
|
|
||||||
(:refer-clojure :exclude [memoize])
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.text :as txt]
|
|
||||||
[app.main.fonts :as fonts]
|
|
||||||
[app.util.http :as http]
|
|
||||||
[app.util.time :as dt]
|
|
||||||
[app.util.webapi :as wapi]
|
|
||||||
[app.util.object :as obj]
|
|
||||||
[clojure.set :as set]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[beicon.core :as rx]
|
|
||||||
[rumext.alpha :as mf]))
|
|
||||||
|
|
||||||
|
|
||||||
(defonce cache (atom {}))
|
|
||||||
|
|
||||||
(defn with-cache
|
|
||||||
[{:keys [key max-age]} observable]
|
|
||||||
(let [entry (get @cache key)
|
|
||||||
age (when entry
|
|
||||||
(dt/diff (dt/now)
|
|
||||||
(:created-at entry)))]
|
|
||||||
(if (and (some? entry)
|
|
||||||
(< age max-age))
|
|
||||||
(rx/of (:data entry))
|
|
||||||
(->> observable
|
|
||||||
(rx/tap (fn [data]
|
|
||||||
(let [entry {:created-at (dt/now) :data data}]
|
|
||||||
(swap! cache assoc key entry))))))))
|
|
||||||
|
|
||||||
(def font-face-template "
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: '%(family)s';
|
|
||||||
font-style: %(style)s;
|
|
||||||
font-weight: %(weight)s;
|
|
||||||
font-display: block;
|
|
||||||
src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff');
|
|
||||||
}
|
|
||||||
")
|
|
||||||
|
|
||||||
;; -- Embed fonts into styles
|
|
||||||
|
|
||||||
(defn get-node-fonts
|
|
||||||
[node]
|
|
||||||
(let [current-font (if (not (nil? (:font-id node)))
|
|
||||||
#{(select-keys node [:font-id :font-variant-id])}
|
|
||||||
#{(select-keys txt/default-text-attrs [:font-id :font-variant-id])})
|
|
||||||
children-font (map get-node-fonts (:children node))]
|
|
||||||
(reduce set/union (conj children-font current-font))))
|
|
||||||
|
|
||||||
(defn get-font-css
|
|
||||||
"Given a font and the variant-id, retrieves the style CSS for it."
|
|
||||||
[{:keys [id backend family variants] :as font} font-variant-id]
|
|
||||||
(if (= :google backend)
|
|
||||||
(let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})]
|
|
||||||
(->> (http/send! {:method :get
|
|
||||||
:mode :cors
|
|
||||||
:omit-default-headers true
|
|
||||||
:uri uri
|
|
||||||
:response-type :text})
|
|
||||||
(rx/map :body)
|
|
||||||
(http/as-promise)))
|
|
||||||
(let [{:keys [name weight style suffix] :as variant} (d/seek #(= (:id %) font-variant-id) variants)
|
|
||||||
result (str/fmt font-face-template {:family family
|
|
||||||
:style style
|
|
||||||
:suffix (or suffix font-variant-id)
|
|
||||||
:weight weight})]
|
|
||||||
(p/resolved result))))
|
|
||||||
|
|
||||||
(defn- to-promise
|
|
||||||
[observable]
|
|
||||||
(p/create (fn [resolve reject]
|
|
||||||
(->> (rx/take 1 observable)
|
|
||||||
(rx/subs resolve reject)))))
|
|
||||||
|
|
||||||
(defn fetch-font-data
|
|
||||||
"Parses the CSS and retrieves the font data as DataURI."
|
|
||||||
[^string css]
|
|
||||||
(let [uris (->> (re-seq #"url\(([^)]+)\)" css)
|
|
||||||
(mapv second))]
|
|
||||||
(with-cache {:key uris :max-age (dt/duration {:hours 4})}
|
|
||||||
(->> (rx/from (seq uris))
|
|
||||||
(rx/mapcat (fn [uri]
|
|
||||||
(->> (http/send! {:method :get :uri uri :response-type :blob :omit-default-headers true})
|
|
||||||
(rx/map :body)
|
|
||||||
(rx/mapcat wapi/read-file-as-data-url)
|
|
||||||
(rx/map #(vector uri %)))))
|
|
||||||
(rx/reduce conj [])))))
|
|
||||||
|
|
||||||
(defn get-font-data
|
|
||||||
"Parses the CSS and retrieves the font data as DataURI."
|
|
||||||
[^string css]
|
|
||||||
(->> (fetch-font-data css)
|
|
||||||
(http/as-promise)))
|
|
||||||
|
|
||||||
(defn embed-font
|
|
||||||
"Given a font-id and font-variant-id, retrieves the CSS for it and
|
|
||||||
convert all external urls to embedded data URI's."
|
|
||||||
[{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}]
|
|
||||||
(let [{:keys [backend family] :as font} (get @fonts/fontsdb font-id)]
|
|
||||||
(p/let [css (get-font-css font font-variant-id)
|
|
||||||
url-to-data (get-font-data css)
|
|
||||||
replace-text (fn [text [url data]] (str/replace text url data))]
|
|
||||||
(reduce replace-text css url-to-data))))
|
|
||||||
|
|
||||||
;; NOTE: we can't move this to generic hooks namespace because that
|
|
||||||
;; namespace imports some code incompatible with webworkers and this
|
|
||||||
;; font embbeding should be able run on browser and webworker
|
|
||||||
;; contexts.
|
|
||||||
(defn- memoize
|
|
||||||
[val]
|
|
||||||
(let [ref (mf/use-ref #js {})]
|
|
||||||
(when-not (= (mf/ref-val ref) val)
|
|
||||||
(mf/set-ref-val! ref val))
|
|
||||||
(mf/ref-val ref)))
|
|
||||||
|
|
||||||
(mf/defc embed-fontfaces-style
|
|
||||||
{::mf/wrap-props false
|
|
||||||
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
|
|
||||||
[props]
|
|
||||||
(let [shapes (obj/get props "shapes")
|
|
||||||
node {:children (->> shapes (map :content))}
|
|
||||||
fonts (-> node get-node-fonts memoize)
|
|
||||||
style (mf/use-state nil)]
|
|
||||||
|
|
||||||
(mf/use-effect
|
|
||||||
(mf/deps fonts)
|
|
||||||
(fn []
|
|
||||||
(-> (p/all (map embed-font fonts))
|
|
||||||
(p/then (fn [result]
|
|
||||||
(reset! style (str/join "\n" result)))))))
|
|
||||||
|
|
||||||
(when (some? @style)
|
|
||||||
[:style @style])))
|
|
79
frontend/src/app/main/ui/shapes/text/fontfaces.cljs
Normal file
79
frontend/src/app/main/ui/shapes/text/fontfaces.cljs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
;; 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.shapes.text.fontfaces
|
||||||
|
(:require
|
||||||
|
[app.main.fonts :as fonts]
|
||||||
|
[app.main.ui.hooks :as hooks]
|
||||||
|
[app.main.ui.shapes.embed :as embed]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[clojure.set :as set]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(defn replace-embeds
|
||||||
|
"Replace into the font-faces of a CSS the URL's that are present in `embed-data` by its
|
||||||
|
data-uri"
|
||||||
|
[css urls embed-data]
|
||||||
|
(letfn [(replace-url [css url]
|
||||||
|
(str/replace css url (get embed-data url url)))]
|
||||||
|
(->> urls
|
||||||
|
(reduce replace-url css))))
|
||||||
|
|
||||||
|
(defn use-fonts-css
|
||||||
|
"Hook that retrieves the CSS of the fonts passed as parameter"
|
||||||
|
[fonts]
|
||||||
|
(let [fonts-css-ref (mf/use-ref "")
|
||||||
|
redraw (mf/use-state 0)]
|
||||||
|
|
||||||
|
(hooks/use-effect-ssr
|
||||||
|
(mf/deps fonts)
|
||||||
|
(fn []
|
||||||
|
(let [sub
|
||||||
|
(->> (rx/from fonts)
|
||||||
|
(rx/merge-map fonts/fetch-font-css)
|
||||||
|
(rx/reduce conj [])
|
||||||
|
(rx/subs
|
||||||
|
(fn [result]
|
||||||
|
(let [css (str/join "\n" result)]
|
||||||
|
(when-not (= (mf/ref-val fonts-css-ref) css)
|
||||||
|
(mf/set-ref-val! fonts-css-ref css)
|
||||||
|
(reset! redraw inc))))))]
|
||||||
|
#(rx/dispose! sub))))
|
||||||
|
|
||||||
|
(mf/ref-val fonts-css-ref)))
|
||||||
|
|
||||||
|
(mf/defc fontfaces-style
|
||||||
|
{::mf/wrap-props false
|
||||||
|
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
|
||||||
|
[props]
|
||||||
|
(let [shapes (obj/get props "shapes")
|
||||||
|
|
||||||
|
content (->> shapes (mapv :content))
|
||||||
|
|
||||||
|
;; Retrieve the fonts ids used by the text shapes
|
||||||
|
fonts (->> content
|
||||||
|
(mapv fonts/get-content-fonts)
|
||||||
|
(reduce set/union #{})
|
||||||
|
(hooks/use-equal-memo))
|
||||||
|
|
||||||
|
;; Fetch its CSS fontfaces
|
||||||
|
fonts-css (use-fonts-css fonts)
|
||||||
|
|
||||||
|
;; Extract from the CSS the URL's to embed
|
||||||
|
fonts-urls (mf/use-memo
|
||||||
|
(mf/deps fonts-css)
|
||||||
|
#(fonts/extract-fontface-urls fonts-css))
|
||||||
|
|
||||||
|
;; Calculate the data-uris for these fonts
|
||||||
|
fonts-embed (embed/use-data-uris fonts-urls)
|
||||||
|
|
||||||
|
;; Creates a style tag by replacing the urls with the data uri
|
||||||
|
style (replace-embeds fonts-css fonts-urls fonts-embed)]
|
||||||
|
|
||||||
|
(when (and (some? style) (not (empty? style)))
|
||||||
|
[:style style])))
|
|
@ -6,45 +6,15 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.shapes.frame
|
(ns app.main.ui.workspace.shapes.frame
|
||||||
(:require
|
(:require
|
||||||
[app.common.geom.point :as gpt]
|
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.main.data.workspace :as dw]
|
|
||||||
[app.main.data.workspace.changes :as dch]
|
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
|
||||||
[app.main.ui.context :as muc]
|
|
||||||
[app.main.ui.shapes.frame :as frame]
|
[app.main.ui.shapes.frame :as frame]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
[app.main.ui.shapes.text.embed :as ste]
|
|
||||||
[app.util.dom :as dom]
|
|
||||||
[app.util.keyboard :as kbd]
|
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.timers :as ts]
|
[app.util.timers :as ts]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[okulary.core :as l]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def obs-config
|
|
||||||
#js {:attributes true
|
|
||||||
:childList true
|
|
||||||
:subtree true
|
|
||||||
:characterData true})
|
|
||||||
|
|
||||||
(defn make-is-moving-ref
|
|
||||||
[id]
|
|
||||||
(let [check-moving (fn [local]
|
|
||||||
(and (= :move (:transform local))
|
|
||||||
(contains? (:selected local) id)))]
|
|
||||||
(l/derived check-moving refs/workspace-local)))
|
|
||||||
|
|
||||||
(defn check-props
|
|
||||||
([props] (check-props props =))
|
|
||||||
([props eqfn?]
|
|
||||||
(fn [np op]
|
|
||||||
(every? #(eqfn? (unchecked-get np %)
|
|
||||||
(unchecked-get op %))
|
|
||||||
props))))
|
|
||||||
|
|
||||||
(defn check-frame-props
|
(defn check-frame-props
|
||||||
"Checks for changes in the props of a frame"
|
"Checks for changes in the props of a frame"
|
||||||
[new-props old-props]
|
[new-props old-props]
|
||||||
|
@ -107,14 +77,9 @@
|
||||||
thumbnail? (unchecked-get props "thumbnail?")
|
thumbnail? (unchecked-get props "thumbnail?")
|
||||||
|
|
||||||
edition (mf/deref refs/selected-edition)
|
edition (mf/deref refs/selected-edition)
|
||||||
embed-fonts? (mf/use-ctx muc/embed-ctx)
|
|
||||||
|
|
||||||
shape (gsh/transform-shape shape)
|
shape (gsh/transform-shape shape)
|
||||||
children (mapv #(get objects %) (:shapes shape))
|
children (mapv #(get objects %) (:shapes shape))
|
||||||
text-childs (->> objects
|
|
||||||
vals
|
|
||||||
(filterv #(and (= :text (:type %))
|
|
||||||
(= (:id shape) (:frame-id %)))))
|
|
||||||
|
|
||||||
ds-modifier (get-in shape [:modifiers :displacement])
|
ds-modifier (get-in shape [:modifiers :displacement])
|
||||||
|
|
||||||
|
@ -131,12 +96,7 @@
|
||||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||||
|
|
||||||
(when-not show-thumbnail?
|
(when-not show-thumbnail?
|
||||||
[:> shape-container {:shape shape
|
[:> shape-container {:shape shape :ref on-dom}
|
||||||
:ref on-dom}
|
|
||||||
|
|
||||||
(when embed-fonts?
|
|
||||||
[:& ste/embed-fontfaces-style {:shapes text-childs}])
|
|
||||||
|
|
||||||
[:& frame-shape {:shape shape
|
[:& frame-shape {:shape shape
|
||||||
:childs children}]])
|
:childs children}]])
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
(ns app.main.ui.workspace.viewport
|
(ns app.main.ui.workspace.viewport
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.pages :as cp]
|
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.pages :as cp]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.ui.context :as ctx]
|
[app.main.ui.context :as ctx]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.measurements :as msr]
|
[app.main.ui.measurements :as msr]
|
||||||
|
[app.main.ui.shapes.embed :as embed]
|
||||||
[app.main.ui.workspace.shapes :as shapes]
|
[app.main.ui.workspace.shapes :as shapes]
|
||||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
[app.main.ui.workspace.shapes.text.editor :as editor]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
|
@ -187,7 +188,7 @@
|
||||||
:style {:background-color (get options :background "#E8E9EA")
|
:style {:background-color (get options :background "#E8E9EA")
|
||||||
:pointer-events "none"}}
|
:pointer-events "none"}}
|
||||||
|
|
||||||
[:& (mf/provider muc/embed-ctx) {:value true}
|
[:& (mf/provider embed/context) {:value true}
|
||||||
;; Render root shape
|
;; Render root shape
|
||||||
[:& shapes/root-shape {:key page-id
|
[:& shapes/root-shape {:key page-id
|
||||||
:objects objects
|
:objects objects
|
||||||
|
|
26
frontend/src/app/util/cache.cljs
Normal file
26
frontend/src/app/util/cache.cljs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
;; 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.util.cache
|
||||||
|
(:require
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[beicon.core :as rx]))
|
||||||
|
|
||||||
|
(defonce cache (atom {}))
|
||||||
|
|
||||||
|
(defn with-cache
|
||||||
|
[{:keys [key max-age]} observable]
|
||||||
|
(let [entry (get @cache key)
|
||||||
|
age (when entry
|
||||||
|
(dt/diff (dt/now)
|
||||||
|
(:created-at entry)))]
|
||||||
|
(if (and (some? entry) (< age max-age))
|
||||||
|
(rx/of (:data entry))
|
||||||
|
(->> observable
|
||||||
|
(rx/tap
|
||||||
|
(fn [data]
|
||||||
|
(let [entry {:created-at (dt/now) :data data}]
|
||||||
|
(swap! cache assoc key entry))))))))
|
|
@ -10,9 +10,12 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
|
[app.util.cache :as c]
|
||||||
[app.util.globals :as globals]
|
[app.util.globals :as globals]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
[app.util.transit :as t]
|
||||||
|
[app.util.webapi :as wapi]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[promesa.core :as p]))
|
[promesa.core :as p]))
|
||||||
|
@ -152,6 +155,27 @@
|
||||||
|
|
||||||
(defn as-promise
|
(defn as-promise
|
||||||
[observable]
|
[observable]
|
||||||
(p/create (fn [resolve reject]
|
(p/create
|
||||||
|
(fn [resolve reject]
|
||||||
(->> (rx/take 1 observable)
|
(->> (rx/take 1 observable)
|
||||||
(rx/subs resolve reject)))))
|
(rx/subs resolve reject)))))
|
||||||
|
|
||||||
|
(defn fetch-data-uri [uri]
|
||||||
|
(c/with-cache {:key uri :max-age (dt/duration {:hours 4})}
|
||||||
|
(->> (send! {:method :get
|
||||||
|
:uri uri
|
||||||
|
:response-type :blob
|
||||||
|
:omit-default-headers true})
|
||||||
|
(rx/map :body)
|
||||||
|
(rx/mapcat wapi/read-file-as-data-url)
|
||||||
|
(rx/map #(hash-map uri %)))))
|
||||||
|
|
||||||
|
(defn fetch-text [url]
|
||||||
|
(c/with-cache {:key url :max-age (dt/duration {:hours 4})}
|
||||||
|
(->> (send!
|
||||||
|
{:method :get
|
||||||
|
:mode :cors
|
||||||
|
:omit-default-headers true
|
||||||
|
:uri url
|
||||||
|
:response-type :text})
|
||||||
|
(rx/map :body))))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue