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.
This commit is contained in:
Andrey Antukh 2025-09-25 16:36:09 +02:00
parent 27e311277a
commit f32112544e
4 changed files with 79 additions and 147 deletions

View File

@ -42,7 +42,6 @@
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe] [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.fix-deleted-fonts :as fdf]
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.guides :as dwgu] [app.main.data.workspace.guides :as dwgu]
@ -232,8 +231,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dp/check-open-plugin) (rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts) (fdf/fix-deleted-fonts-for-local-library file-id)))))
(fbs/fix-broken-shapes)))))
(defn- bundle-fetched (defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}] [{:keys [file file-id thumbnails] :as bundle}]

View File

@ -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))))))

View File

@ -14,8 +14,9 @@
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
;; This event will update the file so the texts with non existing custom fonts try to be fixed. ;; This event will update the file so the texts with non existing
;; This can happen when: ;; custom fonts try to be fixed. This can happen when:
;;
;; - Exporting/importing files to different teams or penpot instances ;; - Exporting/importing files to different teams or penpot instances
;; - Moving files from one team to another in the same instance ;; - Moving files from one team to another in the same instance
;; - Custom fonts are explicitly deleted in the team area ;; - Custom fonts are explicitly deleted in the team area
@ -23,112 +24,99 @@
(defn- calculate-alternative-font-id (defn- calculate-alternative-font-id
[value] [value]
(let [fonts (deref fonts/fontsdb)] (let [fonts (deref fonts/fontsdb)]
(->> (vals fonts) (reduce-kv (fn [_ _ font]
(filter #(= (:family %) value)) (if (= (:family font) value)
(first) (reduced (:id font))
:id))) nil))
nil
fonts)))
(defn- has-invalid-font-family? (defn- has-invalid-font-family?
[node] [node]
(let [fonts (deref fonts/fontsdb) (let [fonts (deref fonts/fontsdb)
font-family (:font-family node) font-family (:font-family node)]
alternative-font-id (calculate-alternative-font-id font-family)]
(and (some? font-family) (and (some? font-family)
(nil? (get fonts (:font-id node))) (nil? (get fonts (:font-id node))))))
(some? alternative-font-id))))
(defn- should-fix-deleted-font-shape? (defn- shape-has-invalid-font-family??
[shape] [shape]
(let [text-nodes (txt/node-seq txt/is-text-node? (:content shape))] (and (cfh/text-shape? shape)
(and (cfh/text-shape? shape) (some has-invalid-font-family?
(some has-invalid-font-family? text-nodes)))) (txt/node-seq txt/is-text-node? (:content shape)))))
(defn- should-fix-deleted-font-component?
[component]
(let [xf (comp (map val)
(filter should-fix-deleted-font-shape?))]
(first (sequence xf (:objects component)))))
(defn- fix-deleted-font (defn- fix-deleted-font
[node] [node]
(let [alternative-font-id (calculate-alternative-font-id (:font-family node))] (if-let [alternative-font-id (calculate-alternative-font-id (:font-family node))]
(cond-> node (assoc node :font-id alternative-font-id)
(some? alternative-font-id) (assoc :font-id alternative-font-id)))) node))
(defn- fix-deleted-font-shape (defn- fix-shape-content
[shape] [shape]
(let [transform (partial txt/transform-nodes has-invalid-font-family? fix-deleted-font)] (txt/transform-nodes has-invalid-font-family? fix-deleted-font
(update shape :content transform))) (:content shape)))
(defn- fix-deleted-font-component (defn- fix-typography
[component]
(update component
:objects
(fn [objects]
(update-vals objects fix-deleted-font-shape))))
(defn fix-deleted-font-typography
[typography] [typography]
(let [alternative-font-id (calculate-alternative-font-id (:font-family typography))] (if-let [alternative-font-id (calculate-alternative-font-id (:font-family typography))]
(cond-> typography (assoc typography :font-id alternative-font-id)
(some? alternative-font-id) (assoc :font-id alternative-font-id)))) typography))
(defn- generate-deleted-font-shape-changes (defn- generate-page-changes
[{:keys [objects id]}] [{:keys [objects id]}]
(sequence (reduce-kv (fn [changes shape-id shape]
(comp (map val) (if (shape-has-invalid-font-family?? shape)
(filter should-fix-deleted-font-shape?) (conj changes {:type :mod-obj
(map (fn [shape] :id shape-id
{:type :mod-obj :page-id id
:id (:id shape) :operations [{:type :set
:page-id id :attr :content
:operations [{:type :set :val (fix-shape-content shape)}
:attr :content {:type :set
:val (:content (fix-deleted-font-shape shape))} :attr :position-data
{:type :set :val nil}]})
:attr :position-data changes))
:val nil}]}))) []
objects)) objects))
(defn- generate-deleted-font-components-changes (defn- generate-library-changes
[fdata] [fdata]
(sequence (reduce-kv (fn [changes _ typography]
(comp (map val) (if (has-invalid-font-family? typography)
(filter should-fix-deleted-font-component?) (conj changes {:type :mod-typography
(map (fn [component] :typography (fix-typography typography)})
{:type :mod-component changes))
:id (:id component) []
:objects (-> (fix-deleted-font-component component) :objects)}))) (:typographies fdata)))
(:components fdata)))
(defn- generate-deleted-font-typography-changes (defn fix-deleted-fonts-for-local-library
[fdata] "Looks the file local library for deleted fonts and emit changes if
(sequence invalid but fixable typographyes found."
(comp (map val) [file-id]
(filter has-invalid-font-family?) (ptk/reify ::fix-deleted-fonts-for-local-library
(map (fn [typography]
{:type :mod-typography
:typography (fix-deleted-font-typography typography)})))
(:typographies fdata)))
(defn fix-deleted-fonts
[]
(ptk/reify ::fix-deleted-fonts
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [fdata (dsh/lookup-file-data state) (let [fdata (dsh/lookup-file-data state file-id)]
pages (:pages-index fdata) (when-let [changes (-> (generate-library-changes fdata)
(not-empty))]
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)
(rx/of (dwc/commit-changes (rx/of (dwc/commit-changes
{:origin it {:origin it
:redo-changes (vec changes) :redo-changes changes
:undo-changes [] :undo-changes []
:save-undo? false})) :save-undo? false})))))))
(rx/empty))))))
;; 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})))))))

View File

@ -24,6 +24,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.persistence :as-alias dps] [app.main.data.persistence :as-alias dps]
[app.main.data.workspace.drawing :as dwd] [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.layout :as layout]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.thumbnails :as dwth] [app.main.data.workspace.thumbnails :as dwth]
@ -104,6 +105,7 @@
(if (dsh/lookup-page state file-id page-id) (if (dsh/lookup-page state file-id page-id)
(rx/concat (rx/concat
(rx/of (initialize-page* file-id page-id) (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) (dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes)) (dwl/watch-component-changes))
(let [profile (:profile state) (let [profile (:profile state)