mirror of
https://github.com/penpot/penpot.git
synced 2026-11-15 06:31:02 +00:00
* 🐛 Fix font upload crash and preserve variant metadata opentype.js 2.0.0 returns no fontSubfamily name for fonts whose Macintosh name table only holds custom records. That made parse-font-weight/parse-font-style call re-seq on nil and crash the upload. A nil or blank variant is now treated as no match in app.common.media, defaulting to weight 400 and style normal. The variant parsing also accepts oblique as italic, and the new parse-font-family helper derives the family from a filename by stripping the known weight/style tokens, separating a style token glued to a weight token ("Roboto-BoldItalic" -> "Roboto"). first-nonblank-string returns the first nonblank candidate, so a blank preferredFamily/preferredSubfamily no longer shadows a valid fontFamily/fontSubfamily. app.main.data.fonts gains resolve-font-metadata and prepare-font-entry. opentype.js values win per field; any missing value falls back to the filename-derived family, weight and style. The parsed and woff2 branches share the same helper, so metadata is preserved instead of collapsing every face to weight 400, and fonts with missing vertical-metric tables upload with no height warning instead of failing. process-upload reports per-file failures (unknown signature, unparseable file, preparation error) with a single bad-font notification instead of silently dropping them, and never errors for per-file failures. The upload now sends :variant-name to create-font-variant, which accepts it as an optional param (ships in 2.20), so the foundry name survives the upload. Closes #10436 AI-assisted-by: deepseek-v4.1-flash AI-assisted-by: muse-spark-1.3-contributor Signed-off-by: CHOI JUNSOO <junsoo1172@gmail.com> * 🐛 Parse glued font style tokens in the font parsers EvaMarco's review on #11951: parse-font-family and parse-font-weight handled glued tokens like "BoldItalic", but parse-font-style did not. For names such as "opensans-bolditalic-webfont.woff2" the three parsers disagreed: the italic face resolved to style normal and collided with the bold-only file on the (family, weight, style) join key. One binary silently overwrote the other during upload, losing one font face without any warning. A new private normalize-font-name helper is applied by all three parsers. It splits a style token glued to a weight token, now with a lookahead boundary so "LightItalic_v2" splits too, and strips a trailing technical suffix (-VF or _v<number>). parse-font-family, parse-font-weight and parse-font-style now interpret the name consistently. Tests cover the three cases from the review, the bold-only control case, and an end-to-end process-upload check that a bold and a bold-italic WOFF2 survive as two variants. Relates to #10436 and PR #11951. AI-assisted-by: inkling-2-small-free * Remove duplicate frontend test registration --------- Signed-off-by: CHOI JUNSOO <junsoo1172@gmail.com>
159 lines
6.5 KiB
Clojure
159 lines
6.5 KiB
Clojure
;; 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 SUBSIDIARY SL
|
|
|
|
(ns frontend-tests.data.fonts-test
|
|
"Tests for the upload metadata resolution in app.main.data.fonts."
|
|
(:require
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.fonts :as df]
|
|
[app.main.data.uploads :as uploads]
|
|
[app.main.repo :as rp]
|
|
[beicon.v2.core :as rx]
|
|
[cljs.test :as t :include-macros true]
|
|
[frontend-tests.helpers.async :as async]
|
|
[frontend-tests.helpers.mock :as mock]))
|
|
|
|
(t/deftest resolve-font-metadata-prefers-opentype-values
|
|
(t/testing "opentype.js values win over the filename"
|
|
(t/is (= {:font-family "Inter"
|
|
:font-weight 700
|
|
:font-style "normal"
|
|
:variant-name "Bold"}
|
|
(df/resolve-font-metadata {:family "Inter" :variant "Bold"}
|
|
"Inter.ttf")))))
|
|
|
|
(t/deftest resolve-font-metadata-falls-back-to-filename
|
|
(t/testing "derives weight and style from the filename when the variant is missing"
|
|
(t/is (= {:font-family "Pretendard"
|
|
:font-weight 600
|
|
:font-style "normal"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {:family "Pretendard" :variant nil}
|
|
"Pretendard-SemiBold.otf"))))
|
|
|
|
(t/testing "derives weight and style from the filename when the variant is blank"
|
|
(t/is (= {:font-family "Lato"
|
|
:font-weight 300
|
|
:font-style "normal"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {:family "Lato" :variant ""}
|
|
"Lato-Light.ttf"))))
|
|
|
|
(t/testing "derives the family from the filename when it is missing"
|
|
(t/is (= {:font-family "Blender Inter"
|
|
:font-weight 700
|
|
:font-style "normal"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {:family nil :variant nil}
|
|
"Blender-Inter-Bold.ttf"))))
|
|
|
|
(t/testing "derives family, weight and style from the filename when nothing is available"
|
|
(t/is (= {:font-family "Pretendard"
|
|
:font-weight 600
|
|
:font-style "normal"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {} "Pretendard-SemiBold.woff2"))))
|
|
|
|
(t/testing "derives italic from the filename"
|
|
(t/is (= {:font-family "Roboto"
|
|
:font-weight 400
|
|
:font-style "italic"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {} "Roboto-Italic.ttf"))))
|
|
|
|
(t/testing "separates a style token glued to a weight token"
|
|
(t/is (= {:font-family "Roboto"
|
|
:font-weight 700
|
|
:font-style "italic"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {:family "Roboto" :variant nil}
|
|
"Roboto-BoldItalic.ttf"))))
|
|
|
|
(t/testing "treats an oblique variant as italic"
|
|
(t/is (= {:font-family "Roboto"
|
|
:font-weight 700
|
|
:font-style "italic"
|
|
:variant-name "BoldOblique"}
|
|
(df/resolve-font-metadata {:family "Roboto" :variant "BoldOblique"}
|
|
"Roboto.ttf"))))
|
|
|
|
(t/testing "handles filenames without extension"
|
|
(t/is (= {:font-family "Lato"
|
|
:font-weight 300
|
|
:font-style "normal"
|
|
:variant-name nil}
|
|
(df/resolve-font-metadata {} "Lato-Light")))))
|
|
|
|
(t/deftest ^:async upload-font-variant-sends-variant-name
|
|
(await
|
|
(mock/with-mocks*
|
|
{rp/cmd! mock/rpc-cmd-mock
|
|
uploads/upload-blob-chunked
|
|
(fn [_blob & _opts] (rx/of {:session-id (uuid/next)}))}
|
|
(let [item {:data {"font/ttf" (js/Uint8Array. 4)}
|
|
:team-id (uuid/next)
|
|
:font-id (uuid/next)
|
|
:font-family "Roboto"
|
|
:font-weight 700
|
|
:font-style "italic"
|
|
:variant-name "BoldOblique"}]
|
|
(await (async/->promise (df/upload-font-variant item)))
|
|
(let [{:keys [cmd params]} (first @mock/rpc-calls)]
|
|
(t/is (= :create-font-variant cmd))
|
|
(t/is (= "BoldOblique" (:variant-name params)))))
|
|
(await (async/settle)))))
|
|
|
|
(defn- fake-font
|
|
"A minimal opentype.js-like font object exposing only what
|
|
prepare-font-entry reads."
|
|
[names]
|
|
#js {:getEnglishName (fn [k] (get names k))
|
|
:tables #js {:hhea #js {:ascender 800 :descender -200}
|
|
:os2 #js {:usWinAscent 800
|
|
:usWinDescent 200
|
|
:sTypoAscender 800
|
|
:sTypoDescender -200
|
|
:fsSelection 0}}})
|
|
|
|
(t/deftest prepare-font-entry-falls-back-when-metadata-is-missing
|
|
(t/testing "parsed font with a missing subfamily"
|
|
(let [result (df/prepare-font-entry
|
|
{:font (fake-font {"preferredFamily" "Pretendard"})
|
|
:type "font/ttf"
|
|
:name "Pretendard-SemiBold.otf"
|
|
:data (js/Uint8Array. 4)})]
|
|
(t/is (= "Pretendard" (:font-family result)))
|
|
(t/is (= 600 (:font-weight result)))
|
|
(t/is (= "normal" (:font-style result)))
|
|
(t/is (nil? (:variant-name result)))
|
|
(t/is (false? (:height-warning? result)))
|
|
(t/is (= {:name "Pretendard-SemiBold.otf" :type "font/ttf"}
|
|
(select-keys (:content result) [:name :type])))))
|
|
|
|
(t/testing "unparseable woff2 entry is resolved from the filename"
|
|
(let [result (df/prepare-font-entry
|
|
{:font nil
|
|
:type "font/woff2"
|
|
:name "Pretendard-SemiBold.woff2"
|
|
:data (js/Uint8Array. 4)})]
|
|
(t/is (= "Pretendard" (:font-family result)))
|
|
(t/is (= 600 (:font-weight result)))
|
|
(t/is (= "normal" (:font-style result)))
|
|
(t/is (false? (:height-warning? result)))))
|
|
|
|
(t/testing "a blank first candidate does not shadow a valid second one"
|
|
(let [result (df/prepare-font-entry
|
|
{:font (fake-font {"preferredFamily" " "
|
|
"preferredSubfamily" " "
|
|
"fontSubfamily" "Bold"})
|
|
:type "font/ttf"
|
|
:name "Roboto-Light.ttf"
|
|
:data (js/Uint8Array. 4)})]
|
|
(t/is (= "Roboto" (:font-family result)))
|
|
(t/is (= 700 (:font-weight result)))
|
|
(t/is (= "normal" (:font-style result)))
|
|
(t/is (= "Bold" (:variant-name result))))))
|