mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
🐛 Fix silent failure when uploading custom fonts (#12315)
* 🐛 Fix silent failure when uploading custom fonts Custom font upload crashed with re-seq must match against a string when opentype.js returned nil subfamily names, leaving the user with no feedback. Unknown signatures and unparseable files were also silently dropped. Make variant parsing nil-safe, harden entry preparation for missing tables, report every per-file failure with a bad-font notification, and show a generic error if the stream itself fails. Closes #12304 AI-assisted-by: muse-spark-1.3-contributor * ♻️ Rename font mtype helper and mark helpers private Avoid confusion with app.worker.import/parse-mtype, which detects binfile signatures. The font helper now lives as parse-font-mtype, and the upload helpers only used internally are private, with tests reaching them through var references. AI-assisted-by: muse-spark-1.3-contributor * 💄 Fix formatting in font upload tests Realign map keys shifted by the var-quote prefix so cljfmt check passes again. No behavior change. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Fix infer warnings in fonts metrics code Add missing ^js type hints to the hhea and os2 metric lookups in prepare-font so shadow-cljs can infer the interop targets and the release build reports zero warnings. AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
parent
ccdfaffbef
commit
03134d35ae
@ -99,26 +99,34 @@
|
||||
(str/replace filename image-extensions-re "")))
|
||||
|
||||
(defn parse-font-weight
|
||||
"Parses a font variant/subfamily string into a numeric weight.
|
||||
A nil or non-string variant means the font carries no subfamily
|
||||
metadata, so it defaults to 400 (regular), the same as a blank string."
|
||||
[variant]
|
||||
(cond
|
||||
(re-seq #"(?i)(?:^|[-_\s])(hairline|thin)(?=(?:[-_\s]|$|italic\b))" variant) 100
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*light|ultra\s*light)(?=(?:[-_\s]|$|italic\b))" variant) 200
|
||||
(re-seq #"(?i)(?:^|[-_\s])(light)(?=(?:[-_\s]|$|italic\b))" variant) 300
|
||||
(re-seq #"(?i)(?:^|[-_\s])(normal|regular)(?=(?:[-_\s]|$|italic\b))" variant) 400
|
||||
(re-seq #"(?i)(?:^|[-_\s])(medium)(?=(?:[-_\s]|$|italic\b))" variant) 500
|
||||
(re-seq #"(?i)(?:^|[-_\s])(semi\s*bold|demi\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 600
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*bold|ultra\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 800
|
||||
(re-seq #"(?i)(?:^|[-_\s])(bold)(?=(?:[-_\s]|$|italic\b))" variant) 700
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*black|ultra\s*black)(?=(?:[-_\s]|$|italic\b))" variant) 950
|
||||
(re-seq #"(?i)(?:^|[-_\s])(black|heavy|solid)(?=(?:[-_\s]|$|italic\b))" variant) 900
|
||||
:else 400))
|
||||
(let [variant (if (string? variant) variant "")]
|
||||
(cond
|
||||
(re-seq #"(?i)(?:^|[-_\s])(hairline|thin)(?=(?:[-_\s]|$|italic\b))" variant) 100
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*light|ultra\s*light)(?=(?:[-_\s]|$|italic\b))" variant) 200
|
||||
(re-seq #"(?i)(?:^|[-_\s])(light)(?=(?:[-_\s]|$|italic\b))" variant) 300
|
||||
(re-seq #"(?i)(?:^|[-_\s])(normal|regular)(?=(?:[-_\s]|$|italic\b))" variant) 400
|
||||
(re-seq #"(?i)(?:^|[-_\s])(medium)(?=(?:[-_\s]|$|italic\b))" variant) 500
|
||||
(re-seq #"(?i)(?:^|[-_\s])(semi\s*bold|demi\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 600
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*bold|ultra\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 800
|
||||
(re-seq #"(?i)(?:^|[-_\s])(bold)(?=(?:[-_\s]|$|italic\b))" variant) 700
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*black|ultra\s*black)(?=(?:[-_\s]|$|italic\b))" variant) 950
|
||||
(re-seq #"(?i)(?:^|[-_\s])(black|heavy|solid)(?=(?:[-_\s]|$|italic\b))" variant) 900
|
||||
:else 400)))
|
||||
|
||||
(defn parse-font-style
|
||||
"Parses a font variant/subfamily string into \"italic\" or \"normal\".
|
||||
A nil or non-string variant means the font carries no subfamily
|
||||
metadata, so it defaults to \"normal\"."
|
||||
[variant]
|
||||
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic)(?:[-_\s]|$)" variant)
|
||||
(re-seq #"(?i)italic$" variant))
|
||||
"italic"
|
||||
"normal"))
|
||||
(let [variant (if (string? variant) variant "")]
|
||||
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic)(?:[-_\s]|$)" variant)
|
||||
(re-seq #"(?i)italic$" variant))
|
||||
"italic"
|
||||
"normal")))
|
||||
|
||||
(defn font-weight->name
|
||||
[weight]
|
||||
|
||||
@ -35,7 +35,12 @@
|
||||
(t/is (= 400 (media/parse-font-weight "Lighthaus")))
|
||||
(t/is (= 400 (media/parse-font-weight "Blackwood")))
|
||||
(t/is (= 400 (media/parse-font-weight "Thinker")))
|
||||
(t/is (= 400 (media/parse-font-weight "Mediaeval")))))
|
||||
(t/is (= 400 (media/parse-font-weight "Mediaeval"))))
|
||||
|
||||
(t/testing "defaults to regular when the variant metadata is missing"
|
||||
(t/is (= 400 (media/parse-font-weight nil)))
|
||||
(t/is (= 400 (media/parse-font-weight "")))
|
||||
(t/is (= 400 (media/parse-font-weight " ")))))
|
||||
|
||||
(t/deftest test-parse-font-style
|
||||
(t/testing "matches italic with proper boundaries"
|
||||
@ -47,7 +52,12 @@
|
||||
|
||||
(t/testing "does not match italic embedded in words"
|
||||
(t/is (= "normal" (media/parse-font-style "Italica")))
|
||||
(t/is (= "normal" (media/parse-font-style "Roboto-Regular")))))
|
||||
(t/is (= "normal" (media/parse-font-style "Roboto-Regular"))))
|
||||
|
||||
(t/testing "defaults to normal when the variant metadata is missing"
|
||||
(t/is (= "normal" (media/parse-font-style nil)))
|
||||
(t/is (= "normal" (media/parse-font-style "")))
|
||||
(t/is (= "normal" (media/parse-font-style " ")))))
|
||||
|
||||
(t/deftest test-strip-image-extension
|
||||
(t/testing "removes extension from supported image files"
|
||||
|
||||
@ -115,6 +115,128 @@
|
||||
:font-style font-style
|
||||
:uploads uploads})))))
|
||||
|
||||
(defn- parse-font-mtype
|
||||
"Detects a font mime-type from the first four bytes of an ArrayBuffer.
|
||||
|
||||
Returns the `font/*` mime-type string, or nil when the buffer is
|
||||
missing, too short, or carries an unknown signature. Unknown
|
||||
signatures are treated as errors by `read-blob`, never silently
|
||||
dropped."
|
||||
[ba]
|
||||
(when (and (some? ba)
|
||||
(>= (.-byteLength ^js ba) 4))
|
||||
(let [u8 (js/Uint8Array. ba 0 4)
|
||||
sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))]
|
||||
(case sg
|
||||
"117 124 124 117" "font/otf"
|
||||
"0 1 0 0" "font/ttf"
|
||||
"167 117 106 106" "font/woff"
|
||||
"167 117 106 62" "font/woff2"
|
||||
nil))))
|
||||
|
||||
(defn- parse-font
|
||||
"Associates the opentype.js font object for TTF/OTF/WOFF blobs.
|
||||
|
||||
Takes a `read-blob` result map. Passes `{:error ...}` maps through
|
||||
untouched. WOFF2 blobs cannot be parsed by opentype.js, so they keep
|
||||
`:font nil` and metadata is derived from the filename later. Any other
|
||||
blob is parsed with opentype.js; when parsing fails an
|
||||
`{:error filename}` map is returned so the caller can notify the user
|
||||
instead of silently dropping the file."
|
||||
[{:keys [error data type name] :as params}]
|
||||
(cond
|
||||
(some? error)
|
||||
params
|
||||
|
||||
(= type "font/woff2")
|
||||
;; woff2 cannot be parsed by opentype.js, extract metadata from filename
|
||||
(assoc params :font nil)
|
||||
|
||||
:else
|
||||
(try
|
||||
(assoc params :font (ot/parse data))
|
||||
(catch :default _e
|
||||
(log/warn :msg (str/fmt "skipping file %s, unsupported format" name))
|
||||
{:error name}))))
|
||||
|
||||
(defn- prepare-font
|
||||
"Builds a ready-to-upload font entry from a parsed blob.
|
||||
|
||||
Never throws for missing font metadata: a nil subfamily (opentype.js
|
||||
returns nil when the font carries no English preferred/subfamily
|
||||
names, as seen with some Fontshare families) defaults to regular
|
||||
weight/style, and missing vertical-metric tables default to no
|
||||
height warning."
|
||||
[{:keys [font type name data] :as _params}]
|
||||
(let [name (or name "")]
|
||||
(if (some? font)
|
||||
;; Font was parsed with opentype.js (ttf, otf, woff)
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily")
|
||||
"")
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily")
|
||||
"")
|
||||
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of
|
||||
;; text. For historical reasons, there are three pairs of ascender/descender
|
||||
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
|
||||
;; system and application a different set will be used to render text on the
|
||||
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
|
||||
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
|
||||
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
|
||||
;; table. On Windows, all browsers use the usWin metrics, but respect the
|
||||
;; useTypoMetrics setting and if set will use the OS/2 values.
|
||||
|
||||
hhea (some-> ^js font .-tables .-hhea)
|
||||
os2 (some-> ^js font .-tables .-os2)
|
||||
|
||||
hhea-ascender (some-> ^js hhea .-ascender abs)
|
||||
hhea-descender (some-> ^js hhea .-descender abs)
|
||||
|
||||
win-ascent (some-> ^js os2 .-usWinAscent abs)
|
||||
win-descent (some-> ^js os2 .-usWinDescent abs)
|
||||
|
||||
os2-ascent (some-> ^js os2 .-sTypoAscender abs)
|
||||
os2-descent (some-> ^js os2 .-sTypoDescender abs)
|
||||
|
||||
;; useTypoMetrics can be read from the 7th bit
|
||||
f-selection (some-> ^js os2 .-fsSelection (bit-test 7))
|
||||
|
||||
height-warning? (boolean
|
||||
(and hhea-ascender hhea-descender win-ascent win-descent
|
||||
(or (not= hhea-ascender win-ascent)
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection os2-ascent os2-descent
|
||||
(or (not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
:font-weight (cm/parse-font-weight variant)
|
||||
:font-style (cm/parse-font-style variant)
|
||||
:variant-name variant
|
||||
:height-warning? height-warning?})
|
||||
;; Font could not be parsed (woff2), extract metadata from filename
|
||||
(let [base-name (str/replace name #"\.[^.]+$" "")
|
||||
;; Strip known weight/style tokens and separators to derive family name
|
||||
;; Use word boundaries to avoid matching substrings (e.g. "Boldini" should not match "bold")
|
||||
raw-family-name (-> base-name
|
||||
(str/replace #"(?i)(^|[-_\s])(extra\s*black|ultra\s*black|extra\s*bold|ultra\s*bold|semi\s*bold|demi\s*bold|extra\s*light|ultra\s*light|hairline|thin|light|normal|regular|medium|bold|black|heavy|solid|italic)([-_\s]|$)" "$1$3")
|
||||
(str/replace #"[-_\s]+" " ")
|
||||
(str/trim))
|
||||
family-name (if (str/blank? raw-family-name) base-name raw-family-name)
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:font-family family-name
|
||||
:font-weight (cm/parse-font-weight base-name)
|
||||
:font-style (cm/parse-font-style base-name)
|
||||
:height-warning? false}))))
|
||||
|
||||
(defn process-upload
|
||||
"Given a seq of blobs and the team id, creates a ready-to-use fonts
|
||||
map with temporal ID's associated to each font entry.
|
||||
@ -122,71 +244,16 @@
|
||||
Each font entry's `:data` is a map of `{mtype -> ArrayBuffer}`. The
|
||||
raw `ArrayBuffer` is kept as-is so that `upload-font-variant` can
|
||||
wrap it in a `Blob` and hand it directly to `upload-blob-chunked`
|
||||
without any intermediate client-side chunking."
|
||||
without any intermediate client-side chunking.
|
||||
|
||||
Files that cannot be read, carry an unknown signature, fail
|
||||
opentype.js parsing, or fail entry preparation are collected and
|
||||
reported with a `bad-font` notification instead of silently dropped,
|
||||
so the user always gets feedback. The returned observable never
|
||||
errors for per-file failures; it emits the map of successfully
|
||||
processed fonts (possibly empty)."
|
||||
[blobs team-id]
|
||||
(letfn [(prepare [{:keys [font type name data] :as params}]
|
||||
(if font
|
||||
;; Font was parsed with opentype.js (ttf, otf, woff)
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily"))
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))
|
||||
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of
|
||||
;; text. For historical reasons, there are three pairs of ascender/descender
|
||||
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
|
||||
;; system and application a different set will be used to render text on the
|
||||
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
|
||||
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
|
||||
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
|
||||
;; table. On Windows, all browsers use the usWin metrics, but respect the
|
||||
;; useTypoMetrics setting and if set will use the OS/2 values.
|
||||
|
||||
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
|
||||
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
|
||||
|
||||
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
|
||||
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
|
||||
|
||||
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
|
||||
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
|
||||
|
||||
;; useTypoMetrics can be read from the 7th bit
|
||||
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
|
||||
|
||||
height-warning? (or (not= hhea-ascender win-ascent)
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
:font-weight (cm/parse-font-weight variant)
|
||||
:font-style (cm/parse-font-style variant)
|
||||
:variant-name variant
|
||||
:height-warning? height-warning?})
|
||||
;; Font could not be parsed (woff2), extract metadata from filename
|
||||
(let [base-name (str/replace name #"\.[^.]+$" "")
|
||||
;; Strip known weight/style tokens and separators to derive family name
|
||||
;; Use word boundaries to avoid matching substrings (e.g. "Boldini" should not match "bold")
|
||||
raw-family-name (-> base-name
|
||||
(str/replace #"(?i)(^|[-_\s])(extra\s*black|ultra\s*black|extra\s*bold|ultra\s*bold|semi\s*bold|demi\s*bold|extra\s*light|ultra\s*light|hairline|thin|light|normal|regular|medium|bold|black|heavy|solid|italic)([-_\s]|$)" "$1$3")
|
||||
(str/replace #"[-_\s]+" " ")
|
||||
(str/trim))
|
||||
family-name (if (str/blank? raw-family-name) base-name raw-family-name)
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:font-family family-name
|
||||
:font-weight (cm/parse-font-weight base-name)
|
||||
:font-style (cm/parse-font-style base-name)
|
||||
:height-warning? false})))
|
||||
|
||||
(join [res {:keys [content] :as font}]
|
||||
(letfn [(join [res {:keys [content] :as font}]
|
||||
(let [key-fn (juxt :font-family :font-weight :font-style)
|
||||
existing (d/seek #(= (key-fn font) (key-fn %)) (vals res))]
|
||||
(if existing
|
||||
@ -206,60 +273,42 @@
|
||||
(:data content)})
|
||||
(dissoc :content)))))))
|
||||
|
||||
(parse-mtype [ba]
|
||||
(let [u8 (js/Uint8Array. ba 0 4)
|
||||
sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))]
|
||||
(case sg
|
||||
"117 124 124 117" "font/otf"
|
||||
"0 1 0 0" "font/ttf"
|
||||
"167 117 106 106" "font/woff"
|
||||
"167 117 106 62" "font/woff2")))
|
||||
|
||||
(parse-font [{:keys [data type name] :as params}]
|
||||
(if (= type "font/woff2")
|
||||
;; woff2 cannot be parsed by opentype.js, extract metadata from filename
|
||||
(assoc params :font nil)
|
||||
(try
|
||||
(assoc params :font (ot/parse data))
|
||||
(catch :default _e
|
||||
(log/warn :msg (str/fmt "skipping file %s, unsupported format" name))
|
||||
nil))))
|
||||
|
||||
(read-blob [blob]
|
||||
(->> (wa/read-file-as-array-buffer blob)
|
||||
(rx/map (fn [data]
|
||||
{:data data
|
||||
:name (.-name blob)
|
||||
:type (parse-mtype data)}))
|
||||
(rx/catch (fn []
|
||||
(rx/of {:error (.-name blob)})))
|
||||
(rx/mapcat (fn [{:keys [type, error] :as font}]
|
||||
(if (or type error)
|
||||
(rx/of font)
|
||||
(rx/empty))))))]
|
||||
(let [blob-name (.-name blob)]
|
||||
(->> (wa/read-file-as-array-buffer blob)
|
||||
(rx/map (fn [data]
|
||||
{:data data
|
||||
:name blob-name
|
||||
:type (parse-font-mtype data)}))
|
||||
(rx/catch (fn []
|
||||
(rx/of {:error blob-name})))
|
||||
(rx/mapcat (fn [{:keys [type error] :as font}]
|
||||
(if (or type error)
|
||||
(rx/of font)
|
||||
;; Unknown signature: report it instead of silently dropping it.
|
||||
(rx/of {:error blob-name})))))))]
|
||||
|
||||
(let [fonts (->> (rx/from blobs)
|
||||
(rx/mapcat read-blob))
|
||||
errors (->> fonts
|
||||
(rx/filter #(some? (:error %)))
|
||||
(rx/reduce (fn [acc font]
|
||||
(conj acc (str "'" (:error font) "'")))
|
||||
[]))]
|
||||
|
||||
(rx/subscribe errors
|
||||
#(when
|
||||
(not-empty %)
|
||||
(st/emit!
|
||||
(ntf/error
|
||||
(if (> (count %) 1)
|
||||
(tr "errors.bad-font-plural" (str/join ", " %))
|
||||
(tr "errors.bad-font" (first %)))))))
|
||||
(->> fonts
|
||||
(rx/filter #(nil? (:error %)))
|
||||
(rx/map parse-font)
|
||||
(rx/filter some?)
|
||||
(rx/map prepare)
|
||||
(rx/reduce join {})))))
|
||||
(->> (rx/from blobs)
|
||||
(rx/mapcat read-blob)
|
||||
(rx/map parse-font)
|
||||
(rx/filter some?)
|
||||
(rx/reduce (fn [acc item]
|
||||
(if (:error item)
|
||||
(update acc :errors conj (str "'" (:error item) "'"))
|
||||
(try
|
||||
(update acc :fonts #(join % (prepare-font item)))
|
||||
(catch :default cause
|
||||
(log/warn :msg "skipping font file" :cause cause :name (:name item))
|
||||
(update acc :errors conj (str "'" (:name item) "'"))))))
|
||||
{:fonts {} :errors []})
|
||||
(rx/mapcat (fn [{:keys [fonts errors]}]
|
||||
(when (seq errors)
|
||||
(st/emit!
|
||||
(ntf/error
|
||||
(if (> (count errors) 1)
|
||||
(tr "errors.bad-font-plural" (str/join ", " errors))
|
||||
(tr "errors.bad-font" (first errors))))))
|
||||
(rx/of fonts))))))
|
||||
|
||||
(defn- calculate-family-to-id-mapping
|
||||
[existing]
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
(rx/subs! (fn [result]
|
||||
(swap! fonts* df/merge-and-group-fonts installed-fonts result))
|
||||
(fn [error]
|
||||
(st/emit! (ntf/error (tr "errors.generic")))
|
||||
(js/console.error "error" error))))))
|
||||
|
||||
on-upload
|
||||
|
||||
@ -6,13 +6,18 @@
|
||||
|
||||
(ns frontend-tests.fonts-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.http :as http]
|
||||
[app.util.webapi :as wa]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as async]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def sample-font
|
||||
{:id "sourcesanspro"
|
||||
@ -244,3 +249,105 @@
|
||||
;; Trailing settle: the body is synchronous, but `with-mocks*`
|
||||
;; evaluates to a promise, so the body must settle one.
|
||||
(await (async/settle))))))
|
||||
|
||||
;; --- custom font upload ------------------------------------------------------
|
||||
;;
|
||||
;; Regression tests for https://github.com/penpot/penpot/issues/12304:
|
||||
;; uploading a font whose opentype.js names are missing (nil subfamily,
|
||||
;; as seen with some Fontshare families) crashed `process-upload` with
|
||||
;; `re-seq must match against a string` and left the user with no
|
||||
;; feedback. Unknown signatures and unparseable files were silently
|
||||
;; dropped as well.
|
||||
|
||||
(defn- bytes->buffer
|
||||
[bytes]
|
||||
(.-buffer (js/Uint8Array. (clj->js bytes))))
|
||||
|
||||
(defn- fake-ot-font
|
||||
[{:keys [family variant hhea os2]}]
|
||||
(let [names {"preferredFamily" family
|
||||
"fontFamily" family
|
||||
"preferredSubfamily" variant
|
||||
"fontSubfamily" variant}]
|
||||
#js {:getEnglishName (fn [k] (get names k))
|
||||
:tables #js {:hhea hhea :os2 os2}}))
|
||||
|
||||
(t/deftest parse-font-mtype-detects-font-signatures
|
||||
(t/is (= "font/ttf" (#'df/parse-font-mtype (bytes->buffer [0 1 0 0]))))
|
||||
(t/is (= "font/otf" (#'df/parse-font-mtype (bytes->buffer [79 84 84 79]))))
|
||||
(t/is (= "font/woff" (#'df/parse-font-mtype (bytes->buffer [119 79 70 70]))))
|
||||
(t/is (= "font/woff2" (#'df/parse-font-mtype (bytes->buffer [119 79 70 50]))))
|
||||
(t/is (nil? (#'df/parse-font-mtype (bytes->buffer [1 2 3 4]))))
|
||||
(t/is (nil? (#'df/parse-font-mtype nil)))
|
||||
(t/is (nil? (#'df/parse-font-mtype (bytes->buffer [0 1])))))
|
||||
|
||||
(t/deftest prepare-font-defaults-missing-subfamily
|
||||
(let [font (fake-ot-font {:family "Satoshi"
|
||||
:variant nil
|
||||
:hhea #js {:ascender 1000 :descender 300}
|
||||
:os2 #js {:usWinAscent 1000 :usWinDescent 300
|
||||
:sTypoAscender 800 :sTypoDescender 200
|
||||
:fsSelection 0}})
|
||||
entry (#'df/prepare-font {:font font
|
||||
:type "font/otf"
|
||||
:name "Satoshi-Regular.otf"
|
||||
:data (bytes->buffer [79 84 84 79 0 0 0 0])})]
|
||||
(t/is (= "Satoshi" (:font-family entry)))
|
||||
(t/is (= 400 (:font-weight entry)))
|
||||
(t/is (= "normal" (:font-style entry)))
|
||||
(t/is (false? (:height-warning? entry)))))
|
||||
|
||||
(t/deftest prepare-font-tolerates-missing-tables
|
||||
(let [font (fake-ot-font {:family "Satoshi" :variant "Regular" :hhea nil :os2 nil})
|
||||
entry (#'df/prepare-font {:font font
|
||||
:type "font/ttf"
|
||||
:name "Satoshi-Regular.ttf"
|
||||
:data (bytes->buffer [0 1 0 0 0 0 0 0])})]
|
||||
(t/is (= "Satoshi" (:font-family entry)))
|
||||
(t/is (false? (:height-warning? entry)))))
|
||||
|
||||
(t/deftest parse-font-returns-error-on-unparseable
|
||||
(t/testing "read errors pass through"
|
||||
(t/is (= {:error "x.ttf"} (#'df/parse-font {:error "x.ttf"}))))
|
||||
(t/testing "woff2 keeps a nil font for filename-based metadata"
|
||||
(let [result (#'df/parse-font {:data (bytes->buffer [119 79 70 50])
|
||||
:type "font/woff2"
|
||||
:name "Satoshi-Bold.woff2"})]
|
||||
(t/is (nil? (:font result)))
|
||||
(t/is (nil? (:error result)))))
|
||||
(t/testing "opentype.js failures become errors instead of silent drops"
|
||||
(let [garbage (bytes->buffer (into [0 1 0 0] (repeat 96 0)))
|
||||
result (#'df/parse-font {:data garbage :type "font/ttf" :name "bad.ttf"})]
|
||||
(t/is (= "bad.ttf" (:error result))))))
|
||||
|
||||
(t/deftest ^:async process-upload-reports-unknown-signature
|
||||
(let [team-id (uuid/next)
|
||||
blob #js {:name "mystery.bin"}
|
||||
emitted (atom [])
|
||||
result (atom nil)]
|
||||
(await (mock/with-mocks*
|
||||
{wa/read-file-as-array-buffer (mock/stub (fn [_] (rx/of (bytes->buffer [1 2 3 4]))))
|
||||
st/emit! (mock/stub (fn [& evts] (swap! emitted into evts) nil))}
|
||||
(await (async/observe (df/process-upload [blob] team-id)
|
||||
:on-next #(reset! result %)))))
|
||||
(t/is (= {} @result))
|
||||
(t/is (= 1 (count @emitted)))
|
||||
(t/is (= :app.main.data.notifications/show (ptk/type (first @emitted))))))
|
||||
|
||||
(t/deftest ^:async process-upload-woff2-succeeds-from-filename
|
||||
(let [team-id (uuid/next)
|
||||
blob #js {:name "Satoshi-Bold.woff2"}
|
||||
emitted (atom [])
|
||||
result (atom nil)]
|
||||
(await (mock/with-mocks*
|
||||
{wa/read-file-as-array-buffer (mock/stub (fn [_] (rx/of (bytes->buffer [119 79 70 50 0 0 0 0]))))
|
||||
st/emit! (mock/stub (fn [& evts] (swap! emitted into evts) nil))}
|
||||
(await (async/observe (df/process-upload [blob] team-id)
|
||||
:on-next #(reset! result %)))
|
||||
(await (async/settle))))
|
||||
(t/is (= 1 (count (vals @result))))
|
||||
(let [entry (first (vals @result))]
|
||||
(t/is (= "Satoshi" (:font-family entry)))
|
||||
(t/is (= 700 (:font-weight entry)))
|
||||
(t/is (= "normal" (:font-style entry))))
|
||||
(t/is (empty? @emitted))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user