;; 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))