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>
185 lines
9.5 KiB
Clojure
185 lines
9.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 common-tests.media-test
|
|
(:require
|
|
[app.common.media :as media]
|
|
[clojure.test :as t]))
|
|
|
|
(t/deftest test-storage-object-types
|
|
(t/testing "covers every image and font type the system stores"
|
|
(t/is (every? #(contains? media/storage-object-types %) media/image-types))
|
|
(t/is (every? #(contains? media/storage-object-types %) media/font-types))
|
|
(t/is (contains? media/storage-object-types "image/svg+xml"))
|
|
(t/is (contains? media/storage-object-types "application/octet-stream"))))
|
|
|
|
(t/deftest test-parse-font-weight
|
|
(t/testing "matches weight tokens with proper boundaries"
|
|
(t/is (= 700 (media/parse-font-weight "Roboto-Bold")))
|
|
(t/is (= 700 (media/parse-font-weight "Roboto_Bold")))
|
|
(t/is (= 700 (media/parse-font-weight "Roboto Bold")))
|
|
(t/is (= 700 (media/parse-font-weight "Bold")))
|
|
(t/is (= 800 (media/parse-font-weight "Roboto-ExtraBold")))
|
|
(t/is (= 600 (media/parse-font-weight "OpenSans-SemiBold")))
|
|
(t/is (= 300 (media/parse-font-weight "Lato-Light")))
|
|
(t/is (= 100 (media/parse-font-weight "Roboto-Thin")))
|
|
(t/is (= 200 (media/parse-font-weight "Roboto-ExtraLight")))
|
|
(t/is (= 500 (media/parse-font-weight "Roboto-Medium")))
|
|
(t/is (= 900 (media/parse-font-weight "Roboto-Black"))))
|
|
|
|
(t/testing "does not match weight tokens embedded in words"
|
|
(t/is (= 400 (media/parse-font-weight "Boldini")))
|
|
(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/testing "matches a weight glued to a style token"
|
|
(t/is (= 700 (media/parse-font-weight "Roboto-BoldItalic")))
|
|
(t/is (= 700 (media/parse-font-weight "Roboto-BoldOblique")))
|
|
(t/is (= 600 (media/parse-font-weight "Roboto-SemiBoldItalic"))))
|
|
|
|
(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"
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-Italic")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto_Italic")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto Italic")))
|
|
(t/is (= "italic" (media/parse-font-style "Italic")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-BoldItalic"))))
|
|
|
|
(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/testing "treats oblique as italic"
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-Oblique")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-BoldOblique")))
|
|
(t/is (= "italic" (media/parse-font-style "RobotoOblique"))))
|
|
|
|
(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-parse-font-family
|
|
(t/testing "strips weight and style tokens"
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-Bold")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto_Bold")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto Bold")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-Oblique")))
|
|
(t/is (= "OpenSans" (media/parse-font-family "OpenSans-SemiBold")))
|
|
(t/is (= "Lato" (media/parse-font-family "Lato-Light"))))
|
|
|
|
(t/testing "separates a style token glued to a weight token"
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-BoldItalic")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-SemiBoldItalic")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-LightOblique")))
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-BlackOblique"))))
|
|
|
|
(t/testing "does not strip tokens embedded in words"
|
|
(t/is (= "Boldini" (media/parse-font-family "Boldini-Bold")))
|
|
(t/is (= "Lighthaus" (media/parse-font-family "Lighthaus")))
|
|
(t/is (= "Blackwood" (media/parse-font-family "Blackwood")))
|
|
(t/is (= "Mediaeval" (media/parse-font-family "Mediaeval"))))
|
|
|
|
(t/testing "keeps unknown tokens in the family"
|
|
(t/is (= "Satoshi Variable" (media/parse-font-family "Satoshi-Variable"))))
|
|
|
|
(t/testing "falls back to the base name when everything is stripped"
|
|
(t/is (= "BoldItalic" (media/parse-font-family "BoldItalic")))))
|
|
|
|
(t/deftest test-font-glued-tokens
|
|
(t/testing "a glued style token is recognized consistently by all three parsers"
|
|
;; From EvaMarco's review on #11951: without a shared interpretation of
|
|
;; the name, the bold-italic face collided with the bold-only one and
|
|
;; was silently overwritten on upload.
|
|
(t/is (= "opensans webfont" (media/parse-font-family "opensans-bolditalic-webfont")))
|
|
(t/is (= 700 (media/parse-font-weight "opensans-bolditalic-webfont")))
|
|
(t/is (= "italic" (media/parse-font-style "opensans-bolditalic-webfont"))))
|
|
|
|
(t/testing "a glued style token followed by a technical suffix"
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-BoldItalic-VF")))
|
|
(t/is (= 700 (media/parse-font-weight "Roboto-BoldItalic-VF")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-BoldItalic-VF"))))
|
|
|
|
(t/testing "numeric build suffix after a glued style token"
|
|
(t/is (= "Roboto" (media/parse-font-family "Roboto-LightItalic_v2")))
|
|
(t/is (= 300 (media/parse-font-weight "Roboto-LightItalic_v2")))
|
|
(t/is (= "italic" (media/parse-font-style "Roboto-LightItalic_v2"))))
|
|
|
|
(t/testing "the bold-only control case is unchanged"
|
|
(t/is (= "opensans webfont" (media/parse-font-family "opensans-bold-webfont")))
|
|
(t/is (= 700 (media/parse-font-weight "opensans-bold-webfont")))
|
|
(t/is (= "normal" (media/parse-font-style "opensans-bold-webfont")))))
|
|
|
|
(t/deftest test-first-nonblank-string
|
|
(t/testing "returns the first nonblank string"
|
|
(t/is (= "Bold" (media/first-nonblank-string " " "Bold")))
|
|
(t/is (= "Bold" (media/first-nonblank-string "Bold" "Light")))
|
|
(t/is (= "Bold" (media/first-nonblank-string nil "Bold")))
|
|
(t/is (= "Inter" (media/first-nonblank-string "" "Inter"))))
|
|
|
|
(t/testing "returns nil when every candidate is blank or not a string"
|
|
(t/is (nil? (media/first-nonblank-string nil " ")))
|
|
(t/is (nil? (media/first-nonblank-string)))
|
|
(t/is (nil? (media/first-nonblank-string 42 " ")))))
|
|
|
|
(t/deftest test-strip-image-extension
|
|
(t/testing "removes extension from supported image files"
|
|
(t/is (= (media/strip-image-extension "foo.png") "foo"))
|
|
(t/is (= (media/strip-image-extension "foo.webp") "foo"))
|
|
(t/is (= (media/strip-image-extension "foo.jpg") "foo"))
|
|
(t/is (= (media/strip-image-extension "foo.jpeg") "foo"))
|
|
(t/is (= (media/strip-image-extension "foo.svg") "foo"))
|
|
(t/is (= (media/strip-image-extension "foo.gif") "foo")))
|
|
|
|
(t/testing "does not remove extension for unsupported files"
|
|
(t/is (= (media/strip-image-extension "foo.txt") "foo.txt"))
|
|
(t/is (= (media/strip-image-extension "foo.bmp") "foo.bmp")))
|
|
|
|
(t/testing "leaves filename intact when it has no extension"
|
|
(t/is (= (media/strip-image-extension "README") "README"))))
|
|
|
|
(t/deftest test-font-display-variant
|
|
(t/testing "preserves the foundry-supplied variant string verbatim"
|
|
(t/is (= "Thin" (media/font-display-variant "Thin" 100 "normal")))
|
|
(t/is (= "SemiBold" (media/font-display-variant "SemiBold" 600 "normal")))
|
|
(t/is (= "Medium Oblique" (media/font-display-variant "Medium Oblique" 500 "italic")))
|
|
(t/is (= "Ultra" (media/font-display-variant "Ultra" 900 "normal"))))
|
|
|
|
(t/testing "trims surrounding whitespace from upstream variant strings"
|
|
(t/is (= "Bold" (media/font-display-variant " Bold " 700 "normal"))))
|
|
|
|
(t/testing "ignores blank or nil variant strings"
|
|
(t/is (= "Hairline" (media/font-display-variant nil 100 "normal")))
|
|
(t/is (= "Regular" (media/font-display-variant "" 400 "normal")))
|
|
(t/is (= "Bold" (media/font-display-variant " " 700 "normal")))
|
|
(t/is (= "Bold Italic" (media/font-display-variant nil 700 "italic"))))
|
|
|
|
(t/testing "fallback covers every supported numeric weight"
|
|
(t/is (= "Hairline" (media/font-display-variant nil 100 "normal")))
|
|
(t/is (= "Extra Light" (media/font-display-variant nil 200 "normal")))
|
|
(t/is (= "Light" (media/font-display-variant nil 300 "normal")))
|
|
(t/is (= "Regular" (media/font-display-variant nil 400 "normal")))
|
|
(t/is (= "Medium" (media/font-display-variant nil 500 "normal")))
|
|
(t/is (= "Semi Bold" (media/font-display-variant nil 600 "normal")))
|
|
(t/is (= "Bold" (media/font-display-variant nil 700 "normal")))
|
|
(t/is (= "Extra Bold" (media/font-display-variant nil 800 "normal")))
|
|
(t/is (= "Black" (media/font-display-variant nil 900 "normal")))
|
|
(t/is (= "Extra Black" (media/font-display-variant nil 950 "normal"))))
|
|
|
|
(t/testing "italic suffix only applied via the fallback path"
|
|
(t/is (= "Italic" (media/font-display-variant "Italic" 400 "italic")))
|
|
(t/is (= "Regular Italic" (media/font-display-variant nil 400 "italic"))))
|
|
|
|
(t/testing "stored variant survives even when its derived weight disagrees"
|
|
(t/is (= "Ultra" (media/font-display-variant "Ultra" 400 "normal")))))
|