mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
345 lines
15 KiB
Clojure
345 lines
15 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.render-wasm.api.fonts
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.logging :as log]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.main.fonts :as fonts]
|
|
[app.main.store :as st]
|
|
[app.render-wasm.helpers :as h]
|
|
[app.render-wasm.wasm :as wasm]
|
|
[app.util.http :as http]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[goog.object :as gobj]
|
|
[lambdaisland.uri :as u]
|
|
[okulary.core :as l]))
|
|
|
|
(def ^:private fonts
|
|
(l/derived :fonts st/state))
|
|
|
|
(def ^:private default-font-size 14)
|
|
(def ^:private default-line-height 1.2)
|
|
(def ^:private default-letter-spacing 0.0)
|
|
|
|
(defn- google-font-id->uuid
|
|
[font-id]
|
|
(let [font (fonts/get-font-data font-id)]
|
|
(:uuid font)))
|
|
|
|
(defn- custom-font-id->uuid
|
|
[font-id]
|
|
(uuid/uuid (subs font-id (inc (str/index-of font-id "-")))))
|
|
|
|
(defn- font-backend
|
|
[font-id]
|
|
(cond
|
|
(str/starts-with? font-id "gfont-")
|
|
:google
|
|
(str/starts-with? font-id "custom-")
|
|
:custom
|
|
:else
|
|
:builtin))
|
|
|
|
(defn- font-db-data
|
|
[font-id font-variant-id]
|
|
(let [font (fonts/get-font-data font-id)
|
|
variant (fonts/get-variant font font-variant-id)]
|
|
variant))
|
|
|
|
(defn- font-id->uuid [font-id]
|
|
(case (font-backend font-id)
|
|
:google
|
|
(google-font-id->uuid font-id)
|
|
:custom
|
|
(custom-font-id->uuid font-id)
|
|
:builtin
|
|
uuid/zero))
|
|
|
|
(defn ^:private font-id->asset-id [font-id font-variant-id]
|
|
(case (font-backend font-id)
|
|
:google
|
|
font-id
|
|
:custom
|
|
(let [font-uuid (custom-font-id->uuid font-id)
|
|
matching-font (d/seek (fn [[_ font]]
|
|
(let [variant-id (or (:font-variant-id font) (dm/str (:font-style font) "-" (:font-weight font)))]
|
|
(and (= (:font-id font) font-uuid)
|
|
(or (nil? font-variant-id)
|
|
(= variant-id font-variant-id)))))
|
|
(seq @fonts))]
|
|
(when matching-font
|
|
(:ttf-file-id (second matching-font))))
|
|
:builtin
|
|
(let [variant (font-db-data font-id font-variant-id)]
|
|
(:ttf-url variant))))
|
|
|
|
(defn update-text-layout
|
|
[id]
|
|
(when wasm/context-initialized?
|
|
(let [shape-id-buffer (uuid/get-u32 id)]
|
|
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
|
(aget shape-id-buffer 0)
|
|
(aget shape-id-buffer 1)
|
|
(aget shape-id-buffer 2)
|
|
(aget shape-id-buffer 3)))))
|
|
|
|
;; IMPORTANT: Only TTF fonts can be stored.
|
|
(defn- store-font-buffer
|
|
[shape-id font-data font-array-buffer emoji? fallback?]
|
|
(let [font-id-buffer (:family-id-buffer font-data)
|
|
shape-id-buffer (uuid/get-u32 shape-id)
|
|
size (.-byteLength font-array-buffer)
|
|
ptr (h/call wasm/internal-module "_alloc_bytes" size)
|
|
heap (gobj/get ^js wasm/internal-module "HEAPU8")
|
|
mem (js/Uint8Array. (.-buffer heap) ptr size)]
|
|
(.set mem (js/Uint8Array. font-array-buffer))
|
|
(h/call wasm/internal-module "_store_font"
|
|
(aget shape-id-buffer 0)
|
|
(aget shape-id-buffer 1)
|
|
(aget shape-id-buffer 2)
|
|
(aget shape-id-buffer 3)
|
|
(aget font-id-buffer 0)
|
|
(aget font-id-buffer 1)
|
|
(aget font-id-buffer 2)
|
|
(aget font-id-buffer 3)
|
|
(:weight font-data)
|
|
(:style font-data)
|
|
emoji?
|
|
fallback?)
|
|
|
|
(update-text-layout shape-id)
|
|
|
|
true))
|
|
|
|
(defn- fetch-font
|
|
[shape-id font-data font-url emoji? fallback?]
|
|
{:key font-url
|
|
:callback #(->> (http/send! {:method :get
|
|
:uri font-url
|
|
:response-type :buffer})
|
|
(rx/map (fn [{:keys [body]}]
|
|
(store-font-buffer shape-id font-data body emoji? fallback?)))
|
|
(rx/catch (fn [cause]
|
|
(log/error :hint "Could not fetch font"
|
|
:font-url font-url
|
|
:cause cause)
|
|
(rx/empty))))})
|
|
|
|
(defn- google-font-ttf-url
|
|
[font-id font-variant-id]
|
|
(let [variant (font-db-data font-id font-variant-id)]
|
|
(if-let [ttf-url (:ttf-url variant)]
|
|
(str/replace ttf-url "https://fonts.gstatic.com/s/" (u/join cf/public-uri "/internal/gfonts/font/"))
|
|
nil)))
|
|
|
|
(defn- font-id->ttf-url
|
|
[font-id asset-id font-variant-id]
|
|
(case (font-backend font-id)
|
|
:google
|
|
(google-font-ttf-url font-id font-variant-id)
|
|
:custom
|
|
(dm/str (u/join cf/public-uri "assets/by-id/" asset-id))
|
|
:builtin
|
|
(dm/str (u/join cf/public-uri "fonts/" asset-id))))
|
|
|
|
(defn- store-font-id
|
|
[shape-id font-data asset-id emoji? fallback?]
|
|
(when asset-id
|
|
(let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data))
|
|
id-buffer (uuid/get-u32 (:wasm-id font-data))
|
|
font-data (assoc font-data :family-id-buffer id-buffer)
|
|
font-stored? (not= 0 (h/call wasm/internal-module "_is_font_uploaded"
|
|
(aget id-buffer 0)
|
|
(aget id-buffer 1)
|
|
(aget id-buffer 2)
|
|
(aget id-buffer 3)
|
|
(:weight font-data)
|
|
(:style font-data)
|
|
emoji?))]
|
|
(when-not font-stored?
|
|
(fetch-font shape-id font-data uri emoji? fallback?)))))
|
|
|
|
(defn serialize-font-style
|
|
[font-style]
|
|
(case font-style
|
|
"normal" 0
|
|
"regular" 0
|
|
"italic" 1
|
|
0))
|
|
|
|
(defn normalize-font-id
|
|
[font-id]
|
|
(try
|
|
(if ^boolean (str/starts-with? font-id "gfont-")
|
|
(google-font-id->uuid font-id)
|
|
(let [no-prefix (subs font-id (inc (str/index-of font-id "-")))]
|
|
(if (or (nil? no-prefix) (not (string? no-prefix)) (str/blank? no-prefix))
|
|
uuid/zero
|
|
(uuid/parse no-prefix))))
|
|
(catch :default _e
|
|
uuid/zero)))
|
|
|
|
(defn serialize-font-size
|
|
[font-size]
|
|
(cond
|
|
(number? font-size)
|
|
font-size
|
|
|
|
(string? font-size)
|
|
(or (d/parse-double font-size) default-font-size)))
|
|
|
|
(defn serialize-font-weight
|
|
[font-weight]
|
|
(if (number? font-weight)
|
|
font-weight
|
|
(let [font-weight-str (str font-weight)]
|
|
(cond
|
|
(re-matches #"\d+" font-weight-str)
|
|
(js/Number font-weight-str)
|
|
|
|
(str/includes? font-weight-str "bold")
|
|
700
|
|
(str/includes? font-weight-str "black")
|
|
900
|
|
(str/includes? font-weight-str "extrabold")
|
|
800
|
|
(str/includes? font-weight-str "extralight")
|
|
200
|
|
(str/includes? font-weight-str "light")
|
|
300
|
|
(str/includes? font-weight-str "medium")
|
|
500
|
|
(str/includes? font-weight-str "semibold")
|
|
600
|
|
(str/includes? font-weight-str "thin")
|
|
100
|
|
:else
|
|
400))))
|
|
|
|
(defn serialize-line-height
|
|
([line-height]
|
|
(serialize-line-height line-height default-line-height))
|
|
([line-height default-value]
|
|
(cond
|
|
(number? line-height)
|
|
line-height
|
|
|
|
(string? line-height)
|
|
(or (d/parse-double line-height) default-value))))
|
|
|
|
(defn serialize-letter-spacing
|
|
[letter-spacing]
|
|
(cond
|
|
(number? letter-spacing)
|
|
letter-spacing
|
|
|
|
(string? letter-spacing)
|
|
(or (d/parse-double letter-spacing) default-letter-spacing)))
|
|
|
|
(defn store-font
|
|
[shape-id font]
|
|
(let [font-id (get font :font-id)
|
|
font-variant-id (get font :font-variant-id)
|
|
emoji? (get font :is-emoji false)
|
|
fallback? (get font :is-fallback false)
|
|
wasm-id (font-id->uuid font-id)
|
|
raw-weight (or (:weight (font-db-data font-id font-variant-id)) 400)
|
|
weight (serialize-font-weight raw-weight)
|
|
style (serialize-font-style (cond
|
|
(str/includes? font-variant-id "italic") "italic"
|
|
(str/includes? raw-weight "italic") "italic"
|
|
:else "normal"))
|
|
asset-id (font-id->asset-id font-id font-variant-id)
|
|
font-data {:wasm-id wasm-id
|
|
:font-id font-id
|
|
:font-variant-id font-variant-id
|
|
:style style
|
|
:weight weight}]
|
|
|
|
(store-font-id shape-id font-data asset-id emoji? fallback?)))
|
|
|
|
;; FIXME: This is a temporary function to load the fallback fonts for the editor.
|
|
;; Once we render the editor content within wasm, we can remove this function.
|
|
(defn load-fallback-fonts-for-editor!
|
|
[fonts]
|
|
(doseq [font fonts]
|
|
(fonts/ensure-loaded! (:font-id font) (:font-variant-id font))))
|
|
|
|
(defn store-fonts
|
|
[shape-id fonts]
|
|
(keep (fn [font] (store-font shape-id font)) fonts))
|
|
|
|
(defn add-emoji-font
|
|
[fonts]
|
|
(conj fonts {:font-id "gfont-noto-color-emoji"
|
|
:font-variant-id "regular"
|
|
:style 0
|
|
:weight 400
|
|
:is-emoji true
|
|
:is-fallback true}))
|
|
|
|
(def noto-fonts
|
|
{:japanese {:font-id "gfont-noto-sans-jp" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:chinese {:font-id "gfont-noto-sans-sc" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:korean {:font-id "gfont-noto-sans-kr" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:arabic {:font-id "gfont-noto-sans-arabic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:cyrillic {:font-id "gfont-noto-sans-cyrillic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:greek {:font-id "gfont-noto-sans-greek" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:hebrew {:font-id "gfont-noto-sans-hebrew" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:thai {:font-id "gfont-noto-sans-thai" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:devanagari {:font-id "gfont-noto-sans-devanagari" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:tamil {:font-id "gfont-noto-sans-tamil" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:latin-ext {:font-id "gfont-noto-sans" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:vietnamese {:font-id "gfont-noto-sans" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:armenian {:font-id "gfont-noto-sans-armenian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:bengali {:font-id "gfont-noto-sans-bengali" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:cherokee {:font-id "gfont-noto-sans-cherokee" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:ethiopic {:font-id "gfont-noto-sans-ethiopic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:georgian {:font-id "gfont-noto-sans-georgian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:gujarati {:font-id "gfont-noto-sans-gujarati" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:gurmukhi {:font-id "gfont-noto-sans-gurmukhi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:khmer {:font-id "gfont-noto-sans-khmer" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:lao {:font-id "gfont-noto-sans-lao" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:malayalam {:font-id "gfont-noto-sans-malayalam" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:myanmar {:font-id "gfont-noto-sans-myanmar" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:sinhala {:font-id "gfont-noto-sans-sinhala" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:telugu {:font-id "gfont-noto-sans-telugu" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:tibetan {:font-id "gfont-noto-sans-tibetan" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:javanese {:font-id "gfont-noto-sans-javanese" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:kannada {:font-id "gfont-noto-sans-kannada" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:oriya {:font-id "gfont-noto-sans-oriya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:mongolian {:font-id "gfont-noto-sans-mongolian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:syriac {:font-id "gfont-noto-sans-syriac" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:tifinagh {:font-id "gfont-noto-sans-tifinagh" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:coptic {:font-id "gfont-noto-sans-coptic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:ol-chiki {:font-id "gfont-noto-sans-ol-chiki" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:vai {:font-id "gfont-noto-sans-vai" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:shavian {:font-id "gfont-noto-sans-shavian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:osmanya {:font-id "gfont-noto-sans-osmanya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:runic {:font-id "gfont-noto-sans-runic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:old-italic {:font-id "gfont-noto-sans-old-italic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:brahmi {:font-id "gfont-noto-sans-brahmi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:modi {:font-id "gfont-noto-sans-modi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:sora-sompeng {:font-id "gfont-noto-sans-sora-sompeng" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:bamum {:font-id "gfont-noto-sans-bamum" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:meroitic {:font-id "gfont-noto-sans-meroitic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:symbols {:font-id "gfont-noto-sans-symbols" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:symbols-2 {:font-id "gfont-noto-sans-symbols-2" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
|
|
:music {:font-id "gfont-noto-music" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}})
|
|
|
|
(defn add-noto-fonts [fonts languages]
|
|
(reduce (fn [acc lang]
|
|
(if-let [font (get noto-fonts lang)]
|
|
(conj acc font)
|
|
acc))
|
|
fonts
|
|
languages))
|