From 273a968c62110e6068fc572450456f4acf6bcda3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 28 Sep 2026 13:28:28 +0000 Subject: [PATCH] :bug: 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 --- backend/src/app/rpc/commands/fonts.clj | 4 +- backend/test/backend_tests/rpc_font_test.clj | 45 +++++ common/src/app/common/media.cljc | 57 +++++-- common/test/common_tests/media_test.cljc | 37 ++++ frontend/src/app/main/data/fonts.cljs | 160 +++++++++--------- .../test/frontend_tests/data/fonts_test.cljs | 81 ++++++++- 6 files changed, 285 insertions(+), 99 deletions(-) diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index a35f4e7ce7..27d26e14bd 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -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 diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index 90dabf62a0..21cebb78f9 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -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 ;; ----------------------------------------------------------------------- diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index 63615e3309..05b072047d 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -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"))) diff --git a/common/test/common_tests/media_test.cljc b/common/test/common_tests/media_test.cljc index 7cea9644e1..8b02d87318 100644 --- a/common/test/common_tests/media_test.cljc +++ b/common/test/common_tests/media_test.cljc @@ -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")) diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index fc6cd2d0e0..ac1d34897e 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -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 diff --git a/frontend/test/frontend_tests/data/fonts_test.cljs b/frontend/test/frontend_tests/data/fonts_test.cljs index 29bc2b59d4..d8ae34e57b 100644 --- a/frontend/test/frontend_tests/data/fonts_test.cljs +++ b/frontend/test/frontend_tests/data/fonts_test.cljs @@ -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))))))