diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 5a76b109b6..fc364494e1 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -48,11 +48,30 @@ (.waitForTimeout ^js page ms)) (defn wait-for-fonts - "Wait until the browser has finished loading all fonts" + "Wait until the browser has finished loading all fonts. + + Checking `document.fonts.status === 'loaded'` on its own is not enough: + the render page injects its `@font-face` rules asynchronously and the + browser only loads a face lazily, when some painted text actually uses + it. A face that is declared but not yet requested stays in the `unloaded` + state, which does NOT keep `document.fonts.status` at `loading`, so the + check can pass before any real font glyphs are available. The export is + then captured with a (usually wider) fallback font, and auto-width text + laid out with the real font metrics overflows its bounds and gets clipped. + + To avoid that we explicitly request every declared face and await + `document.fonts.ready` before checking the status." ([page] (wait-for-fonts page nil)) ([page {:keys [timeout] :or {timeout 15000}}] (-> (.waitForFunction ^js page - "() => document.fonts && document.fonts.status === 'loaded'" + "async () => { + if (!document.fonts) return true; + try { + await Promise.all(Array.from(document.fonts, (face) => face.load())); + } catch (e) {} + await document.fonts.ready; + return document.fonts.status === 'loaded'; + }" nil #js {:timeout timeout}) (p/catch (fn [cause] diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 70c75fe43f..e3c97c505e 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -34,6 +34,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.exports.wasm :as wasm.exports] + [app.main.data.persistence :as dwp] [app.main.data.plugins :as dp] [app.main.data.workspace :as dw] [app.main.data.workspace.groups :as dwg] @@ -46,6 +47,7 @@ [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.variants :as dwv] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.plugins.fills :as fills] @@ -1264,7 +1266,19 @@ :scale (:scale value 1)}]}] (js/Promise. (fn [resolve reject] - (->> (rp/cmd! :export payload) + ;; The exporter renders the file from its persisted + ;; state, so flush pending local changes and wait until + ;; they are saved before invoking it. Otherwise it may + ;; export a stale/empty shape. (The wasm export above + ;; renders locally and does not need this.) + (st/emit! ::dwp/force-persist) + (->> (rx/concat + (->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) + (rx/filter #(or (nil? %) (= :saved %))) + (rx/first) + (rx/timeout 5000 (rx/empty)) + (rx/ignore)) + (rp/cmd! :export payload)) (rx/mapcat (fn [{:keys [uri]}] (->> (http/send! {:method :get :uri uri