diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index a1253dc65..2ca686f50 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -13,6 +13,8 @@ [app.common.spec :as us] [app.rlimits :as rlm] [app.rpc.queries.svg :as svg] + [buddy.core.bytes :as bb] + [buddy.core.codecs :as bc] [clojure.java.io :as io] [clojure.java.shell :as sh] [clojure.spec.alpha :as s] @@ -64,7 +66,8 @@ (defmethod process-error :default [error] - (ex/raise :type :internal :cause error)) + (ex/raise :type :internal + :cause error)) (defn run [{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}] @@ -232,6 +235,19 @@ (fs/slurp-bytes output-file)))) + (otf->ttf [data] + (let [input-file (fs/create-tempfile :prefix "penpot") + output-file (fs/path (str input-file ".ttf")) + _ (with-open [out (io/output-stream input-file)] + (IOUtils/writeChunked ^bytes data ^OutputStream out) + (.flush ^OutputStream out)) + res (sh/sh "fontforge" "-lang=ff" "-c" + (str/fmt "Open('%s'); Generate('%s')" + (str input-file) + (str output-file)))] + (when (zero? (:exit res)) + (fs/slurp-bytes output-file)))) + (ttf-or-otf->woff [data] (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") output-file (fs/path (str input-file ".woff")) @@ -250,17 +266,68 @@ (.flush ^OutputStream out)) res (sh/sh "woff2_compress" (str input-file))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file))))] + (fs/slurp-bytes output-file)))) + + (woff->sfnt [data] + (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") + _ (with-open [out (io/output-stream input-file)] + (IOUtils/writeChunked ^bytes data ^OutputStream out) + (.flush ^OutputStream out)) + res (sh/sh "woff2sfnt" (str input-file) + :out-enc :bytes)] + (when (zero? (:exit res)) + (:out res)))) + + ;; Documented here: + ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory + (get-sfnt-type [data] + (let [buff (bb/slice data 0 4) + type (bc/bytes->hex buff)] + (case type + "4f54544f" :otf + "00010000" :ttf + (ex/raise :type :internal + :code :unexpected-data + :hint "unexpected font data")))) + + (gen-if-nil [val factory] + (if (nil? val) + (factory) + val))] (let [current (into #{} (keys input))] - (if (contains? current "font/ttf") - (-> input - (assoc "font/otf" (ttf->otf (get input "font/ttf"))) - (assoc "font/woff" (ttf-or-otf->woff (get input "font/ttf"))) - (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/ttf")))) + (cond + (contains? current "font/ttf") + (let [data (get input "font/ttf")] + (-> input + (update "font/otf" gen-if-nil #(ttf->otf data)) + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) - (-> input - ;; TODO: pending to implement - ;; (assoc "font/ttf" (otf->ttf (get input "font/ttf"))) - (assoc "font/woff" (ttf-or-otf->woff (get input "font/otf"))) - (assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/otf")))))))) + (contains? current "font/otf") + (let [data (get input "font/otf")] + (-> input + (update "font/woff" gen-if-nil #(ttf-or-otf->woff data)) + (assoc "font/ttf" (otf->ttf data)) + (assoc "font/woff2" (ttf-or-otf->woff2 data)))) + + (contains? current "font/woff") + (let [data (get input "font/woff") + sfnt (woff->sfnt data)] + (when-not sfnt + (ex/raise :type :validation + :code :invalid-woff-file + :hint "invalid woff file")) + (let [stype (get-sfnt-type sfnt)] + (cond-> input + true + (-> (assoc "font/woff" data) + (assoc "font/woff2" (ttf-or-otf->woff2 sfnt))) + + (= stype :otf) + (-> (assoc "font/otf" sfnt) + (assoc "font/ttf" (otf->ttf sfnt))) + + (= stype :ttf) + (-> (assoc "font/otf" (ttf->otf sfnt)) + (assoc "font/ttf" sfnt))))))))) diff --git a/backend/tests/app/tests/_files/font-1.otf b/backend/tests/app/tests/_files/font-1.otf new file mode 100644 index 000000000..9326ec784 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.otf differ diff --git a/backend/tests/app/tests/_files/font-1.ttf b/backend/tests/app/tests/_files/font-1.ttf new file mode 100644 index 000000000..cb2f33597 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.ttf differ diff --git a/backend/tests/app/tests/_files/font-1.woff b/backend/tests/app/tests/_files/font-1.woff new file mode 100644 index 000000000..9607e1e19 Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.woff differ diff --git a/backend/tests/app/tests/_files/font-2.otf b/backend/tests/app/tests/_files/font-2.otf new file mode 100644 index 000000000..2fbddd00e Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.otf differ diff --git a/backend/tests/app/tests/_files/font-2.woff b/backend/tests/app/tests/_files/font-2.woff new file mode 100644 index 000000000..4edf9e60e Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.woff differ diff --git a/backend/tests/app/tests/test_services_fonts.clj b/backend/tests/app/tests/test_services_fonts.clj new file mode 100644 index 000000000..50ee24abf --- /dev/null +++ b/backend/tests/app/tests/test_services_fonts.clj @@ -0,0 +1,92 @@ +;; 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) UXBOX Labs SL + +(ns app.tests.test-services-fonts + (:require + [app.common.uuid :as uuid] + [app.db :as db] + [app.http :as http] + [app.storage :as sto] + [app.tests.helpers :as th] + [clojure.java.io :as io] + [clojure.test :as t] + [datoteka.core :as fs])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest ttf-font-upload-1 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + + ttfdata (-> (io/resource "app/tests/_files/font-1.ttf") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id "custom-somefont" + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/ttf" ttfdata}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + +(t/deftest ttf-font-upload-2 + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + + data (-> (io/resource "app/tests/_files/font-1.woff") + (fs/slurp-bytes)) + + params {::th/type :create-font-variant + :profile-id (:id prof) + :team-id team-id + :font-id "custom-somefont" + :font-family "somefont" + :font-weight 400 + :font-style "normal" + :data {"font/woff" data}} + out (th/mutation! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (uuid? (:id result))) + (t/is (uuid? (:ttf-file-id result))) + (t/is (uuid? (:otf-file-id result))) + (t/is (uuid? (:woff1-file-id result))) + (t/is (uuid? (:woff2-file-id result))) + (t/are [k] (= (get params k) + (get result k)) + :team-id + :font-id + :font-family + :font-weight + :font-style)))) + + + + +