🔧 Upload builtin font variants in the wasm exporter

This commit is contained in:
Elena Torro 2026-08-04 17:10:12 +02:00
parent 1f510ae22c
commit 7de5ade845
3 changed files with 94 additions and 71 deletions

View File

@ -0,0 +1,37 @@
;; 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 Sucursal en España SL
(ns app.common.render-wasm.builtin-fonts
"Fonts bundled with the frontend, served from `<public-uri>/fonts/`. Shared so
the workspace and the exporter upload the same TTF for a given weight/style."
(:require
[app.common.render-wasm.gfonts :as gf]))
(def local-fonts
[{:id "sourcesanspro"
:name "Source Sans Pro"
:family "sourcesanspro"
:variants
[{:id "200" :name "200" :weight "200" :style "normal" :suffix "extralight" :ttf-url "sourcesanspro-extralight.ttf"}
{:id "200italic" :name "200 Italic" :weight "200" :style "italic" :suffix "extralightitalic" :ttf-url "sourcesanspro-extralightitalic.ttf"}
{:id "300" :name "300" :weight "300" :style "normal" :suffix "light" :ttf-url "sourcesanspro-light.ttf"}
{:id "300italic" :name "300 Italic" :weight "300" :style "italic" :suffix "lightitalic" :ttf-url "sourcesanspro-lightitalic.ttf"}
{:id "regular" :name "400" :weight "400" :style "normal" :ttf-url "sourcesanspro-regular.ttf"}
{:id "italic" :name "400 Italic" :weight "400" :style "italic" :ttf-url "sourcesanspro-italic.ttf"}
{:id "600" :name "600" :weight "600" :style "normal" :suffix "semibold" :ttf-url "sourcesanspro-semibold.ttf"}
{:id "600italic" :name "600 Italic" :weight "600" :style "italic" :suffix "semibolditalic" :ttf-url "sourcesanspro-semibolditalic.ttf"}
{:id "bold" :name "700" :weight "700" :style "normal" :ttf-url "sourcesanspro-bold.ttf"}
{:id "bolditalic" :name "700 Italic" :weight "700" :style "italic" :ttf-url "sourcesanspro-bolditalic.ttf"}
{:id "black" :name "900" :weight "900" :style "normal" :ttf-url "sourcesanspro-black.ttf"}
{:id "blackitalic" :name "900 Italic" :weight "900" :style "italic" :ttf-url "sourcesanspro-blackitalic.ttf"}]}])
(defn resolve-ttf-file
"Builtin TTF file name for `weight` and `style` (0 normal, 1 italic), by the
same nearest-weight rule as the google catalog."
[weight style]
(let [variants (:variants (first local-fonts))]
(:ttf-url (or (gf/closest-variant variants weight (if (zero? style) "normal" "italic"))
(first variants)))))

View File

@ -25,6 +25,7 @@
[app.common.geom.point] [app.common.geom.point]
[app.common.geom.rect] [app.common.geom.rect]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.render-wasm.builtin-fonts :as bfonts]
[app.common.render-wasm.fallback-fonts :as fbf] [app.common.render-wasm.fallback-fonts :as fbf]
[app.common.render-wasm.gfonts :as gf] [app.common.render-wasm.gfonts :as gf]
[app.common.render-wasm.resources :as resources] [app.common.render-wasm.resources :as resources]
@ -167,8 +168,8 @@
;; ;;
;; The text serializer keeps each font's real uuid, so `wasm/fonts-for-shape` ;; The text serializer keeps each font's real uuid, so `wasm/fonts-for-shape`
;; reports it. Custom (team) fonts resolve through the file's font variants, ;; reports it. Custom (team) fonts resolve through the file's font variants,
;; google fonts through the shared `app.common.render-wasm.gfonts` catalog; builtin falls back to ;; google fonts through the shared `app.common.render-wasm.gfonts` catalog; builtin fonts
;; the bundled default. ;; through `app.common.render-wasm.builtin-fonts` + the frontend's static `/fonts/`.
(defn- fetch-font-variants (defn- fetch-font-variants
"Team (custom) font variants for the file, or nil — a failure here degrades "Team (custom) font variants for the file, or nil — a failure here degrades
@ -189,47 +190,52 @@
:uri uri :detail (explain cause) :cause cause) :uri uri :detail (explain cause) :cause cause)
(p/resolved nil)))))) (p/resolved nil))))))
(defn- fetch-asset-bytes (defn- fetch-ttf-bytes
"Downloads a stored asset (font TTF) by id, returning a promise of an "Downloads a TTF, returning a promise of an ArrayBuffer (or nil). A failure
ArrayBuffer (or nil)." here degrades to fallback fonts, it does not fail the export."
[asset-id {:keys [token]}] ([uri] (fetch-ttf-bytes uri #js {:method "GET"}))
(let [headers (asset-headers token) ([uri opts]
uri (internal-uri (str "assets/by-id/" asset-id))] (->> (fetch! uri opts)
(->> (fetch! uri #js {:method "GET" :headers headers}) (p/mcat (fn [^js resp]
(p/mcat (fn [^js resp] (if (= 200 (.-status resp))
(if (= 200 (.-status resp)) (.arrayBuffer resp)
(.arrayBuffer resp) (p/resolved nil))))
(p/resolved nil)))) (p/merr (fn [cause]
(p/merr (fn [cause] (l/warn :hint "wasm render: font fetch failed"
(l/warn :hint "wasm render: font asset fetch failed" :uri uri :detail (explain cause) :cause cause)
:asset-id (str asset-id) :uri uri (p/resolved nil))))))
:detail (explain cause) :cause cause)
(p/resolved nil))))))
(defn- gfont-proxy-url ;; TTF bytes cached for the process lifetime, keyed by whatever identifies the
"Rewrites a gstatic ttf url to the local gfonts proxy (same rewrite as the ;; variant (a gfont id+weight+style, a builtin file name).
browser's `google-font-ttf-url`)." (defonce ^:private font-bytes* (atom {}))
[ttf-url]
(gf/gstatic->proxy-url ttf-url (internal-uri "internal/gfonts/font"))) (defn- cached-ttf-bytes
[cache-key fetch-fn]
(if-let [bytes (get @font-bytes* cache-key)]
(p/resolved bytes)
(->> (fetch-fn)
(p/fmap (fn [buf]
(when buf (swap! font-bytes* assoc cache-key buf))
buf)))))
(defn- fetch-asset-bytes
[asset-id {:keys [token]}]
(fetch-ttf-bytes (internal-uri (str "assets/by-id/" asset-id))
#js {:method "GET" :headers (asset-headers token)}))
(defn- fetch-gfont-bytes (defn- fetch-gfont-bytes
"Downloads a google font TTF through the local gfonts proxy."
[ttf-url] [ttf-url]
(let [uri (gfont-proxy-url ttf-url)] (fetch-ttf-bytes (gf/gstatic->proxy-url ttf-url (internal-uri "internal/gfonts/font"))))
(->> (fetch! uri #js {:method "GET"})
(p/mcat (fn [^js resp] (defn- fetch-builtin-font-bytes
(if (= 200 (.-status resp)) [ttf-file]
(.arrayBuffer resp) (cached-ttf-bytes ttf-file #(fetch-ttf-bytes (internal-uri (str "fonts/" ttf-file)))))
(p/resolved nil))))
(p/merr (fn [cause]
(l/warn :hint "wasm render: gfont fetch failed"
:url uri :detail (explain cause) :cause cause)
(p/resolved nil))))))
(defn- make-resolve-font (defn- make-resolve-font
"Builds a `resolve-font` fn (family map -> promise of TTF bytes). Custom "Builds a `resolve-font` fn (family map -> promise of TTF bytes). Custom
variants first, matching uuid+weight+style then degrading to uuid+weight then variants first, matching uuid+weight+style then degrading to uuid+weight then
uuid; google catalog after that; nil when nothing matches." uuid; the bundled fonts for `uuid/zero`, which is what `font-id->uuid` maps
every builtin family to; google catalog otherwise."
[variants params] [variants params]
(fn [{:keys [id weight style]}] (fn [{:keys [id weight style]}]
(let [font-uuid (uuid/from-unsigned-parts (aget id 0) (aget id 1) (aget id 2) (aget id 3)) (let [font-uuid (uuid/from-unsigned-parts (aget id 0) (aget id 1) (aget id 2) (aget id 3))
@ -242,8 +248,14 @@
(= (:font-weight v) weight))) (= (:font-weight v) weight)))
variants) variants)
(d/seek (fn [v] (= (:font-id v) font-uuid)) variants))] (d/seek (fn [v] (= (:font-id v) font-uuid)) variants))]
(if-let [ttf-id (:ttf-file-id variant)] (cond
(fetch-asset-bytes ttf-id params) (:ttf-file-id variant)
(fetch-asset-bytes (:ttf-file-id variant) params)
(= uuid/zero font-uuid)
(fetch-builtin-font-bytes (bfonts/resolve-ttf-file weight style))
:else
(if-let [gurl (gf/resolve-ttf-url font-uuid weight style)] (if-let [gurl (gf/resolve-ttf-url font-uuid weight style)]
(fetch-gfont-bytes gurl) (fetch-gfont-bytes gurl)
(p/resolved nil)))))) (p/resolved nil))))))
@ -272,25 +284,14 @@
(cond-> (fbf/add-noto-fonts [] langs) (cond-> (fbf/add-noto-fonts [] langs)
emoji? (fbf/add-emoji-font))))) emoji? (fbf/add-emoji-font)))))
(defonce ^:private fallback-font-bytes* (atom {}))
(defn- fetch-fallback-font-bytes (defn- fetch-fallback-font-bytes
"Downloads (and caches for the process lifetime) one fallback font's TTF. "Downloads one fallback font's TTF. Cached by the whole variant, not just
Keyed by the whole variant, not just `font-id`: `resolve-ttf-url` picks a `font-id`: `resolve-ttf-url` picks a different TTF per weight/style, so a
different TTF per weight/style, so a font-id-only key would serve the first font-id-only key would serve the first downloaded variant for every other one."
downloaded variant for every other one."
[{:keys [font-id weight style]}] [{:keys [font-id weight style]}]
(let [cache-key [font-id weight style]] (if-let [ttf-url (some-> (gf/gfont-id->uuid font-id) (gf/resolve-ttf-url weight style))]
(if-let [bytes (get @fallback-font-bytes* cache-key)] (cached-ttf-bytes [font-id weight style] #(fetch-gfont-bytes ttf-url))
(p/resolved bytes) (p/resolved nil)))
(let [font-uuid (gf/gfont-id->uuid font-id)
ttf-url (some-> font-uuid (gf/resolve-ttf-url weight style))]
(if ttf-url
(->> (fetch-gfont-bytes ttf-url)
(p/fmap (fn [buf]
(when buf (swap! fallback-font-bytes* assoc cache-key buf))
buf)))
(p/resolved nil))))))
(defn- provision-fallback-fonts! (defn- provision-fallback-fonts!
[scene] [scene]

View File

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.render-wasm.builtin-fonts :as bfonts]
[app.common.render-wasm.gfonts :as gf] [app.common.render-wasm.gfonts :as gf]
[app.common.types.text :as txt] [app.common.types.text :as txt]
[app.common.uri :as u] [app.common.uri :as u]
@ -27,23 +28,7 @@
(def google-fonts gf/catalog) (def google-fonts gf/catalog)
(def local-fonts (def local-fonts bfonts/local-fonts)
[{:id "sourcesanspro"
:name "Source Sans Pro"
:family "sourcesanspro"
:variants
[{:id "200" :name "200" :weight "200" :style "normal" :suffix "extralight" :ttf-url "sourcesanspro-extralight.ttf"}
{:id "200italic" :name "200 Italic" :weight "200" :style "italic" :suffix "extralightitalic" :ttf-url "sourcesanspro-extralightitalic.ttf"}
{:id "300" :name "300" :weight "300" :style "normal" :suffix "light" :ttf-url "sourcesanspro-light.ttf"}
{:id "300italic" :name "300 Italic" :weight "300" :style "italic" :suffix "lightitalic" :ttf-url "sourcesanspro-lightitalic.ttf"}
{:id "regular" :name "400" :weight "400" :style "normal" :ttf-url "sourcesanspro-regular.ttf"}
{:id "italic" :name "400 Italic" :weight "400" :style "italic" :ttf-url "sourcesanspro-italic.ttf"}
{:id "600" :name "600" :weight "600" :style "normal" :suffix "semibold" :ttf-url "sourcesanspro-semibold.ttf"}
{:id "600italic" :name "600 Italic" :weight "600" :style "italic" :suffix "semibolditalic" :ttf-url "sourcesanspro-semibolditalic.ttf"}
{:id "bold" :name "700" :weight "700" :style "normal" :ttf-url "sourcesanspro-bold.ttf"}
{:id "bolditalic" :name "700 Italic" :weight "700" :style "italic" :ttf-url "sourcesanspro-bolditalic.ttf"}
{:id "black" :name "900" :weight "900" :style "normal" :ttf-url "sourcesanspro-black.ttf"}
{:id "blackitalic" :name "900 Italic" :weight "900" :style "italic" :ttf-url "sourcesanspro-blackitalic.ttf"}]}])
(defonce fontsdb (l/atom {})) (defonce fontsdb (l/atom {}))
(defonce fonts (l/atom [])) (defonce fonts (l/atom []))