From f32112544eaed3ff8473e881d0b0b78d544ca148 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 25 Sep 2025 16:36:09 +0200 Subject: [PATCH] :sparkles: Make deleted fonts fixer to run with more granular stragegy Instead of running it on all the file, only run it to local library and the current page, reducing considerably the overhead of analyzing the whole file on each file load. It stills executes for page each time the page is loaded, and add some kind of local cache for not doing repeated work each time page loads is pending to be implemented in other commit. --- frontend/src/app/main/data/workspace.cljs | 4 +- .../data/workspace/fix_broken_shapes.cljs | 56 ------ .../data/workspace/fix_deleted_fonts.cljs | 164 ++++++++---------- .../src/app/main/data/workspace/pages.cljs | 2 + 4 files changed, 79 insertions(+), 147 deletions(-) delete mode 100644 frontend/src/app/main/data/workspace/fix_broken_shapes.cljs diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 8a19ef1871..b516e9e75a 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -42,7 +42,6 @@ [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.edition :as dwe] - [app.main.data.workspace.fix-broken-shapes :as fbs] [app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] @@ -232,8 +231,7 @@ ptk/WatchEvent (watch [_ _ _] (rx/of (dp/check-open-plugin) - (fdf/fix-deleted-fonts) - (fbs/fix-broken-shapes))))) + (fdf/fix-deleted-fonts-for-local-library file-id))))) (defn- bundle-fetched [{:keys [file file-id thumbnails] :as bundle}] diff --git a/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs b/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs deleted file mode 100644 index 78f88dc823..0000000000 --- a/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs +++ /dev/null @@ -1,56 +0,0 @@ -;; 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.main.data.workspace.fix-broken-shapes - (:require - [app.main.data.changes :as dch] - [app.main.data.helpers :as dsh] - [beicon.v2.core :as rx] - [potok.v2.core :as ptk])) - -(defn- generate-broken-link-changes - [attr {:keys [objects id] :as container}] - (let [base {:type :fix-obj :fix :broken-children attr id} - contains? (partial contains? objects) - xform (comp - ;; FIXME: Ensure all obj have id field (this is needed - ;; because some bug adds an ephimeral shape with id ZERO, - ;; with a single attr `:shapes` having a vector of ids - ;; pointing to not existing shapes). That happens on - ;; components. THIS IS A WORKAOURD - (map (fn [[id obj]] - (if (some? (:id obj)) - obj - (assoc obj :id id)))) - - ;; Remove all valid shapes - (remove (fn [obj] - (every? contains? (:shapes obj)))) - - (map (fn [obj] - (assoc base :id (:id obj)))))] - - (sequence xform objects))) - -(defn fix-broken-shapes - [] - (ptk/reify ::fix-broken-shapes - ptk/WatchEvent - (watch [it state _] - (let [fdata (dsh/lookup-file-data state) - changes (concat - (mapcat (partial generate-broken-link-changes :page-id) - (vals (:pages-index fdata))) - (mapcat (partial generate-broken-link-changes :component-id) - (vals (:components fdata))))] - - (if (seq changes) - (rx/of (dch/commit-changes - {:origin it - :redo-changes (vec changes) - :undo-changes [] - :save-undo? false})) - (rx/empty)))))) diff --git a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs index 1ffe8a1ba4..fb33a74dc7 100644 --- a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs +++ b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs @@ -14,8 +14,9 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -;; This event will update the file so the texts with non existing custom fonts try to be fixed. -;; This can happen when: +;; This event will update the file so the texts with non existing +;; custom fonts try to be fixed. This can happen when: +;; ;; - Exporting/importing files to different teams or penpot instances ;; - Moving files from one team to another in the same instance ;; - Custom fonts are explicitly deleted in the team area @@ -23,112 +24,99 @@ (defn- calculate-alternative-font-id [value] (let [fonts (deref fonts/fontsdb)] - (->> (vals fonts) - (filter #(= (:family %) value)) - (first) - :id))) + (reduce-kv (fn [_ _ font] + (if (= (:family font) value) + (reduced (:id font)) + nil)) + nil + fonts))) (defn- has-invalid-font-family? [node] - (let [fonts (deref fonts/fontsdb) - font-family (:font-family node) - alternative-font-id (calculate-alternative-font-id font-family)] + (let [fonts (deref fonts/fontsdb) + font-family (:font-family node)] (and (some? font-family) - (nil? (get fonts (:font-id node))) - (some? alternative-font-id)))) + (nil? (get fonts (:font-id node)))))) -(defn- should-fix-deleted-font-shape? +(defn- shape-has-invalid-font-family?? [shape] - (let [text-nodes (txt/node-seq txt/is-text-node? (:content shape))] - (and (cfh/text-shape? shape) - (some has-invalid-font-family? text-nodes)))) - -(defn- should-fix-deleted-font-component? - [component] - (let [xf (comp (map val) - (filter should-fix-deleted-font-shape?))] - (first (sequence xf (:objects component))))) + (and (cfh/text-shape? shape) + (some has-invalid-font-family? + (txt/node-seq txt/is-text-node? (:content shape))))) (defn- fix-deleted-font [node] - (let [alternative-font-id (calculate-alternative-font-id (:font-family node))] - (cond-> node - (some? alternative-font-id) (assoc :font-id alternative-font-id)))) + (if-let [alternative-font-id (calculate-alternative-font-id (:font-family node))] + (assoc node :font-id alternative-font-id) + node)) -(defn- fix-deleted-font-shape +(defn- fix-shape-content [shape] - (let [transform (partial txt/transform-nodes has-invalid-font-family? fix-deleted-font)] - (update shape :content transform))) + (txt/transform-nodes has-invalid-font-family? fix-deleted-font + (:content shape))) -(defn- fix-deleted-font-component - [component] - (update component - :objects - (fn [objects] - (update-vals objects fix-deleted-font-shape)))) - -(defn fix-deleted-font-typography +(defn- fix-typography [typography] - (let [alternative-font-id (calculate-alternative-font-id (:font-family typography))] - (cond-> typography - (some? alternative-font-id) (assoc :font-id alternative-font-id)))) + (if-let [alternative-font-id (calculate-alternative-font-id (:font-family typography))] + (assoc typography :font-id alternative-font-id) + typography)) -(defn- generate-deleted-font-shape-changes +(defn- generate-page-changes [{:keys [objects id]}] - (sequence - (comp (map val) - (filter should-fix-deleted-font-shape?) - (map (fn [shape] - {:type :mod-obj - :id (:id shape) - :page-id id - :operations [{:type :set - :attr :content - :val (:content (fix-deleted-font-shape shape))} - {:type :set - :attr :position-data - :val nil}]}))) - objects)) + (reduce-kv (fn [changes shape-id shape] + (if (shape-has-invalid-font-family?? shape) + (conj changes {:type :mod-obj + :id shape-id + :page-id id + :operations [{:type :set + :attr :content + :val (fix-shape-content shape)} + {:type :set + :attr :position-data + :val nil}]}) + changes)) + [] + objects)) -(defn- generate-deleted-font-components-changes +(defn- generate-library-changes [fdata] - (sequence - (comp (map val) - (filter should-fix-deleted-font-component?) - (map (fn [component] - {:type :mod-component - :id (:id component) - :objects (-> (fix-deleted-font-component component) :objects)}))) - (:components fdata))) + (reduce-kv (fn [changes _ typography] + (if (has-invalid-font-family? typography) + (conj changes {:type :mod-typography + :typography (fix-typography typography)}) + changes)) + [] + (:typographies fdata))) -(defn- generate-deleted-font-typography-changes - [fdata] - (sequence - (comp (map val) - (filter has-invalid-font-family?) - (map (fn [typography] - {:type :mod-typography - :typography (fix-deleted-font-typography typography)}))) - (:typographies fdata))) - -(defn fix-deleted-fonts - [] - (ptk/reify ::fix-deleted-fonts +(defn fix-deleted-fonts-for-local-library + "Looks the file local library for deleted fonts and emit changes if + invalid but fixable typographyes found." + [file-id] + (ptk/reify ::fix-deleted-fonts-for-local-library ptk/WatchEvent (watch [it state _] - (let [fdata (dsh/lookup-file-data state) - pages (:pages-index fdata) - - shape-changes (mapcat generate-deleted-font-shape-changes (vals pages)) - components-changes (generate-deleted-font-components-changes fdata) - typography-changes (generate-deleted-font-typography-changes fdata) - changes (concat shape-changes - components-changes - typography-changes)] - (if (seq changes) + (let [fdata (dsh/lookup-file-data state file-id)] + (when-let [changes (-> (generate-library-changes fdata) + (not-empty))] (rx/of (dwc/commit-changes {:origin it - :redo-changes (vec changes) + :redo-changes changes :undo-changes [] - :save-undo? false})) - (rx/empty)))))) + :save-undo? false}))))))) + +;; FIXME: would be nice to not execute this code twice per page in the +;; same working session, maybe some local memoization can improve that + +(defn fix-deleted-fonts-for-page + [file-id page-id] + (ptk/reify ::fix-deleted-fonts-for-page + ptk/WatchEvent + (watch [it state _] + (let [page (dsh/lookup-page state file-id page-id)] + (when-let [changes (-> (generate-page-changes page) + (not-empty))] + (rx/of (dwc/commit-changes + {:origin it + :redo-changes changes + :undo-changes [] + :save-undo? false}))))))) diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index a9c9d3354e..5b91d10864 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -24,6 +24,7 @@ [app.main.data.helpers :as dsh] [app.main.data.persistence :as-alias dps] [app.main.data.workspace.drawing :as dwd] + [app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.layout :as layout] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.thumbnails :as dwth] @@ -104,6 +105,7 @@ (if (dsh/lookup-page state file-id page-id) (rx/concat (rx/of (initialize-page* file-id page-id) + (fdf/fix-deleted-fonts-for-page file-id page-id) (dwth/watch-state-changes file-id page-id) (dwl/watch-component-changes)) (let [profile (:profile state)