mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
🐛 Fix font family parsing and persist variant name
Address the review findings on the earlier font upload fix. parse-font-family now lives in app.common.media and separates a style token glued to a weight token, so "Roboto-BoldItalic" yields family "Roboto". oblique is recognized as italic in parse-font-style and in the weight lookahead, avoiding a collision with the regular face. The upload now sends :variant-name to create-font-variant, which accepts it as an optional param, so the foundry name survives the upload. prepare-font-entry is extracted and covered by a regression test for the parsed and woff2 branches. Closes #10436 AI-assisted-by: deepseek-v4.1-flash
This commit is contained in:
parent
6999b7e501
commit
273a968c62
@ -112,6 +112,7 @@
|
||||
[:font-family types.font/schema:font-family]
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]
|
||||
[:variant-name {:optional true} [:maybe [::sm/text {:max 250}]]]
|
||||
[:uploads [:map-of ::sm/text ::sm/uuid]]])
|
||||
|
||||
(defn- prepare-font-data-from-uploads
|
||||
@ -139,7 +140,8 @@
|
||||
chunked-upload API)."
|
||||
{::doc/added "1.18"
|
||||
::doc/changes [["2.16" "Add :uploads param for chunked upload support"]
|
||||
["2.18" "Remove :data param, use :uploads exclusively"]]
|
||||
["2.18" "Remove :data param, use :uploads exclusively"]
|
||||
["2.19" "Add optional :variant-name param"]]
|
||||
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
||||
[:process-font/global]]
|
||||
::webhooks/event? true
|
||||
|
||||
@ -403,6 +403,51 @@
|
||||
(t/is (nil? (:error out)))
|
||||
(assert-font-variant-result params (:result out)))))
|
||||
|
||||
(t/deftest create-font-variant-persists-variant-name
|
||||
"The optional :variant-name is stored and returned by get-font-variants."
|
||||
(with-mocks [_mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
font-id (uuid/custom 10 34)
|
||||
font-bytes (-> (io/resource "backend_tests/test_files/font-1.ttf") (io/read*))
|
||||
session-id (upload-font-chunked! prof font-bytes "font/ttf" (* 4 1024 1024))
|
||||
out (th/command! {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "variant-name-font"
|
||||
:font-weight 600
|
||||
:font-style "normal"
|
||||
:variant-name "SemiBold"
|
||||
:uploads {"font/ttf" session-id}})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= "SemiBold" (-> out :result :variant-name)))
|
||||
|
||||
(let [variants (-> (th/command! {::th/type :get-font-variants
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id})
|
||||
:result)
|
||||
variant (first (filter #(= font-id (:font-id %)) variants))]
|
||||
(t/is (= "SemiBold" (:variant-name variant)))))
|
||||
|
||||
;; An explicit nil must be accepted (the param is optional).
|
||||
(let [prof (th/create-profile* 2 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
font-id (uuid/custom 10 35)
|
||||
font-bytes (-> (io/resource "backend_tests/test_files/font-1.ttf") (io/read*))
|
||||
session-id (upload-font-chunked! prof font-bytes "font/ttf" (* 4 1024 1024))
|
||||
out (th/command! {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "variant-name-nil"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:variant-name nil
|
||||
:uploads {"font/ttf" session-id}})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (nil? (-> out :result :variant-name))))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Error cases
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
@ -81,27 +81,58 @@
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
(str/replace filename image-extensions-re "")))
|
||||
|
||||
(def ^:private font-variant-tokens
|
||||
"Weight and style tokens stripped from a font name to derive its family.
|
||||
Keep the alternatives in sync with `parse-font-weight` and
|
||||
`parse-font-style`."
|
||||
["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" "oblique"])
|
||||
|
||||
(def ^:private font-variant-tokens-re
|
||||
;; The trailing boundary is a lookahead, so two tokens separated by a single
|
||||
;; space are both stripped in one pass (the separator is not consumed).
|
||||
(re-pattern (str "(?i)(^|[-_\\s])(" (str/join "|" font-variant-tokens) ")(?=[-_\\s]|$)")))
|
||||
|
||||
(def ^:private font-concatenated-style-re
|
||||
;; Separate a style token glued to a weight token, e.g. "BoldItalic" ->
|
||||
;; "Bold Italic". Only a known weight token followed by a style token is
|
||||
;; split, so families such as "OpenSans" are left untouched.
|
||||
#"(?i)(black|bold|heavy|solid|medium|light|thin|hairline|regular|normal)(italic|oblique)\b")
|
||||
|
||||
(defn parse-font-family
|
||||
"Derive a font family name from a base name (a filename without its
|
||||
extension), stripping the known weight/style tokens."
|
||||
[base-name]
|
||||
(let [normalized (str/replace base-name font-concatenated-style-re "$1 $2")
|
||||
stripped (-> normalized
|
||||
(str/replace font-variant-tokens-re "$1")
|
||||
(str/replace #"[-_\s]+" " ")
|
||||
(str/trim))]
|
||||
(if (str/blank? stripped) base-name stripped)))
|
||||
|
||||
(defn parse-font-weight
|
||||
[variant]
|
||||
(let [variant (or 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)))
|
||||
(re-seq #"(?i)(?:^|[-_\s])(hairline|thin)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 100
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*light|ultra\s*light)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 200
|
||||
(re-seq #"(?i)(?:^|[-_\s])(light)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 300
|
||||
(re-seq #"(?i)(?:^|[-_\s])(normal|regular)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 400
|
||||
(re-seq #"(?i)(?:^|[-_\s])(medium)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 500
|
||||
(re-seq #"(?i)(?:^|[-_\s])(semi\s*bold|demi\s*bold)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 600
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*bold|ultra\s*bold)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 800
|
||||
(re-seq #"(?i)(?:^|[-_\s])(bold)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 700
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*black|ultra\s*black)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 950
|
||||
(re-seq #"(?i)(?:^|[-_\s])(black|heavy|solid)(?=(?:[-_\s]|$|italic\b|oblique\b))" variant) 900
|
||||
:else 400)))
|
||||
|
||||
(defn parse-font-style
|
||||
[variant]
|
||||
(let [variant (or variant "")]
|
||||
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic)(?:[-_\s]|$)" variant)
|
||||
(re-seq #"(?i)italic$" variant))
|
||||
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic|oblique)(?:[-_\s]|$)" variant)
|
||||
(re-seq #"(?i)(?:italic|oblique)$" variant))
|
||||
"italic"
|
||||
"normal")))
|
||||
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
(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 "handles nil and blank variants safely"
|
||||
(t/is (= 400 (media/parse-font-weight nil)))
|
||||
(t/is (= 400 (media/parse-font-weight "")))
|
||||
@ -47,11 +52,43 @@
|
||||
(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 "handles nil and blank variants safely"
|
||||
(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-strip-image-extension
|
||||
(t/testing "removes extension from supported image files"
|
||||
(t/is (= (media/strip-image-extension "foo.png") "foo"))
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
can assemble the chunks and materialise the final font-variant record.
|
||||
|
||||
Returns an observable that emits the created font-variant."
|
||||
[{:keys [data team-id font-id font-family font-weight font-style] :as _item}]
|
||||
[{:keys [data team-id font-id font-family font-weight font-style variant-name] :as _item}]
|
||||
;; Upload each mtype as a separate chunked session in parallel, collect
|
||||
;; all [mtype session-id] pairs, then call create-font-variant with :uploads.
|
||||
(->> (rx/from (seq data))
|
||||
@ -108,47 +108,90 @@
|
||||
{})
|
||||
(rx/mapcat (fn [uploads]
|
||||
(rp/cmd! :create-font-variant
|
||||
{:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family font-family
|
||||
:font-weight font-weight
|
||||
:font-style font-style
|
||||
:uploads uploads})))))
|
||||
|
||||
(def ^:private font-variant-tokens-re
|
||||
;; Strip known weight/style tokens and separators to derive the family name.
|
||||
;; Use word boundaries to avoid matching substrings (e.g. "Boldini" must not
|
||||
;; match "bold").
|
||||
#"(?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]|$)")
|
||||
|
||||
(defn- font-family-from-name
|
||||
"Derive a font family name from a font filename, stripping the known
|
||||
weight/style tokens and separators."
|
||||
[name]
|
||||
(let [base-name (str/replace name #"\.[^.]+$" "")
|
||||
raw-family-name (-> base-name
|
||||
(str/replace font-variant-tokens-re "$1$3")
|
||||
(str/replace #"[-_\s]+" " ")
|
||||
(str/trim))]
|
||||
(if (str/blank? raw-family-name) base-name raw-family-name)))
|
||||
{:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family font-family
|
||||
:font-weight font-weight
|
||||
:font-style font-style
|
||||
:variant-name variant-name
|
||||
:uploads uploads})))))
|
||||
|
||||
(defn resolve-font-metadata
|
||||
"Resolve the upload metadata for a font file.
|
||||
|
||||
`names` holds the family and variant names read by opentype.js; each
|
||||
may be nil when the font has no usable name records. `filename` is the
|
||||
original file name. The opentype.js values win per field; any missing
|
||||
value is derived from the filename instead."
|
||||
[{:keys [family variant]} filename]
|
||||
(let [base-name (str/replace filename #"\.[^.]+$" "")
|
||||
`names` is a map carrying the `:family` and `:variant` values read by
|
||||
opentype.js; each may be nil when the font has no usable name records.
|
||||
`filename` is the original file name. The opentype.js values win per
|
||||
field; any missing value is derived from the filename instead."
|
||||
[names filename]
|
||||
(let [{:keys [family variant]} names
|
||||
base-name (str/replace filename #"\.[^.]+$" "")
|
||||
variant (not-empty (str/trim (or variant "")))
|
||||
family (not-empty (str/trim (or family "")))
|
||||
variant-source (or variant base-name)]
|
||||
{:font-family (or family (font-family-from-name filename))
|
||||
{:font-family (or family (cm/parse-font-family base-name))
|
||||
:font-weight (cm/parse-font-weight variant-source)
|
||||
:font-style (cm/parse-font-style variant-source)
|
||||
:variant-name variant}))
|
||||
|
||||
(defn prepare-font-entry
|
||||
"Prepare a single read font entry for upload.
|
||||
|
||||
`:font` is the object parsed by opentype.js (nil for woff2, which cannot
|
||||
be parsed); `:name`, `:type` and `:data` come from reading the file.
|
||||
Returns the entry metadata, the `:content` to upload and, for parsed
|
||||
fonts, the `:height-warning?` flag."
|
||||
[{: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)
|
||||
metadata (resolve-font-metadata {:family family :variant variant} name)]
|
||||
(assoc metadata
|
||||
:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:height-warning? height-warning?))
|
||||
;; Font could not be parsed (woff2), extract metadata from filename
|
||||
(let [data (js/Uint8Array. data)
|
||||
metadata (resolve-font-metadata {} name)]
|
||||
(assoc metadata
|
||||
:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
: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.
|
||||
@ -158,58 +201,7 @@
|
||||
wrap it in a `Blob` and hand it directly to `upload-blob-chunked`
|
||||
without any intermediate client-side chunking."
|
||||
[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)
|
||||
metadata (resolve-font-metadata {:family family :variant variant} name)]
|
||||
(assoc metadata
|
||||
:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
:height-warning? height-warning?))
|
||||
;; Font could not be parsed (woff2), extract metadata from filename
|
||||
(let [data (js/Uint8Array. data)
|
||||
metadata (resolve-font-metadata {} name)]
|
||||
(assoc metadata
|
||||
:content {:data data
|
||||
:name name
|
||||
:type type}
|
||||
: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
|
||||
@ -281,7 +273,7 @@
|
||||
(rx/filter #(nil? (:error %)))
|
||||
(rx/map parse-font)
|
||||
(rx/filter some?)
|
||||
(rx/map prepare)
|
||||
(rx/map prepare-font-entry)
|
||||
(rx/reduce join {})))))
|
||||
|
||||
(defn- calculate-family-to-id-mapping
|
||||
|
||||
@ -7,8 +7,14 @@
|
||||
(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]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
[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"
|
||||
@ -58,9 +64,82 @@
|
||||
: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))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user