🔧 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.rect]
[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.gfonts :as gf]
[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`
;; 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
;; the bundled default.
;; google fonts through the shared `app.common.render-wasm.gfonts` catalog; builtin fonts
;; through `app.common.render-wasm.builtin-fonts` + the frontend's static `/fonts/`.
(defn- fetch-font-variants
"Team (custom) font variants for the file, or nil — a failure here degrades
@ -189,47 +190,52 @@
:uri uri :detail (explain cause) :cause cause)
(p/resolved nil))))))
(defn- fetch-asset-bytes
"Downloads a stored asset (font TTF) by id, returning a promise of an
ArrayBuffer (or nil)."
[asset-id {:keys [token]}]
(let [headers (asset-headers token)
uri (internal-uri (str "assets/by-id/" asset-id))]
(->> (fetch! uri #js {:method "GET" :headers headers})
(p/mcat (fn [^js resp]
(if (= 200 (.-status resp))
(.arrayBuffer resp)
(p/resolved nil))))
(p/merr (fn [cause]
(l/warn :hint "wasm render: font asset fetch failed"
:asset-id (str asset-id) :uri uri
:detail (explain cause) :cause cause)
(p/resolved nil))))))
(defn- fetch-ttf-bytes
"Downloads a TTF, returning a promise of an ArrayBuffer (or nil). A failure
here degrades to fallback fonts, it does not fail the export."
([uri] (fetch-ttf-bytes uri #js {:method "GET"}))
([uri opts]
(->> (fetch! uri opts)
(p/mcat (fn [^js resp]
(if (= 200 (.-status resp))
(.arrayBuffer resp)
(p/resolved nil))))
(p/merr (fn [cause]
(l/warn :hint "wasm render: font fetch failed"
:uri uri :detail (explain cause) :cause cause)
(p/resolved nil))))))
(defn- gfont-proxy-url
"Rewrites a gstatic ttf url to the local gfonts proxy (same rewrite as the
browser's `google-font-ttf-url`)."
[ttf-url]
(gf/gstatic->proxy-url ttf-url (internal-uri "internal/gfonts/font")))
;; TTF bytes cached for the process lifetime, keyed by whatever identifies the
;; variant (a gfont id+weight+style, a builtin file name).
(defonce ^:private font-bytes* (atom {}))
(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
"Downloads a google font TTF through the local gfonts proxy."
[ttf-url]
(let [uri (gfont-proxy-url ttf-url)]
(->> (fetch! uri #js {:method "GET"})
(p/mcat (fn [^js resp]
(if (= 200 (.-status resp))
(.arrayBuffer resp)
(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))))))
(fetch-ttf-bytes (gf/gstatic->proxy-url ttf-url (internal-uri "internal/gfonts/font"))))
(defn- fetch-builtin-font-bytes
[ttf-file]
(cached-ttf-bytes ttf-file #(fetch-ttf-bytes (internal-uri (str "fonts/" ttf-file)))))
(defn- make-resolve-font
"Builds a `resolve-font` fn (family map -> promise of TTF bytes). Custom
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]
(fn [{:keys [id weight style]}]
(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)))
variants)
(d/seek (fn [v] (= (:font-id v) font-uuid)) variants))]
(if-let [ttf-id (:ttf-file-id variant)]
(fetch-asset-bytes ttf-id params)
(cond
(: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)]
(fetch-gfont-bytes gurl)
(p/resolved nil))))))
@ -272,25 +284,14 @@
(cond-> (fbf/add-noto-fonts [] langs)
emoji? (fbf/add-emoji-font)))))
(defonce ^:private fallback-font-bytes* (atom {}))
(defn- fetch-fallback-font-bytes
"Downloads (and caches for the process lifetime) one fallback font's TTF.
Keyed by the whole variant, not just `font-id`: `resolve-ttf-url` picks a
different TTF per weight/style, so a font-id-only key would serve the first
downloaded variant for every other one."
"Downloads one fallback font's TTF. Cached by the whole variant, not just
`font-id`: `resolve-ttf-url` picks a different TTF per weight/style, so a
font-id-only key would serve the first downloaded variant for every other one."
[{:keys [font-id weight style]}]
(let [cache-key [font-id weight style]]
(if-let [bytes (get @fallback-font-bytes* cache-key)]
(p/resolved bytes)
(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))))))
(if-let [ttf-url (some-> (gf/gfont-id->uuid font-id) (gf/resolve-ttf-url weight style))]
(cached-ttf-bytes [font-id weight style] #(fetch-gfont-bytes ttf-url))
(p/resolved nil)))
(defn- provision-fallback-fonts!
[scene]

View File

@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.render-wasm.builtin-fonts :as bfonts]
[app.common.render-wasm.gfonts :as gf]
[app.common.types.text :as txt]
[app.common.uri :as u]
@ -27,23 +28,7 @@
(def google-fonts gf/catalog)
(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"}]}])
(def local-fonts bfonts/local-fonts)
(defonce fontsdb (l/atom {}))
(defonce fonts (l/atom []))