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.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}]

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]
[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})))))))

View File

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