mirror of
https://github.com/penpot/penpot.git
synced 2026-05-07 09:08:33 +00:00
309 lines
9.4 KiB
Clojure
309 lines
9.4 KiB
Clojure
;; 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.fonts
|
|
"Fonts management and loading logic."
|
|
(:require-macros [app.main.fonts :refer [preload-gfonts]])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.logging :as log]
|
|
[app.common.text :as txt]
|
|
[app.config :as cf]
|
|
[app.util.dom :as dom]
|
|
[app.util.http :as http]
|
|
[app.util.object :as obj]
|
|
[beicon.core :as rx]
|
|
[clojure.set :as set]
|
|
[cuerdas.core :as str]
|
|
[goog.events :as gev]
|
|
[lambdaisland.uri :as u]
|
|
[okulary.core :as l]
|
|
[promesa.core :as p]))
|
|
|
|
(log/set-level! :info)
|
|
|
|
(def google-fonts
|
|
(preload-gfonts "fonts/gfonts.2022.07.11.json"))
|
|
|
|
(def local-fonts
|
|
[{:id "sourcesanspro"
|
|
:name "Source Sans Pro"
|
|
:family "sourcesanspro"
|
|
:variants
|
|
[{:id "200" :name "200" :weight "200" :style "normal" :suffix "extralight"}
|
|
{:id "200italic" :name "200 (italic)" :weight "200" :style "italic" :suffix "extralightitalic"}
|
|
{:id "300" :name "300" :weight "300" :style "normal" :suffix "light"}
|
|
{:id "300italic" :name "300 (italic)" :weight "300" :style "italic" :suffix "lightitalic"}
|
|
{:id "regular" :name "regular" :weight "400" :style "normal"}
|
|
{:id "italic" :name "italic" :weight "400" :style "italic"}
|
|
{:id "bold" :name "bold" :weight "bold" :style "normal"}
|
|
{:id "bolditalic" :name "bold (italic)" :weight "bold" :style "italic"}
|
|
{:id "black" :name "black" :weight "900" :style "normal"}
|
|
{:id "blackitalic" :name "black (italic)" :weight "900" :style "italic"}]}])
|
|
|
|
(defonce fontsdb (l/atom {}))
|
|
(defonce fonts (l/atom []))
|
|
|
|
(add-watch fontsdb "main"
|
|
(fn [_ _ _ db]
|
|
(->> (vals db)
|
|
(sort-by :name)
|
|
(map-indexed #(assoc %2 :index %1))
|
|
(vec)
|
|
(reset! fonts))))
|
|
|
|
(defn register!
|
|
[backend fonts]
|
|
(swap! fontsdb
|
|
(fn [db]
|
|
(let [db (reduce-kv #(cond-> %1 (= backend (:backend %3)) (dissoc %2)) db db)
|
|
fonts (map #(assoc % :backend backend) fonts)]
|
|
(merge db (d/index-by :id fonts))))))
|
|
|
|
(register! :builtin local-fonts)
|
|
(register! :google google-fonts)
|
|
|
|
(defn get-font-data [id]
|
|
(get @fontsdb id))
|
|
|
|
(defn resolve-variants
|
|
[id]
|
|
(get-in @fontsdb [id :variants]))
|
|
|
|
(defn resolve-fonts
|
|
[backend]
|
|
(get @fonts backend))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; FONTS LOADING
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defonce loaded (l/atom #{}))
|
|
(defonce loading (l/atom {}))
|
|
|
|
(defn- create-link-element
|
|
[uri]
|
|
(let [node (.createElement js/document "link")]
|
|
(unchecked-set node "href" uri)
|
|
(unchecked-set node "rel" "stylesheet")
|
|
(unchecked-set node "type" "text/css")
|
|
node))
|
|
|
|
(defn- create-style-element
|
|
[css]
|
|
(let [node (.createElement js/document "style")]
|
|
(unchecked-set node "innerHTML" css)
|
|
node))
|
|
|
|
(defn- load-font-css!
|
|
"Creates a link element and attaches it to the dom for correctly
|
|
load external css resource."
|
|
[url on-loaded]
|
|
(let [node (create-link-element url)
|
|
head (.-head ^js js/document)]
|
|
(gev/listenOnce node "load" (fn [_]
|
|
(when (fn? on-loaded)
|
|
(on-loaded))))
|
|
(dom/append-child! head node)))
|
|
|
|
(defn- add-font-css!
|
|
"Creates a style element and attaches it to the dom."
|
|
[css]
|
|
(let [head (.-head ^js js/document)]
|
|
(->> (create-style-element css)
|
|
(dom/append-child! head))))
|
|
|
|
;; --- LOADER: BUILTIN
|
|
|
|
(defmulti ^:private load-font :backend)
|
|
|
|
(defmethod load-font :default
|
|
[{:keys [backend] :as font}]
|
|
(log/warn :msg "no implementation found for" :backend backend))
|
|
|
|
(defmethod load-font :builtin
|
|
[{:keys [id ::on-loaded] :as font}]
|
|
(log/debug :hint "load-font" :font-id id :backend "builtin")
|
|
(when (fn? on-loaded)
|
|
(on-loaded id)))
|
|
|
|
;; --- LOADER: GOOGLE
|
|
|
|
(defn generate-gfonts-url
|
|
[{:keys [family variants]}]
|
|
(let [base (str "https://fonts.googleapis.com/css?family=" family)
|
|
variants (str/join "," (map :id variants))]
|
|
(str base ":" variants "&display=block")))
|
|
|
|
(defmethod load-font :google
|
|
[{:keys [id ::on-loaded] :as font}]
|
|
(when (exists? js/window)
|
|
(log/info :hint "load-font" :font-id id :backend "google")
|
|
(let [url (generate-gfonts-url font)]
|
|
(load-font-css! url (partial on-loaded id))
|
|
nil)))
|
|
|
|
;; --- LOADER: CUSTOM
|
|
|
|
(def font-css-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');
|
|
}")
|
|
|
|
(defn- asset-id->uri
|
|
[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
|
|
{: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))}))
|
|
|
|
(defn- generate-custom-font-css
|
|
[{:keys [family variants] :as font}]
|
|
(->> variants
|
|
(map #(generate-custom-font-variant-css family %))
|
|
(str/join "\n")))
|
|
|
|
(defmethod load-font :custom
|
|
[{:keys [id ::on-loaded] :as font}]
|
|
(when (exists? js/window)
|
|
(log/info :hint "load-font" :font-id id :backend "custom")
|
|
(let [css (generate-custom-font-css font)]
|
|
(add-font-css! css)
|
|
(when (fn? on-loaded)
|
|
(on-loaded)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; LOAD API
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn ensure-loaded!
|
|
([id]
|
|
(p/create (fn [resolve]
|
|
(ensure-loaded! id resolve))))
|
|
([id on-loaded]
|
|
(log/debug :action "try-ensure-loaded!" :font-id 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))
|
|
|
|
(when-let [font (get @fontsdb id)]
|
|
(cond
|
|
;; Font already loaded, we just continue
|
|
(contains? @loaded id)
|
|
(p/do
|
|
(on-loaded id)
|
|
id)
|
|
|
|
;; Font is currently downloading. We attach the caller to the promise
|
|
(contains? @loading id)
|
|
(-> (get @loading id)
|
|
(p/then #(do (on-loaded id) id)))
|
|
|
|
;; First caller, we create the promise and then wait
|
|
:else
|
|
(let [on-load (fn [resolve]
|
|
(swap! loaded conj id)
|
|
(swap! loading dissoc id)
|
|
(on-loaded id)
|
|
(resolve id))
|
|
|
|
load-p (p/create
|
|
(fn [resolve _]
|
|
(-> font
|
|
(assoc ::on-loaded (partial on-load resolve))
|
|
(load-font))))]
|
|
|
|
(swap! loading assoc 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)))
|
|
|
|
;; 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}]
|
|
(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)]
|
|
(cond
|
|
(= :google backend)
|
|
(let [variant (d/seek #(= (:id %) font-variant-id) variants)]
|
|
(-> (generate-gfonts-url
|
|
{:family family
|
|
:variants [variant]})
|
|
(http/fetch-text)))
|
|
|
|
(= :custom backend)
|
|
(let [variant (d/seek #(= (:id %) font-variant-id) variants)
|
|
result (generate-custom-font-variant-css family variant)]
|
|
(p/resolved 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))))))
|
|
|
|
(defn extract-fontface-urls
|
|
"Parses the CSS and retrieves the font urls"
|
|
[^string css]
|
|
(->> (re-seq #"url\(([^)]+)\)" css)
|
|
(mapv second)))
|