Merge pull request #6609 from penpot/elenatorro-11213-fix-language-font-fallback

🎉 Implement font fallback to support multiple languages
This commit is contained in:
Alejandro Alonso 2025-06-03 17:19:39 +02:00 committed by GitHub
commit 5a7d9e3f18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 180 additions and 39 deletions

View file

@ -614,7 +614,8 @@
(let [paragraph-set (first (dm/get-prop content :children))
paragraphs (dm/get-prop paragraph-set :children)
fonts (fonts/get-content-fonts content)
emoji? (atom false)]
emoji? (atom false)
languages (atom #{})]
(loop [index 0]
(when (< index (count paragraphs))
(let [paragraph (nth paragraphs index)
@ -623,12 +624,14 @@
(let [text (apply str (map :text leaves))]
(when (and (not @emoji?) (t/contains-emoji? text))
(reset! emoji? true))
(swap! languages into (t/get-languages text))
(t/write-shape-text leaves paragraph text))
(recur (inc index))))))
(let [fonts (if @emoji?
(f/add-emoji-font fonts)
fonts)]
(f/store-fonts fonts))))
(let [updated-fonts
(-> fonts
(cond-> emoji? (f/add-emoji-font))
(f/add-noto-fonts @languages))]
(f/store-fonts updated-fonts))))
(defn set-shape-text
[content]

View file

@ -15,7 +15,6 @@
[app.render-wasm.helpers :as h]
[app.render-wasm.wasm :as wasm]
[app.util.http :as http]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.object :as gobj]
@ -77,7 +76,7 @@
;; IMPORTANT: It should be noted that only TTF fonts can be stored.
(defn- store-font-buffer
[font-data font-array-buffer emoji?]
[font-data font-array-buffer emoji? fallback?]
(let [id-buffer (:family-id-buffer font-data)
size (.-byteLength font-array-buffer)
ptr (h/call wasm/internal-module "_alloc_bytes" size)
@ -91,17 +90,17 @@
(aget id-buffer 3)
(:weight font-data)
(:style font-data)
emoji?)
emoji?
fallback?)
true))
(defn- store-font-url
[font-data font-url emoji?]
[font-data font-url emoji? fallback?]
(->> (http/send! {:method :get
:uri font-url
:response-type :blob})
(rx/map :body)
(rx/mapcat wapi/read-file-as-array-buffer)
(rx/map (fn [array-buffer] (store-font-buffer font-data array-buffer emoji?)))))
:response-type :buffer})
(rx/map (fn [{:keys [body]}]
(store-font-buffer font-data body emoji? fallback?)))))
(defn- google-font-ttf-url
[font-id font-variant-id]
@ -121,7 +120,7 @@
(dm/str (u/join cf/public-uri "fonts/" asset-id))))
(defn- store-font-id
[font-data asset-id emoji?]
[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))
@ -133,7 +132,8 @@
(aget id-buffer 3)
(:weight font-data)
(:style font-data)))]
(when-not font-stored? (store-font-url font-data uri emoji?)))))
(when-not font-stored?
(store-font-url font-data uri emoji? fallback?)))))
(defn serialize-font-style
[font-style]
@ -165,13 +165,13 @@
(defn store-font
[font]
(let [font-id (dm/get-prop font :font-id)
font-variant-id (dm/get-prop font :font-variant-id)
(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"
:else "normal"))
@ -180,9 +180,8 @@
:font-id font-id
:font-variant-id font-variant-id
:style style
:weight weight}
emoji? (dm/get-prop font :emoji?)]
(store-font-id font-data asset-id emoji?)))
:weight weight}]
(store-font-id font-data asset-id emoji? fallback?)))
(defn store-fonts
[fonts]
@ -195,4 +194,59 @@
:font-variant-id "regular"
:style 0
:weight 400
:emoji? true}))
:is-emoji 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 "noto-sans-javanese" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:kannada {:font-id "noto-sans-kannada" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:oriya {:font-id "noto-sans-oriya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:mongolian {:font-id "noto-sans-mongolian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:syriac {:font-id "noto-sans-syriac" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:tifinagh {:font-id "noto-sans-tifinagh" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:coptic {:font-id "noto-sans-coptic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:ol-chiki {:font-id "noto-sans-ol-chiki" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:vai {:font-id "noto-sans-vai" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:shavian {:font-id "noto-sans-shavian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:osmanya {:font-id "noto-sans-osmanya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:runic {:font-id "noto-sans-runic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:old-italic {:font-id "noto-sans-old-italic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:brahmi {:font-id "noto-sans-brahmi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:modi {:font-id "noto-sans-modi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:sora-sompeng {:font-id "noto-sans-sora-sompeng" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:bamum {:font-id "noto-sans-bamum" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}
:meroitic {:font-id "noto-sans-meroitic" :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))

View file

@ -148,7 +148,62 @@
(h/call wasm/internal-module "_set_shape_text_content"))
(def emoji-pattern #"[\uD83C-\uDBFF][\uDC00-\uDFFF]")
(def ^:private emoji-pattern #"[\uD83C-\uDBFF][\uDC00-\uDFFF]")
(defn contains-emoji? [s]
(boolean (re-find emoji-pattern s)))
(def ^:private unicode-ranges
{:japanese #"[\u3040-\u30FF\u31F0-\u31FF\uFF66-\uFF9F]"
:chinese #"[\u4E00-\u9FFF\u3400-\u4DBF]"
:korean #"[\uAC00-\uD7AF]"
:arabic #"[\u0600-\u06FF\u0750-\u077F\u0870-\u089F\u08A0-\u08FF]"
:cyrillic #"[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F]"
:greek #"[\u0370-\u03FF\u1F00-\u1FFF]"
:hebrew #"[\u0590-\u05FF\uFB1D-\uFB4F]"
:thai #"[\u0E00-\u0E7F]"
:devanagari #"[\u0900-\u097F\uA8E0-\uA8FF]"
:tamil #"[\u0B80-\u0BFF]"
:latin-ext #"[\u0100-\u017F\u0180-\u024F]"
:vietnamese #"[\u1EA0-\u1EF9]"
:armenian #"[\u0530-\u058F\uFB13-\uFB17]"
:bengali #"[\u0980-\u09FF]"
:cherokee #"[\u13A0-\u13FF]"
:ethiopic #"[\u1200-\u137F]"
:georgian #"[\u10A0-\u10FF]"
:gujarati #"[\u0A80-\u0AFF]"
:gurmukhi #"[\u0A00-\u0A7F]"
:khmer #"[\u1780-\u17FF\u19E0-\u19FF]"
:lao #"[\u0E80-\u0EFF]"
:malayalam #"[\u0D00-\u0D7F]"
:myanmar #"[\u1000-\u109F\uAA60-\uAA7F]"
:sinhala #"[\u0D80-\u0DFF]"
:telugu #"[\u0C00-\u0C7F]"
:tibetan #"[\u0F00-\u0FFF]"
:javanese #"[\uA980-\uA9DF]"
:kannada #"[\u0C80-\u0CFF]"
:oriya #"[\u0B00-\u0B7F]"
:mongolian #"[\u1800-\u18AF]"
:syriac #"[\u0700-\u074F]"
:tifinagh #"[\u2D30-\u2D7F]"
:coptic #"[\u2C80-\u2CFF]"
:ol-chiki #"[\u1C50-\u1C7F]"
:vai #"[\uA500-\uA63F]"
:shavian #"[\u10450-\u1047F]"
:osmanya #"[\u10480-\u104AF]"
:runic #"[\u16A0-\u16FF]"
:old-italic #"[\u10300-\u1032F]"
:brahmi #"[\u11000-\u1107F]"
:modi #"[\u11600-\u1165F]"
:sora-sompeng #"[\u110D0-\u110FF]"
:bamum #"[\uA6A0-\uA6FF]"
:meroitic #"[\u10980-\u1099F]"})
(defn contains-emoji? [text]
(boolean (re-find emoji-pattern text)))
(defn get-languages [text]
(reduce-kv (fn [result lang pattern]
(if (re-find pattern text)
(conj result lang)
result))
#{}
unicode-ranges))