diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index db6dfc45e..9794aa6a9 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -205,107 +205,27 @@ (-> (retrieve-file cfg id) (assoc :permissions perms)))) -(declare trim-file-data) - -(s/def ::page-id ::us/uuid) -(s/def ::object-id ::us/uuid) - -(s/def ::trimmed-file - (s/keys :req-un [::profile-id ::id ::object-id ::page-id])) - -(sv/defmethod ::trimmed-file - "Retrieve a file by its ID and trims all unnecesary content from - it. It is mainly used for rendering a concrete object, so we don't - need force download all shapes when only a small subset is - necesseary." - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] - (let [perms (get-permissions pool profile-id id)] - (check-read-permissions! perms) - (-> (retrieve-file cfg id) - (trim-file-data params) - (assoc :permissions perms)))) - -(defn- trim-file-data - [file {:keys [page-id object-id]}] - (let [page (get-in file [:data :pages-index page-id]) - objects (->> (cph/get-children-with-self (:objects page) object-id) - (map #(dissoc % :thumbnail)) - (d/index-by :id)) - page (assoc page :objects objects)] - (-> file - (update :data assoc :pages-index {page-id page}) - (update :data assoc :pages [page-id])))) - ;; --- FILE THUMBNAIL -(declare strip-frames-with-thumbnails) -(declare extract-file-thumbnail) -(declare get-first-page-data) -(declare get-thumbnail-data) +(defn- trim-objects + "Given the page data and the object-id returns the page data with all + other not needed objects removed from the `:objects` data + structure." + [{:keys [objects] :as page} object-id] + (let [objects (cph/get-children-with-self objects object-id)] + (assoc page :objects (d/index-by :id objects)))) -(s/def ::strip-frames-with-thumbnails ::us/boolean) +(defn- prune-thumbnails + "Given the page data, removes the `:thumbnail` prop from all + shapes." + [page] + (update page :objects (fn [objects] + (d/mapm #(dissoc %2 :thumbnail) objects)))) -(s/def ::page - (s/keys :req-un [::profile-id ::file-id] - :opt-un [::strip-frames-with-thumbnails])) - -(sv/defmethod ::page - "Retrieves the first page of the file. Used mainly for render - thumbnails on dashboard. - - DEPRECATED: still here for backward compatibility." - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] - (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id) - data (get-first-page-data file props)] - data)) - -(s/def ::file-data-for-thumbnail - (s/keys :req-un [::profile-id ::file-id] - :opt-un [::strip-frames-with-thumbnails])) - -(sv/defmethod ::file-data-for-thumbnail - "Retrieves the data for generate the thumbnail of the file. Used mainly for render - thumbnails on dashboard." - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] - (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id)] - {:data (get-thumbnail-data file props) - :file-id file-id - :revn (:revn file)})) - -(defn get-thumbnail-data - [{:keys [data] :as file} props] - (if-let [[page frame] (first - (for [page (-> data :pages-index vals) - frame (-> page :objects cph/get-frames) - :when (:file-thumbnail frame)] - [page frame]))] - (let [objects (->> (cph/get-children-with-self (:objects page) (:id frame)) - (d/index-by :id))] - (cond-> (assoc page :objects objects) - (:strip-frames-with-thumbnails props) - (strip-frames-with-thumbnails) - - :always - (assoc :thumbnail-frame frame))) - - (let [page-id (-> data :pages first)] - (cond-> (get-in data [:pages-index page-id]) - (:strip-frames-with-thumbnails props) - (strip-frames-with-thumbnails))))) - -(defn get-first-page-data - [file props] - (let [page-id (get-in file [:data :pages 0]) - data (cond-> (get-in file [:data :pages-index page-id]) - (true? (:strip-frames-with-thumbnails props)) - (strip-frames-with-thumbnails))] - data)) - -(defn strip-frames-with-thumbnails - "Remove unnecesary shapes from frames that have thumbnail." - [data] +(defn- prune-frames-with-thumbnails + "Remove unnecesary shapes from frames that have thumbnail from page + data." + [page] (let [filter-shape? (fn [objects [id shape]] (let [frame-id (:frame-id shape)] @@ -328,7 +248,71 @@ (filter (partial filter-shape? objects))) objects))] - (update data :objects update-objects))) + (update page :objects update-objects))) + +(defn- get-thumbnail-data + [{:keys [data] :as file}] + (if-let [[page frame] (first + (for [page (-> data :pages-index vals) + frame (-> page :objects cph/get-frames) + :when (:file-thumbnail frame)] + [page frame]))] + (let [objects (->> (cph/get-children-with-self (:objects page) (:id frame)) + (d/index-by :id))] + (-> (assoc page :objects objects) + (assoc :thumbnail-frame frame))) + + (let [page-id (-> data :pages first)] + (-> (get-in data [:pages-index page-id]) + (prune-frames-with-thumbnails))))) + +(s/def ::page-id ::us/uuid) +(s/def ::object-id ::us/uuid) +(s/def ::prune-frames-with-thumbnails ::us/boolean) +(s/def ::prune-thumbnails ::us/boolean) + +(s/def ::page + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::page-id + ::object-id + ::prune-frames-with-thumbnails + ::prune-thumbnails])) + +(sv/defmethod ::page + "Retrieves the page data from file and returns it. If no page-id is + specified, the first page will be returned. If object-id is + specified, only that object and its children will be returned in the + page objects data structure. + + Mainly used for rendering purposes." + [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id] :as props}] + (check-read-permissions! pool profile-id file-id) + (let [file (retrieve-file cfg file-id) + page-id (or page-id (-> file :data :pages first)) + page (get-in file [:data :pages-index page-id])] + + (cond-> page + (:prune-frames-with-thumbnails props) + (prune-frames-with-thumbnails) + + (:prune-thumbnails props) + (prune-thumbnails) + + (uuid? object-id) + (trim-objects object-id)))) + +(s/def ::file-data-for-thumbnail + (s/keys :req-un [::profile-id ::file-id])) + +(sv/defmethod ::file-data-for-thumbnail + "Retrieves the data for generate the thumbnail of the file. Used mainly for render + thumbnails on dashboard. Returns the page data." + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] + (check-read-permissions! pool profile-id file-id) + (let [file (retrieve-file cfg file-id)] + {:page (get-thumbnail-data file) + :file-id file-id + :revn (:revn file)})) ;; --- Query: Shared Library Files diff --git a/common/deps.edn b/common/deps.edn index ebc7812dc..22f7ebbc0 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -22,7 +22,7 @@ java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/promesa {:mvn/version "8.0.450"} - funcool/cuerdas {:mvn/version "2022.01.14-391"} + funcool/cuerdas {:mvn/version "2022.03.27-397"} lambdaisland/uri {:mvn/version "1.13.95" :exclusions [org.clojure/data.json]} diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 857892f1d..af628cb0b 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -13,6 +13,7 @@ #?(:clj [clojure.core :as c] :cljs [cljs.core :as c]) [app.common.data :as d] + [cuerdas.core :as str] [cljs.analyzer.api :as aapi])) (defmacro select-keys @@ -36,61 +37,9 @@ `(let [v# (-> ~target ~@(map (fn [key] (list `c/get key)) keys))] (if (some? v#) v# ~default)))) - -;; => benchmarking: clojure.core/str -;; --> WARM: 100000 -;; --> BENCH: 500000 -;; --> TOTAL: 197.82ms -;; --> MEAN: 395.64ns -;; => benchmarking: app.commons.data.macros/str -;; --> WARM: 100000 -;; --> BENCH: 500000 -;; --> TOTAL: 20.31ms -;; --> MEAN: 40.63ns - (defmacro str - "CLJS only macro variant of `str` function that performs string concat much faster." - ([a] - (if (:ns &env) - (list 'js* "\"\"+~{}" a) - (list `c/str a))) - ([a b] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}" a b) - (list `c/str a b))) - ([a b c] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}" a b c) - (list `c/str a b c))) - ([a b c d] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}+~{}" a b c d) - (list `c/str a b c d))) - ([a b c d e] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}" a b c d e) - (list `c/str a b c d e))) - ([a b c d e f] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f) - (list `c/str a b c d e f))) - ([a b c d e f g] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g) - (list `c/str a b c d e f g))) - ([a b c d e f g h] - (if (:ns &env) - (list 'js* "\"\"+~{}+~{}+~{}+~{}+~{}+~{}+~{}+~{}" a b c d e f g h) - (list `c/str a b c d e f g h))) - ([a b c d e f g h & rest] - (let [all (into [a b c d e f g h] rest)] - (if (:ns &env) - (let [xf (map (fn [items] `(str ~@items))) - pall (partition-all 8 all)] - (if (<= (count all) 64) - `(str ~@(sequence xf pall)) - `(c/str ~@(sequence xf pall)))) - `(c/str ~@all))))) + [& params] + `(str/concat ~@params)) (defmacro export "A helper macro that allows reexport a var in a current namespace." @@ -129,36 +78,6 @@ ;; (.setMacro (var ~n))) ~vr)))) -(defn- interpolate - [s params] - (loop [items (->> (re-seq #"([^\%]+)*(\%(\d+)?)?" s) - (remove (fn [[full seg]] (and (nil? seg) (not full))))) - result [] - index 0] - (if-let [[_ segment var? sidx] (first items)] - (cond - (and var? sidx) - (let [cidx (dec (d/read-string sidx))] - (recur (rest items) - (-> result - (conj segment) - (conj (nth params cidx))) - (inc index))) - - var? - (recur (rest items) - (-> result - (conj segment) - (conj (nth params index))) - (inc index)) - - :else - (recur (rest items) - (conj result segment) - (inc index))) - - (remove nil? result)))) - (defmacro fmt "String interpolation helper. Can only be used with strings known at compile time. Can be used with indexed params access or sequential. @@ -169,7 +88,7 @@ (dm/fmt \"url(%1)\" my-url) ; indexed " [s & params] - (cons 'app.common.data.macros/str (interpolate s (vec params)))) + `(str/ffmt ~s ~@params)) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index d0ce43f5c..3a5fc364f 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -9,7 +9,6 @@ [app.common.exceptions :as ex] [app.common.uuid :as uuid] [app.common.spec :as us] - [clojure.pprint :refer [pprint]] [cuerdas.core :as str] [clojure.spec.alpha :as s] [fipp.edn :as fpp] diff --git a/common/src/app/common/pprint.cljc b/common/src/app/common/pprint.cljc new file mode 100644 index 000000000..e95ad84f6 --- /dev/null +++ b/common/src/app/common/pprint.cljc @@ -0,0 +1,27 @@ +;; 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.common.pprint + (:refer-clojure :exclude [prn]) + (:require + [cuerdas.core :as str] + [fipp.edn :as fpp])) + +(defn pprint-str + [expr] + (binding [*print-level* 8 + *print-length* 25] + (with-out-str + (fpp/pprint expr {:width 110})))) + +(defn pprint + ([expr] + (println (pprint-str expr))) + ([label expr] + (println (str/concat "============ " label "============")) + (pprint expr))) + + diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 0f8f898ac..588b12749 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -56,11 +56,12 @@ (defn screenshot ([frame] (screenshot frame {})) - ([frame {:keys [full-page? omit-background? type quality] + ([frame {:keys [full-page? omit-background? type quality path] :or {type "png" full-page? false omit-background? false quality 95}}] (let [options (-> (obj/new) (obj/set! "type" (name type)) (obj/set! "omitBackground" omit-background?) + (cond-> path (obj/set! "path" path)) (cond-> (= "jpeg" type) (obj/set! "quality" quality)) (cond-> full-page? (-> (obj/set! "fullPage" true) (obj/set! "clip" nil))))] @@ -73,10 +74,10 @@ (defn pdf ([page] (pdf page {})) - ([page {:keys [scale save-path page-ranges] + ([page {:keys [scale path page-ranges] :or {page-ranges "1" scale 1}}] - (.pdf ^js page #js {:path save-path + (.pdf ^js page #js {:path path :scale scale :pageRanges page-ranges :printBackground true diff --git a/exporter/src/app/handlers.cljs b/exporter/src/app/handlers.cljs index ce4870ecc..0d0cab3a3 100644 --- a/exporter/src/app/handlers.cljs +++ b/exporter/src/app/handlers.cljs @@ -6,7 +6,7 @@ (ns app.handlers (:require - [app.common.data.macros :as dm] + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] @@ -32,6 +32,7 @@ (let [explain (us/pretty-explain data) data (-> data (assoc :explain explain) + (assoc :type :validation) (dissoc ::s/problems ::s/value ::s/spec))] (-> exchange (assoc :response/status 400) @@ -46,19 +47,24 @@ (and (= :internal type) (= :browser-not-ready code)) - (-> exchange - (assoc :response/status 503) - (assoc :response/body (t/encode data)) - (assoc :response/headers {"content-type" "application/transit+json"})) + (let [data {:type :server-error + :code :internal + :hint (ex-message error) + :data data}] + (-> exchange + (assoc :response/status 503) + (assoc :response/body (t/encode data)) + (assoc :response/headers {"content-type" "application/transit+json"}))) :else (let [data {:type :server-error + :code type :hint (ex-message error) :data data}] (l/error :hint "unexpected internal error" :cause error) (-> exchange (assoc :response/status 500) - (assoc :response/body (t/encode data)) + (assoc :response/body (t/encode (d/without-nils data))) (assoc :response/headers {"content-type" "application/transit+json"})))))) (defmulti command-spec :cmd) @@ -98,4 +104,4 @@ :export-frames (export-frames/handler exchange params) (ex/raise :type :internal :code :method-not-implemented - :hint (dm/fmt "method % not implemented" cmd))))) + :hint (str/istr "method ~{cmd} not implemented"))))) diff --git a/exporter/src/app/handlers/export_frames.cljs b/exporter/src/app/handlers/export_frames.cljs index 9b1b6d0b2..74ac5ab83 100644 --- a/exporter/src/app/handlers/export_frames.cljs +++ b/exporter/src/app/handlers/export_frames.cljs @@ -7,12 +7,14 @@ (ns app.handlers.export-frames (:require ["path" :as path] - [app.common.data.macros :as dm] - [app.common.exceptions :as exc :include-macros true] + [app.common.logging :as l] + [app.common.exceptions :as exc] [app.common.spec :as us] + [app.common.pprint :as pp] [app.handlers.resources :as rsc] + [app.handlers.export-shapes :refer [prepare-exports]] [app.redis :as redis] - [app.renderer.pdf :as rp] + [app.renderer :as rd] [app.util.shell :as sh] [cljs.spec.alpha :as s] [cuerdas.core :as str] @@ -20,19 +22,17 @@ (declare ^:private handle-export) (declare ^:private create-pdf) -(declare ^:private export-frame) (declare ^:private join-pdf) (declare ^:private move-file) -(declare ^:private clean-tmp) (s/def ::name ::us/string) (s/def ::file-id ::us/uuid) (s/def ::page-id ::us/uuid) -(s/def ::frame-id ::us/uuid) +(s/def ::object-id ::us/uuid) (s/def ::uri ::us/uri) (s/def ::export - (s/keys :req-un [::file-id ::page-id ::frame-id ::name])) + (s/keys :req-un [::file-id ::page-id ::object-id ::name])) (s/def ::exports (s/every ::export :kind vector? :min-count 1)) @@ -42,42 +42,53 @@ :opt-un [::uri ::name])) (defn handler - [{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}] - (let [xform (map #(assoc % :token auth-token :uri uri)) - exports (sequence xform exports)] + [{:keys [:request/auth-token] :as exchange} {:keys [exports uri profile-id] :as params}] + ;; NOTE: we need to have the `:type` prop because the exports + ;; datastructure preparation uses it for creating the groups. + (let [exports (-> (map #(assoc % :type :pdf :scale 1 :suffix "") exports) + (prepare-exports auth-token uri))] (handle-export exchange (assoc params :exports exports)))) (defn handle-export - [exchange {:keys [exports wait uri name] :as params}] - (let [topic (-> exports first :file-id str) + [exchange {:keys [exports wait uri name profile-id] :as params}] + (let [total (count exports) + topic (str profile-id) resource (rsc/create :pdf (or name (-> exports first :name))) - on-progress (fn [progress] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :status "running" - :progress progress}] - (redis/pub! topic data))) + on-progress (fn [{:keys [done]}] + (when-not wait + (let [data {:type :export-update + :resource-id (:id resource) + :name (:name resource) + :filename (:filename resource) + :status "running" + :total total + :done done}] + (redis/pub! topic data)))) - on-complete (fn [resource] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :size (:size resource) - :status "ended"}] - (redis/pub! topic data))) + on-complete (fn [] + (when-not wait + (let [data {:type :export-update + :resource-id (:id resource) + :name (:name resource) + :filename (:filename resource) + :status "ended"}] + (redis/pub! topic data)))) on-error (fn [cause] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :status "error" - :cause (ex-message cause)}] - (redis/pub! topic data))) + (l/error :hint "unexpected error on frames exportation" :cause cause) + (if wait + (p/rejected cause) + (let [data {:type :export-update + :resource-id (:id resource) + :name (:name resource) + :filename (:filename resource) + :status "error" + :cause (ex-message cause)}] + (redis/pub! topic data)))) proc (create-pdf :resource resource - :items exports + :exports exports :on-progress on-progress :on-complete on-complete :on-error on-error)] @@ -86,70 +97,46 @@ (assoc exchange :response/body (dissoc resource :path))))) (defn create-pdf - [& {:keys [resource items on-progress on-complete on-error] - :or {on-progress identity - on-complete identity - on-error identity}}] - (p/let [progress (atom 0) - tmpdir (sh/create-tmpdir! "pdfexport") - file-id (-> items first :file-id) - items (into [] (map #(partial export-frame tmpdir %)) items) - xform (map (fn [export-fn] - #(p/finally - (export-fn) - (fn [result _] - (on-progress {:total (count items) - :done (swap! progress inc) - :name (:name result)})))))] - (-> (reduce (fn [res export-fn] - (p/let [res res - out (export-fn)] - (cons (:path out) res))) - (p/resolved nil) - (into '() xform items)) - (p/then (partial join-pdf tmpdir file-id)) + [& {:keys [resource exports on-progress on-complete on-error] + :or {on-progress (constantly nil) + on-complete (constantly nil) + on-error p/rejected}}] + + (let [file-id (-> exports first :file-id) + result (atom []) + + on-object + (fn [{:keys [path] :as object}] + (let [res (swap! result conj path)] + (on-progress {:done (count res)})))] + + (-> (p/loop [exports (seq exports)] + (when-let [export (first exports)] + (p/let [proc (rd/render export on-object)] + (p/recur (rest exports))))) + + (p/then (fn [_] (deref result))) + (p/then (partial join-pdf file-id)) (p/then (partial move-file resource)) - (p/then (partial clean-tmp tmpdir)) + (p/then (constantly resource)) (p/then (fn [resource] (-> (sh/stat (:path resource)) (p/then #(merge resource %))))) + (p/catch on-error) (p/finally (fn [result cause] - (if cause - (on-error cause) - (on-complete result))))))) - -(defn- export-frame - [tmpdir {:keys [file-id page-id frame-id token uri] :as params}] - (let [file-name (dm/fmt "%.pdf" frame-id) - save-path (path/join tmpdir file-name)] - (-> (rp/render {:name (dm/str frame-id) - :uri uri - :suffix "" - :token token - :file-id file-id - :page-id page-id - :object-id frame-id - :scale 1 - :save-path save-path}) - (p/then (fn [_] - {:name file-name - :path save-path}))))) + (when-not cause + (on-complete))))))) (defn- join-pdf - [tmpdir file-id paths] - (let [output-path (path/join tmpdir (str file-id ".pdf")) - paths-str (str/join " " paths)] - (-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path)) - (p/then (constantly output-path))))) + [file-id paths] + (p/let [tmpdir (sh/mktmpdir! "join-pdf") + path (path/join tmpdir (str/concat file-id ".pdf"))] + (sh/run-cmd! (str "pdfunite " (str/join " " paths) " " path)) + path)) (defn- move-file [{:keys [path] :as resource} output-path] (p/do (sh/move! output-path path) + (sh/rmdir! (path/dirname output-path)) resource)) - -(defn- clean-tmp - [tdpath data] - (p/do! - (sh/rmdir! tdpath) - data)) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index d35a460a7..b70e63cf2 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -6,34 +6,35 @@ (ns app.handlers.export-shapes (:require - [app.common.exceptions :as exc :include-macros true] + ["path" :as path] + [app.common.data :as d] + [app.common.exceptions :as exc] + [app.common.logging :as l] [app.common.spec :as us] - [app.redis :as redis] [app.handlers.resources :as rsc] - [app.renderer.bitmap :as rb] - [app.renderer.pdf :as rp] - [app.renderer.svg :as rs] + [app.redis :as redis] + [app.renderer :as rd] + [app.util.mime :as mime] + [app.util.shell :as sh] [cljs.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p])) -(declare ^:private handle-exports) (declare ^:private handle-single-export) (declare ^:private handle-multiple-export) -(declare ^:private run-export) -(declare ^:private assign-file-name) +(declare ^:private assoc-file-name) +(declare prepare-exports) -(s/def ::name ::us/string) -(s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) +(s/def ::filename ::us/string) +(s/def ::name ::us/string) (s/def ::object-id ::us/uuid) +(s/def ::page-id ::us/uuid) +(s/def ::profile-id ::us/uuid) (s/def ::scale ::us/number) (s/def ::suffix ::us/string) (s/def ::type ::us/keyword) -(s/def ::suffix string?) -(s/def ::scale number?) (s/def ::uri ::us/uri) -(s/def ::profile-id ::us/uuid) (s/def ::wait ::us/boolean) (s/def ::export @@ -47,13 +48,13 @@ :opt-un [::uri ::wait ::name])) (defn handler - [{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}] - (let [xform (comp - (map #(assoc % :token auth-token)) - (assign-file-name)) - exports (into [] xform exports)] - (if (= 1 (count exports)) - (handle-single-export exchange (assoc params :export (first exports))) + [{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}] + (let [exports (prepare-exports exports auth-token uri)] + (if (and (= 1 (count exports)) + (= 1 (count (-> exports first :objects)))) + (handle-single-export exchange (-> params + (assoc :export (first exports)) + (dissoc :exports))) (handle-multiple-export exchange (assoc params :exports exports))))) (defn- handle-single-export @@ -61,87 +62,102 @@ (let [topic (str profile-id) resource (rsc/create (:type export) (or name (:name export))) - on-progress (fn [progress] - (let [data {:type :export-update - :resource-id (:id resource) - :status "running" - :progress progress}] - (redis/pub! topic data))) - - on-complete (fn [resource] - (let [data {:type :export-update - :resource-id (:id resource) - :size (:size resource) - :name (:name resource) - :status "ended"}] - (redis/pub! topic data))) + on-progress (fn [{:keys [path] :as object}] + (p/do + ;; Move the generated path to the resource + ;; path destination. + (sh/move! path (:path resource)) + (when-not wait + (redis/pub! topic {:type :export-update + :resource-id (:id resource) + :status "running" + :total 1 + :done 1}) + (redis/pub! topic {:type :export-update + :resource-id (:id resource) + :filename (:filename resource) + :name (:name resource) + :status "ended"})))) on-error (fn [cause] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :status "error" - :cause (ex-message cause)}] - (redis/pub! topic data))) + (l/error :hint "unexpected error happened on export multiple process" + :cause cause) + (if wait + (p/rejected cause) + (redis/pub! topic {:type :export-update + :resource-id (:id resource) + :status "error" + :cause (ex-message cause)}))) + + proc (-> (rd/render export on-progress) + (p/then (constantly resource)) + (p/catch on-error))] - proc (rsc/create-simple :task #(run-export export) - :resource resource - :on-progress on-progress - :on-error on-error - :on-complete on-complete)] (if wait (p/then proc #(assoc exchange :response/body (dissoc % :path))) (assoc exchange :response/body (dissoc resource :path))))) (defn- handle-multiple-export [exchange {:keys [exports wait uri profile-id name] :as params}] - (let [tasks (map #(fn [] (run-export %)) exports) + (let [resource (rsc/create :zip (or name (-> exports first :name))) + total (count exports) topic (str profile-id) - resource (rsc/create :zip (or name (-> exports first :name))) - on-progress (fn [progress] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :status "running" - :progress progress}] - (redis/pub! topic data))) + to-delete (atom #{}) - on-complete (fn [resource] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :size (:size resource) - :status "ended"}] - (redis/pub! topic data))) + on-progress (fn [{:keys [done]}] + (when-not wait + (let [data {:type :export-update + :resource-id (:id resource) + :status "running" + :total total + :done done}] + (redis/pub! topic data)))) + + on-complete (fn [] + (when-not wait + (let [data {:type :export-update + :name (:name resource) + :filename (:filename resource) + :resource-id (:id resource) + :status "ended"}] + (redis/pub! topic data)))) on-error (fn [cause] - (let [data {:type :export-update - :resource-id (:id resource) - :name (:name resource) - :status "error" - :cause (ex-message cause)}] - (redis/pub! topic data))) + (l/error :hint "unexpected error on multiple exportation" :cause cause) + (if wait + (p/rejected cause) + (redis/pub! topic {:type :export-update + :resource-id (:id resource) + :status "error" + :cause (ex-message cause)}))) - proc (rsc/create-zip :resource resource - :tasks tasks - :on-progress on-progress + zip (rsc/create-zip :resource resource :on-complete on-complete - :on-error on-error)] + :on-error on-error + :on-progress on-progress) + + append (fn [{:keys [filename path] :as object}] + (swap! to-delete conj path) + (rsc/add-to-zip! zip path filename)) + + proc (-> (p/do + (p/loop [exports (seq exports)] + (when-let [export (first exports)] + (p/let [proc (rd/render export append)] + (p/recur (rest exports))))) + (.finalize zip)) + (p/then (fn [_] (p/run! #(sh/rmdir! (path/dirname %)) @to-delete))) + (p/then (constantly resource)) + (p/catch on-error)) + ] + (if wait (p/then proc #(assoc exchange :response/body (dissoc % :path))) (assoc exchange :response/body (dissoc resource :path))))) -(defn- run-export - [{:keys [type] :as params}] - (p/let [res (case type - :png (rb/render params) - :jpeg (rb/render params) - :svg (rs/render params) - :pdf (rp/render params))] - (assoc res :type type))) -(defn- assign-file-name +(defn- assoc-file-name "A transducer that assocs a candidate filename and avoid duplicates." [] (letfn [(find-candidate [params used] @@ -149,12 +165,8 @@ (let [candidate (str (:name params) (:suffix params "") (when (pos? index) - (str "-" (inc index))) - (case (:type params) - :png ".png" - :jpeg ".jpg" - :svg ".svg" - :pdf ".pdf"))] + (str/concat "-" (inc index))) + (mime/get-extension (:type params)))] (if (contains? used candidate) (recur (inc index)) candidate))))] @@ -168,3 +180,37 @@ params (assoc params :filename candidate)] (vswap! used conj candidate) (rf result params)))))))) + +(def ^:const ^:private + default-partition-size 50) + +(defn prepare-exports + [exports token uri] + (letfn [(process-group [group] + (sequence (comp (partition-all default-partition-size) + (map process-partition)) + group)) + + (process-partition [[part1 :as part]] + {:file-id (:file-id part1) + :page-id (:page-id part1) + :name (:name part1) + :token token + :uri uri + :type (:type part1) + :scale (:scale part1) + :objects (mapv part-entry->object part)}) + + (part-entry->object [entry] + {:id (:object-id entry) + :filename (:filename entry) + :name (:name entry) + :suffix (:suffix entry)})] + + (let [xform (comp + (map #(assoc % :token token)) + (assoc-file-name))] + (->> (sequence xform exports) + (d/group-by (juxt :scale :type)) + (map second) + (into [] (mapcat process-group)))))) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index 6fa3ddb1a..e02705610 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -12,104 +12,33 @@ ["os" :as os] ["path" :as path] [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.uuid :as uuid] [app.util.shell :as sh] + [app.util.mime :as mime] [cljs.core :as c] [cuerdas.core :as str] [promesa.core :as p])) (defn- get-path [type id] - (path/join (os/tmpdir) (dm/str "exporter." (d/name type) "." id))) - -(defn- get-mtype - [type] - - (case (d/name type) - "zip" "application/zip" - "pdf" "application/pdf" - "svg" "image/svg+xml" - "jpeg" "image/jpeg" - "png" "image/png")) + (path/join (os/tmpdir) (str/concat "exporter-resource." (c/name type) "." id))) (defn create "Generates ephimeral resource object." [type name] (let [task-id (uuid/next)] - {:path (get-path type task-id) - :mtype (get-mtype type) - :name name - :id (dm/str (c/name type) "." task-id)})) - -(defn- write-as-zip! - [{:keys [id path]} items on-progress] - (let [^js zip (arc/create "zip") - ^js out (fs/createWriteStream path) - append! (fn [{:keys [data name] :as result}] - (.append zip data #js {:name name})) - progress (atom 0)] - (p/create - (fn [resolve reject] - (.on zip "error" #(reject %)) - (.on zip "end" resolve) - (.on zip "entry" (fn [data] - (let [name (unchecked-get data "name") - num (swap! progress inc)] - ;; Sample code used for testing failing exports - #_(when (= 2 num) - (.abort ^js zip) - (reject (js/Error. "unable to create zip file"))) - (on-progress - {:total (count items) - :done num})))) - (.pipe zip out) - (-> (reduce (fn [res export-fn] - (p/then res (fn [_] (-> (export-fn) (p/then append!))))) - (p/resolved 1) - items) - (p/then #(.finalize zip)) - (p/catch reject)))))) - -(defn create-simple - [& {:keys [task resource on-progress on-complete on-error] - :or {on-progress identity - on-complete identity - on-error identity} - :as params}] - (let [path (:path resource)] - (-> (task) - (p/then (fn [{:keys [data name]}] - (on-progress {:total 1 :done 1 :name name}) - (.writeFile fs/promises path data))) - (p/then #(sh/stat path)) - (p/then #(merge resource %)) - (p/finally (fn [result cause] - (if cause - (on-error cause) - (on-complete result))))))) - -(defn create-zip - "Creates a resource with multiple files merget into a single zip file." - [& {:keys [resource tasks on-error on-progress on-complete] - :or {on-error identity - on-progress identity - on-complete identity}}] - (let [{:keys [path id] :as resource} resource] - (-> (write-as-zip! resource tasks on-progress) - (p/then #(sh/stat path)) - (p/then #(merge resource %)) - (p/finally (fn [result cause] - (if cause - (on-error cause) - (on-complete result))))))) + {:path (get-path type task-id) + :mtype (mime/get type) + :name name + :filename (str/concat name (mime/get-extension type)) + :id (str/concat (c/name type) "." task-id)})) (defn- lookup [id] (p/let [[type task-id] (str/split id "." 2) path (get-path type task-id) - mtype (get-mtype type) + mtype (mime/get (keyword type)) stat (sh/stat path)] (when-not stat @@ -131,3 +60,25 @@ (assoc :response/status 200) (assoc :response/body stream) (assoc :response/headers headers)))))) + +(defn create-zip + [& {:keys [resource on-complete on-progress on-error]}] + (let [^js zip (arc/create "zip") + ^js out (fs/createWriteStream (:path resource)) + progress (atom 0)] + (.on zip "error" on-error) + (.on zip "end" on-complete) + (.on zip "entry" (fn [data] + (let [name (unchecked-get data "name") + num (swap! progress inc)] + (on-progress {:done num :filename name})))) + (.pipe zip out) + zip)) + +(defn add-to-zip! + [zip path name] + (.file ^js zip path #js {:name name})) + +(defn close-zip! + [zip] + (.finalize ^js zip)) diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs new file mode 100644 index 000000000..90e03ec68 --- /dev/null +++ b/exporter/src/app/renderer.cljs @@ -0,0 +1,45 @@ +;; 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.renderer + "Common renderer interface." + (:require + [app.common.spec :as us] + [app.renderer.bitmap :as rb] + [app.renderer.pdf :as rp] + [app.renderer.svg :as rs] + [cljs.spec.alpha :as s])) + +(s/def ::name ::us/string) +(s/def ::suffix ::us/string) +(s/def ::type #{:jpeg :png :pdf :svg}) +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::scale ::us/number) +(s/def ::token ::us/string) +(s/def ::uri ::us/uri) +(s/def ::filename ::us/string) + +(s/def ::object + (s/keys :req-un [::id ::name ::suffix ::filename])) + +(s/def ::objects + (s/coll-of ::object :min-count 1)) + +(s/def ::render-params + (s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects] + :opt-un [::uri])) + +(defn- render + [{:keys [type] :as params} on-object] + (us/verify ::render-params params) + (us/verify fn? on-object) + (case type + :png (rb/render params on-object) + :jpeg (rb/render params on-object) + :pdf (rp/render params on-object) + :svg (rs/render params on-object))) + diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 67f4b965f..96dff8bea 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -7,75 +7,61 @@ (ns app.renderer.bitmap "A bitmap renderer." (:require + ["path" :as path] [app.browser :as bw] [app.common.data :as d] - [app.common.exceptions :as ex :include-macros true] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] + [app.util.mime :as mime] + [app.util.shell :as sh] [cljs.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p])) -(defn screenshot-object - [{:keys [file-id page-id object-id token scale type uri]}] - (p/let [params {:file-id file-id - :page-id page-id - :object-id object-id - :route "render-object"} - - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/render.html") - (assoc :query (u/map->query-string params)))] - (bw/exec! - #js {:screen #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :viewport #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :locale "en-US" - :storageState #js {:cookies (bw/create-cookies uri {:token token})} - :deviceScaleFactor scale - :userAgent bw/default-user-agent} - (fn [page] - (l/info :uri uri) - (p/do! - (bw/nav! page (str uri)) - (p/let [node (bw/select page "#screenshot")] - (bw/wait-for node) - (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) - (bw/sleep page 2000) ; the good old fix with sleep - (case type - :png (bw/screenshot node {:omit-background? true :type type}) - :jpeg (bw/screenshot node {:omit-background? false :type type})))))))) - -(s/def ::name ::us/string) -(s/def ::suffix ::us/string) -(s/def ::type #{:jpeg :png}) -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) -(s/def ::scale ::us/number) -(s/def ::token ::us/string) -(s/def ::uri ::us/uri) - -(s/def ::params - (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id] - :opt-un [::uri])) - (defn render - [params] - (us/verify ::params params) - (p/let [content (screenshot-object params)] - {:data content - :name (str (:name params) - (:suffix params "") - (case (:type params) - :png ".png" - :jpeg ".jpg")) - :size (alength content) - :mtype (case (:type params) - :png "image/png" - :jpeg "image/jpeg")})) + [{:keys [file-id page-id token scale type uri objects] :as params} on-object] + (letfn [(prepare-options [uri] + #js {:screen #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :viewport #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :locale "en-US" + :storageState #js {:cookies (bw/create-cookies uri {:token token})} + :deviceScaleFactor scale + :userAgent bw/default-user-agent}) + (render-object [page {:keys [id] :as object}] + (p/let [tmpdir (sh/mktmpdir! "bitmap-render") + path (path/join tmpdir (str/concat id (mime/get-extension type))) + node (bw/select page (str/concat "#screenshot-" id))] + (bw/wait-for node) + (case type + :png (bw/screenshot node {:omit-background? true :type type :path path}) + :jpeg (bw/screenshot node {:omit-background? false :type type :path path})) + (on-object (assoc object :path path)))) + + (render [uri page] + (l/info :uri uri) + (p/do + ;; navigate to the page and perform basic setup + (bw/nav! page (str uri)) + (bw/sleep page 1000) ; the good old fix with sleep + (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) + + ;; take the screnshot of requested objects, one by one + (p/run! (partial render-object page) objects) + nil))] + + (p/let [params {:file-id file-id + :page-id page-id + :object-id (mapv :id objects) + :route "objects"} + uri (-> (or uri (cf/get :public-uri)) + (assoc :path "/render.html") + (assoc :query (u/map->query-string params)))] + (bw/exec! (prepare-options uri) (partial render uri))))) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 3131ce22e..be57229f9 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -7,68 +7,62 @@ (ns app.renderer.pdf "A pdf renderer." (:require + ["path" :as path] [app.browser :as bw] + [app.common.data.macros :as dm] [app.common.exceptions :as ex :include-macros true] [app.common.logging :as l] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] + [app.util.mime :as mime] + [app.util.shell :as sh] + [cuerdas.core :as str] [cljs.spec.alpha :as s] [promesa.core :as p])) -(defn pdf-from-object - [{:keys [file-id page-id object-id token scale type save-path uri] :as params}] - (p/let [params {:file-id file-id - :page-id page-id - :object-id object-id - :route "render-object"} - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/render.html") - (assoc :query (u/map->query-string params)))] - - (bw/exec! - #js {:screen #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :viewport #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :locale "en-US" - :storageState #js {:cookies (bw/create-cookies uri {:token token})} - :deviceScaleFactor scale - :userAgent bw/default-user-agent} - (fn [page] - (l/info :uri uri) - (p/do! - (bw/nav! page uri) - (p/let [dom (bw/select page "#screenshot")] - (bw/wait-for dom) - (bw/screenshot dom {:full-page? true}) - (bw/sleep page 2000) ; the good old fix with sleep - (if save-path - (bw/pdf page {:save-path save-path}) - (bw/pdf page)))))))) - -(s/def ::name ::us/string) -(s/def ::suffix ::us/string) -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) -(s/def ::scale ::us/number) -(s/def ::token ::us/string) -(s/def ::save-path ::us/string) -(s/def ::uri ::us/uri) - -(s/def ::render-params - (s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id] - :opt-un [::save-path ::uri])) - (defn render - [params] - (us/assert ::render-params params) - (p/let [content (pdf-from-object params)] - {:data content - :name (str (:name params) - (:suffix params "") - ".pdf") - :size (alength content) - :mtype "application/pdf"})) + [{:keys [file-id page-id token scale type uri objects] :as params} on-object] + (letfn [(prepare-options [uri] + #js {:screen #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :viewport #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :locale "en-US" + :storageState #js {:cookies (bw/create-cookies uri {:token token})} + :deviceScaleFactor scale + :userAgent bw/default-user-agent}) + (prepare-uri [base-uri object-id] + (let [params {:file-id file-id + :page-id page-id + :object-id object-id + :route "objects"}] + (-> base-uri + (assoc :path "/render.html") + (assoc :query (u/map->query-string params))))) + + (render-object [page base-uri {:keys [id] :as object}] + (p/let [uri (prepare-uri base-uri id) + tmp (sh/mktmpdir! "pdf-render") + path (path/join tmp (str/concat id (mime/get-extension type)))] + (l/info :uri uri) + (bw/nav! page uri) + (p/let [dom (bw/select page (dm/str "#screenshot-" id))] + (bw/wait-for dom) + (bw/screenshot dom {:full-page? true}) + (bw/sleep page 2000) ; the good old fix with sleep + (bw/pdf page {:path path}) + path))) + + (render [base-uri page] + (p/loop [objects (seq objects)] + (when-let [object (first objects)] + (p/let [uri (prepare-uri base-uri (:id object)) + path (render-object page base-uri object)] + (on-object (assoc object :path path)) + (p/recur (rest objects))))))] + + (let [base-uri (or uri (cf/get :public-uri))] + (bw/exec! (prepare-options base-uri) + (partial render base-uri))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 4a965ad4d..9a0b681b0 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -10,12 +10,14 @@ ["xml-js" :as xml] [app.browser :as bw] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex :include-macros true] [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] + [app.util.mime :as mime] [app.util.shell :as sh] [cljs.spec.alpha :as s] [clojure.walk :as walk] @@ -111,9 +113,8 @@ {:width width :height height})) - -(defn- render-object - [{:keys [page-id file-id object-id token scale suffix type uri]}] +(defn render + [{:keys [page-id file-id objects token scale suffix type uri]} on-object] (letfn [(convert-to-ppm [pngpath] (l/trace :fn :convert-to-ppm) (let [basepath (path/dirname pngpath) @@ -246,7 +247,7 @@ (trace-node [{:keys [data] :as node}] (l/trace :fn :trace-node) - (p/let [tdpath (sh/create-tmpdir! "svgexport-") + (p/let [tdpath (sh/mktmpdir! "svgexport") pngpath (path/join tdpath "origin.png") _ (sh/write-file! pngpath data) ppmpath (convert-to-ppm pngpath) @@ -293,88 +294,74 @@ (sh/rmdir! tempdir) (dissoc node :tempdir))) - (process-text-node [page item] + (extract-txt-node [page item] (-> (p/resolved item) (p/then (partial resolve-text-node page)) (p/then extract-single-node) (p/then trace-node) (p/then clean-temp-data))) - (process-text-nodes [page] + (extract-txt-nodes [page {:keys [id] :as objects}] (l/trace :fn :process-text-nodes) - (-> (bw/select-all page "#screenshot foreignObject") - (p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes)))))) + (-> (bw/select-all page (str/concat "#screenshot-" id " foreignObject")) + (p/then (fn [nodes] (p/all (map (partial extract-txt-node page) nodes)))) + (p/then (fn [nodes] (d/index-by :id nodes))))) - (extract [page] - (p/let [dom (bw/select page "#screenshot") - xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem))) - nodes (process-text-nodes page) - nodes (d/index-by :id nodes) - result (replace-text-nodes xmldata nodes) + (extract-svg [page {:keys [id] :as object}] + (let [node (bw/select page (str/concat "#screenshot-" id))] + (bw/wait-for node) + (bw/eval! node (fn [elem] (.-outerHTML ^js elem))))) - ;; SVG standard don't allow the entity nbsp.   is equivalent but - ;; compatible with SVG - result (str/replace result " " " ")] - ;; (println "------- ORIGIN:") - ;; (cljs.pprint/pprint (xml->clj xmldata)) - ;; (println "------- RESULT:") - ;; (cljs.pprint/pprint (xml->clj result)) - ;; (println "-------") - result)) - ] + (prepare-options [uri] + #js {:screen #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :viewport #js {:width bw/default-viewport-width + :height bw/default-viewport-height} + :locale "en-US" + :storageState #js {:cookies (bw/create-cookies uri {:token token})} + :deviceScaleFactor scale + :userAgent bw/default-user-agent}) - (p/let [params {:file-id file-id - :page-id page-id - :object-id object-id - :render-texts true - :embed true - :route "render-object"} + (render-object [page {:keys [id] :as object}] + (p/let [tmpdir (sh/mktmpdir! "svg-render") + path (path/join tmpdir (str/concat id (mime/get-extension type))) + node (bw/select page (str/concat "#screenshot-" id))] + (bw/wait-for node) + (p/let [xmldata (extract-svg page object) + txtdata (extract-txt-nodes page object) + result (replace-text-nodes xmldata txtdata) + result (str/replace result " " " ")] - uri (-> (or uri (cf/get :public-uri)) - (assoc :path "/render.html") - (assoc :query (u/map->query-string params)))] + ;; (println "------- ORIGIN:") + ;; (cljs.pprint/pprint (xml->clj xmldata)) + ;; (println "------- RESULT:") + ;; (cljs.pprint/pprint (xml->clj result)) + ;; (println "-------") - (bw/exec! - #js {:screen #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :viewport #js {:width bw/default-viewport-width - :height bw/default-viewport-height} - :locale "en-US" - :storageState #js {:cookies (bw/create-cookies uri {:token token})} - :deviceScaleFactor scale - :userAgent bw/default-user-agent} - (fn [page] - (l/info :uri uri) - (p/do! - (bw/nav! page uri) - (p/let [dom (bw/select page "#screenshot")] - (bw/wait-for dom) - (bw/sleep page 2000)) + (sh/write-file! path result) + (on-object (assoc object :path path)) + path))) - (extract page))))))) + (render [uri page] + (l/info :uri uri) + (p/do + ;; navigate to the page and perform basic setup + (bw/nav! page (str uri)) + (bw/sleep page 1000) ; the good old fix with sleep -(s/def ::name ::us/string) -(s/def ::suffix ::us/string) -(s/def ::type #{:svg}) -(s/def ::page-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) -(s/def ::scale ::us/number) -(s/def ::token ::us/string) -(s/def ::uri ::us/uri) + ;; take the screnshot of requested objects, one by one + (p/run! (partial render-object page) objects) + nil))] -(s/def ::params - (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token] - :opt-un [::uri])) - -(defn render - [params] - (us/assert ::params params) - (p/let [content (render-object params)] - {:data content - :name (str (:name params) - (:suffix params "") - ".svg") - :size (alength content) - :mtype "image/svg+xml"})) + (p/let [params {:file-id file-id + :page-id page-id + :render-texts true + :render-embed true + :object-id (mapv :id objects) + :route "objects"} + uri (-> (or uri (cf/get :public-uri)) + (assoc :path "/render.html") + (assoc :query (u/map->query-string params)))] + (bw/exec! (prepare-options uri) + (partial render uri))))) diff --git a/exporter/src/app/util/mime.cljs b/exporter/src/app/util/mime.cljs new file mode 100644 index 000000000..ed5a19a4c --- /dev/null +++ b/exporter/src/app/util/mime.cljs @@ -0,0 +1,32 @@ +;; 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.util.mime + "Mimetype and file extension helpers." + (:refer-clojure :exclude [get]) + (:require + [app.common.data :as d] + [cljs.core :as c])) + +(defn get-extension + [type] + (case type + :png ".png" + :jpeg ".jpg" + :svg ".svg" + :pdf ".pdf" + :zip ".zip")) + +(defn- get + [type] + (case type + :zip "application/zip" + :pdf "application/pdf" + :svg "image/svg+xml" + :jpeg "image/jpeg" + :png "image/png")) + + diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index ee9f5d1c5..93b5333ed 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -16,12 +16,9 @@ (l/set-level! :trace) -(defn create-tmpdir! +(defn mktmpdir! [prefix] - (-> (.mkdtemp fs/promises prefix) - (p/then (fn [result] - (path/join (os/tmpdir) result))))) - + (.mkdtemp fs/promises (path/join (os/tmpdir) prefix))) (defn move! [origin-path dest-path] diff --git a/frontend/deps.edn b/frontend/deps.edn index e3e6000c8..b3196cfa4 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -10,10 +10,12 @@ funcool/beicon {:mvn/version "2021.07.05-1"} funcool/okulary {:mvn/version "2020.04.14-0"} funcool/potok {:mvn/version "2021.09.20-0"} - funcool/rumext {:mvn/version "2022.01.20.128"} + funcool/rumext {:mvn/version "2022.03.28-131"} funcool/tubax {:mvn/version "2021.05.20-0"} instaparse/instaparse {:mvn/version "1.4.10"} + garden/garden {:mvn/version "1.3.10"} + } :aliases diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 44946090f..46dd9ca1f 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -6,7 +6,6 @@ (ns app.main.data.exports (:require - [app.common.data.macros :as dm] [app.common.uuid :as uuid] [app.main.data.modal :as modal] [app.main.data.workspace.persistence :as dwp] @@ -47,6 +46,7 @@ state (dissoc state :export)))))) + (defn show-workspace-export-dialog ([] (show-workspace-export-dialog nil)) ([{:keys [selected]}] @@ -55,8 +55,6 @@ (watch [_ state _] (let [file-id (:current-file-id state) page-id (:current-page-id state) - - filename (-> (wsh/lookup-page state page-id) :name) selected (or selected (wsh/lookup-selected state page-id {})) shapes (if (seq selected) @@ -74,11 +72,10 @@ (assoc :name (:name shape))))] (rx/of (modal/show :export-shapes - {:exports (vec exports) - :filename filename}))))))) + {:exports (vec exports)}))))))) (defn show-viewer-export-dialog - [{:keys [shapes filename page-id file-id exports]}] + [{:keys [shapes page-id file-id exports]}] (ptk/reify ::show-viewer-export-dialog ptk/WatchEvent (watch [_ _ _] @@ -91,51 +88,44 @@ (assoc :object-id (:id shape)) (assoc :shape (dissoc shape :exports)) (assoc :name (:name shape))))] - (rx/of (modal/show :export-shapes {:exports (vec exports) - :filename filename})))))) + (rx/of (modal/show :export-shapes {:exports (vec exports)})))))) (defn show-workspace-export-frames-dialog - ([frames] - (ptk/reify ::show-workspace-export-frames-dialog - ptk/WatchEvent - (watch [_ state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state) - filename (-> (wsh/lookup-page state page-id) - :name - (dm/str ".pdf")) + [frames] + (ptk/reify ::show-workspace-export-frames-dialog + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + exports (for [frame frames] + {:enabled true + :page-id page-id + :file-id file-id + :object-id (:id frame) + :shape frame + :name (:name frame)})] - exports (for [frame frames] - {:enabled true - :page-id page-id - :file-id file-id - :frame-id (:id frame) - :shape frame - :name (:name frame)})] - - (rx/of (modal/show :export-frames - {:exports (vec exports) - :filename filename}))))))) + (rx/of (modal/show :export-frames + {:exports (vec exports)})))))) (defn- initialize-export-status - [exports filename resource-id query-name] + [exports cmd resource] (ptk/reify ::initialize-export-status ptk/UpdateEvent (update [_ state] (assoc state :export {:in-progress true - :resource-id resource-id + :resource-id (:id resource) :healthy? true :error false :progress 0 :widget-visible true :detail-visible true :exports exports - :filename filename :last-update (dt/now) - :query-name query-name})))) + :cmd cmd})))) (defn- update-export-status - [{:keys [progress status resource-id name] :as data}] + [{:keys [done status resource-id filename] :as data}] (ptk/reify ::update-export-status ptk/UpdateEvent (update [_ state] @@ -144,7 +134,7 @@ healthy? (< time-diff (dt/duration {:seconds 6}))] (cond-> state (= status "running") - (update :export assoc :progress (:done progress) :last-update (dt/now) :healthy? healthy?) + (update :export assoc :progress done :last-update (dt/now) :healthy? healthy?) (= status "error") (update :export assoc :error (:cause data) :last-update (dt/now) :healthy? healthy?) @@ -155,12 +145,12 @@ ptk/WatchEvent (watch [_ _ _] (when (= status "ended") - (->> (rp/query! :download-export-resource resource-id) + (->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id}) (rx/delay 500) - (rx/map #(dom/trigger-download name %))))))) + (rx/map #(dom/trigger-download filename %))))))) (defn request-simple-export - [{:keys [export filename]}] + [{:keys [export]}] (ptk/reify ::request-simple-export ptk/UpdateEvent (update [_ state] @@ -170,22 +160,26 @@ (watch [_ state _] (let [profile-id (:profile-id state) params {:exports [export] - :profile-id profile-id}] + :profile-id profile-id + :cmd :export-shapes + :wait true}] (rx/concat (rx/of ::dwp/force-persist) - (->> (rp/query! :export-shapes-simple params) - (rx/map (fn [data] - (dom/trigger-download filename data) - (clear-export-state uuid/zero))) + (->> (rp/query! :export-shapes params) + (rx/mapcat (fn [{:keys [id filename]}] + (->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id}) + (rx/map (fn [data] + (dom/trigger-download filename data) + (clear-export-state uuid/zero)))))) (rx/catch (fn [cause] - (prn "KKKK" cause) (rx/concat (rx/of (clear-export-state uuid/zero)) (rx/throw cause)))))))))) + (defn request-multiple-export - [{:keys [filename exports query-name] - :or {query-name :export-shapes-multiple} + [{:keys [exports cmd] + :or {cmd :export-shapes} :as params}] (ptk/reify ::request-multiple-export ptk/WatchEvent @@ -194,7 +188,7 @@ profile-id (:profile-id state) ws-conn (:ws-conn state) params {:exports exports - :name filename + :cmd cmd :profile-id profile-id :wait false} @@ -219,11 +213,10 @@ ;; Launch the exportation process and stores the resource id ;; locally. - (->> (rp/query! query-name params) - (rx/tap (fn [{:keys [id]}] - (vreset! resource-id id))) - (rx/map (fn [{:keys [id]}] - (initialize-export-status exports filename id query-name)))) + (->> (rp/query! :exporter params) + (rx/map (fn [{:keys [id] :as resource}] + (vreset! resource-id id) + (initialize-export-status exports cmd resource)))) ;; We proceed to update the export state with incoming ;; progress updates. We delay the stoper for give some time @@ -246,13 +239,12 @@ (rx/map #(clear-export-state @resource-id)) (rx/take-until (rx/delay 6000 stoper)))))))) - (defn retry-last-export [] (ptk/reify ::retry-last-export ptk/WatchEvent (watch [_ state _] - (let [params (select-keys (:export state) [:filename :exports :query-name])] + (let [params (select-keys (:export state) [:exports :cmd])] (when (seq params) (rx/of (request-multiple-export params))))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index d94e206b7..549813a40 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -17,7 +17,6 @@ [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] - [expound.alpha :as expound] [fipp.edn :as fpp] [potok.core :as ptk])) @@ -113,13 +112,12 @@ (ts/schedule (st/emitf (msg/show {:content "Internal error: assertion." - :type :error - :timeout 3000}))) + :type :error + :timeout 3000}))) ;; Print to the console some debugging info (js/console.group message) (js/console.info context) - (js/console.error (with-out-str (expound/printer error))) (js/console.groupEnd message))) ;; That are special case server-errors that should be treated diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 34eebbabc..1136002d6 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -14,7 +14,8 @@ (:require ["react-dom/server" :as rds] [app.common.colors :as clr] - [app.common.geom.align :as gal] + [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -22,10 +23,12 @@ [app.common.pages.helpers :as cph] [app.config :as cfg] [app.main.fonts :as fonts] + [app.main.ui.context :as muc] [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as export] + [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.image :as image] @@ -57,11 +60,9 @@ :fill color}]) (defn- calculate-dimensions - [{:keys [objects] :as data} vport] - (let [shapes (cph/get-immediate-children objects) - rect (cond->> (gsh/selection-rect shapes) - (some? vport) - (gal/adjust-to-viewport vport))] + [objects] + (let [shapes (cph/get-immediate-children objects) + rect (gsh/selection-rect shapes)] (-> rect (update :x mth/finite 0) (update :y mth/finite 0) @@ -156,24 +157,63 @@ (->> [x y width height] (map #(ust/format-precision % viewbox-decimal-precision))))) +(defn adapt-root-frame + [objects object] + (let [shapes (cph/get-immediate-children objects) + srect (gsh/selection-rect shapes) + object (merge object (select-keys srect [:x :y :width :height])) + object (gsh/transform-shape object)] + (assoc object :fill-color "#f0f0f0"))) + +(defn adapt-objects-for-shape + [objects object-id] + (let [object (get objects object-id) + object (cond->> object + (cph/root-frame? object) + (adapt-root-frame objects)) + + ;; Replace the previous object with the new one + objects (assoc objects object-id object) + + modifier (-> (gpt/point (:x object) (:y object)) + (gpt/negate) + (gmt/translate-matrix)) + + mod-ids (cons object-id (cph/get-children-ids objects object-id)) + updt-fn #(-> %1 + (assoc-in [%2 :modifiers :displacement] modifier) + (update %2 gsh/transform-shape))] + + (reduce updt-fn objects mod-ids))) + +(defn get-object-bounds + [objects object-id] + (let [object (get objects object-id) + padding (filters/calculate-padding object) + bounds (-> (filters/get-filters-bounds object) + (update :x - (:horizontal padding)) + (update :y - (:vertical padding)) + (update :width + (* 2 (:horizontal padding))) + (update :height + (* 2 (:vertical padding))))] + + (if (cph/group-shape? object) + (if (:masked-group? object) + (get-object-bounds objects (-> object :shapes first)) + (->> (:shapes object) + (into [bounds] (map (partial get-object-bounds objects))) + (gsh/join-rects))) + bounds))) + (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data width height thumbnails? embed? include-metadata?] :as props - :or {embed? false include-metadata? false}}] + [{:keys [data thumbnails? render-embed? include-metadata?] :as props + :or {render-embed? false include-metadata? false}}] (let [objects (:objects data) shapes (cph/get-immediate-children objects) - root-children - (->> shapes - (remove cph/frame-shape?) - (mapcat #(cph/get-children-with-self objects (:id %)))) - - vport (when (and (some? width) (some? height)) - {:width width :height height}) - - dim (calculate-dimensions data vport) + dim (calculate-dimensions objects) vbox (format-viewbox dim) - background-color (get-in data [:options :background] default-color) + bgcolor (dm/get-in data [:options :background] default-color) frame-wrapper (mf/use-memo @@ -185,7 +225,7 @@ (mf/deps objects) #(shape-wrapper-factory objects))] - [:& (mf/provider embed/context) {:value embed?} + [:& (mf/provider embed/context) {:value render-embed?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} [:svg {:view-box vbox :version "1.1" @@ -194,12 +234,17 @@ :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") :style {:width "100%" :height "100%" - :background background-color}} + :background bgcolor}} (when include-metadata? [:& export/export-page {:options (:options data)}]) - [:& ff/fontfaces-style {:shapes root-children}] + + (let [shapes (->> shapes + (remove cph/frame-shape?) + (mapcat #(cph/get-children-with-self objects (:id %))))] + [:& ff/fontfaces-style {:shapes shapes}]) + (for [item shapes] (let [frame? (= (:type item) :frame)] (cond @@ -214,6 +259,10 @@ [:& shape-wrapper {:shape item :key (:id item)}])))]]])) + +;; Component that serves for render frame thumbnails, mainly used in +;; the viewer and handoff + (mf/defc frame-svg {::mf/wrap [mf/memo]} [{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}] @@ -260,6 +309,10 @@ [:> shape-container {:shape frame} [:& frame/frame-thumbnail {:shape frame}]]))])) + +;; Component for rendering a thumbnail of a single componenent. Mainly +;; used to render thumbnails on assets panel. + (mf/defc component-svg {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [{:keys [objects group zoom] :or {zoom 1} :as props}] @@ -304,81 +357,122 @@ [:> shape-container {:shape group} [:& group-wrapper {:shape group :view-box vbox}]]])) +(mf/defc object-svg + {::mf/wrap [mf/memo]} + [{:keys [objects object zoom render-texts? render-embed?] + :or {zoom 1 render-embed? false} + :as props}] + (let [object (cond-> object + (:hide-fill-on-export object) + (assoc :fills [])) + + obj-id (:id object) + x (* (:x object) zoom) + y (* (:y object) zoom) + width (* (:width object) zoom) + height (* (:height object) zoom) + + vbox (dm/str x " " y " " width " " height) + + frame-wrapper + (mf/with-memo [objects] + (frame-wrapper-factory objects)) + + group-wrapper + (mf/with-memo [objects] + (group-wrapper-factory objects)) + + shape-wrapper + (mf/with-memo [objects] + (shape-wrapper-factory objects)) + + text-shapes (sequence (filter cph/text-shape?) (vals objects)) + render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] + + [:& (mf/provider embed/context) {:value render-embed?} + [:svg {:id (dm/str "screenshot-" obj-id) + :view-box vbox + :width width + :height height + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + ;; Fix Chromium bug about color of html texts + ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 + :style {:-webkit-print-color-adjust :exact}} + + (let [shapes (cph/get-children objects obj-id)] + [:& ff/fontfaces-style {:shapes shapes}]) + + (case (:type object) + :frame [:& frame-wrapper {:shape object :view-box vbox}] + :group [:> shape-container {:shape object} + [:& group-wrapper {:shape object}]] + [:& shape-wrapper {:shape object}])] + + ;; Auxiliary SVG for rendering text-shapes + (when render-texts? + (for [object text-shapes] + [:& (mf/provider muc/text-plain-colors-ctx) {:value true} + [:svg + {:id (dm/str "screenshot-text-" (:id object)) + :view-box (dm/str "0 0 " (:width object) " " (:height object)) + :width (:width object) + :height (:height object) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink"} + [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SPRITES (DEBUG) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (mf/defc component-symbol - {::mf/wrap-props false} - [props] - (let [id (obj/get props "id") - data (obj/get props "data") - name (:name data) - path (:path data) - objects (:objects data) - root (get objects id) - selrect (:selrect root) + [{:keys [id data] :as props}] + (let [name (:name data) + objects (-> (:objects data) + (adapt-objects-for-shape id)) + object (get objects id) + selrect (:selrect object) vbox (format-viewbox {:width (:width selrect) :height (:height selrect)}) - modifier - (mf/use-memo - (mf/deps (:x root) (:y root)) - (fn [] - (-> (gpt/point (:x root) (:y root)) - (gpt/negate) - (gmt/translate-matrix)))) - - objects - (mf/use-memo - (mf/deps modifier id objects) - (fn [] - (let [modifier-ids (cons id (cph/get-children-ids objects id)) - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)] - (reduce update-fn objects modifier-ids)))) - - root - (mf/use-memo - (mf/deps modifier root) - (fn [] (assoc-in root [:modifiers :displacement] modifier))) - group-wrapper (mf/use-memo (mf/deps objects) (fn [] (group-wrapper-factory objects)))] - [:> "symbol" #js {:id (str id) - :viewBox vbox - "penpot:path" path} + [:> "symbol" #js {:id (str id) :viewBox vbox} [:title name] - [:> shape-container {:shape root} - [:& group-wrapper {:shape root :view-box vbox}]]])) + [:> shape-container {:shape object} + [:& group-wrapper {:shape object :view-box vbox}]]])) (mf/defc components-sprite-svg {::mf/wrap-props false} [props] (let [data (obj/get props "data") children (obj/get props "children") - embed? (obj/get props "embed?") + render-embed? (obj/get props "render-embed?") include-metadata? (obj/get props "include-metadata?")] - [:& (mf/provider embed/context) {:value embed?} + [:& (mf/provider embed/context) {:value render-embed?} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} [:svg {:version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") - :style {:width "100vw" - :height "100vh" - :display (when-not (some? children) "none")}} + :style {:display (when-not (some? children) "none")}} [:defs - (for [[component-id component-data] (:components data)] - [:& component-symbol {:id component-id - :key (str component-id) - :data component-data}])] + (for [[id data] (:components data)] + [:& component-symbol {:id id :key (dm/str id) :data data}])] children]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; RENDERING +;; RENDER FOR DOWNLOAD (wrongly called exportation) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- get-image-data [shape] @@ -426,7 +520,7 @@ (->> (rx/of data) (rx/map (fn [data] - (let [elem (mf/element page-svg #js {:data data :embed? true :include-metadata? true})] + (let [elem (mf/element page-svg #js {:data data :render-embed? true :include-metadata? true})] (rds/renderToStaticMarkup elem))))))) (defn render-components @@ -445,5 +539,6 @@ (->> (rx/of data) (rx/map (fn [data] - (let [elem (mf/element components-sprite-svg #js {:data data :embed? true :include-metadata? true})] + (let [elem (mf/element components-sprite-svg + #js {:data data :render-embed? true :include-metadata? true})] (rds/renderToStaticMarkup elem)))))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 2e3a1a970..6ed957f11 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -105,34 +105,22 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) -(defn- send-export-command - [& {:keys [cmd params blob?]}] +(defn- send-export + [{:keys [blob?] :as params}] (->> (http/send! {:method :post :uri (u/join base-uri "api/export") - :body (http/transit-data (assoc params :cmd cmd)) + :body (http/transit-data (dissoc params :blob?)) :credentials "include" :response-type (if blob? :blob :text)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) -(defmethod query :export-shapes-simple +(defmethod query :exporter [_ params] - (let [params (merge {:wait true} params)] - (->> (rx/of params) - (rx/mapcat #(send-export-command :cmd :export-shapes :params % :blob? false)) - (rx/mapcat #(send-export-command :cmd :get-resource :params % :blob? true))))) - -(defmethod query :export-shapes-multiple - [_ params] - (send-export-command :cmd :export-shapes :params params :blob? false)) - -(defmethod query :export-frames-multiple - [_ params] - (send-export-command :cmd :export-frames :params (assoc params :uri (str base-uri)) :blob? false)) - -(defmethod query :download-export-resource - [_ id] - (send-export-command :cmd :get-resource :params {:id id} :blob? true)) + (let [default {:wait false + :blob? false + :uri (str base-uri)}] + (send-export (merge default params)))) (derive :upload-file-media-object ::multipart-upload) (derive :update-profile-photo ::multipart-upload) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 151e14b21..fd65d7569 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -19,7 +19,6 @@ [app.main.ui.onboarding] [app.main.ui.onboarding.questions] [app.main.ui.releases] - [app.main.ui.render :as render] [app.main.ui.settings :as settings] [app.main.ui.static :as static] [app.main.ui.viewer :as viewer] @@ -110,15 +109,6 @@ :index index :share-id share-id}])) - ;; TODO: maybe move to `app.render` entrypoint (handled by render.html) - :render-sprite - (do - (let [file-id (uuid (get-in route [:path-params :file-id])) - component-id (get-in route [:query-params :component-id]) - component-id (when (some? component-id) (uuid component-id))] - [:& render/render-sprite {:file-id file-id - :component-id component-id}])) - :workspace (let [project-id (some-> params :path :project-id uuid) file-id (some-> params :path :file-id uuid) diff --git a/frontend/src/app/main/ui/export.cljs b/frontend/src/app/main/ui/export.cljs index 3b273c399..da0e639d0 100644 --- a/frontend/src/app/main/ui/export.cljs +++ b/frontend/src/app/main/ui/export.cljs @@ -23,7 +23,7 @@ [rumext.alpha :as mf])) (mf/defc export-multiple-dialog - [{:keys [exports filename title query-name no-selection]}] + [{:keys [exports title cmd no-selection]}] (let [lstate (mf/deref refs/export) in-progress? (:in-progress lstate) @@ -33,7 +33,10 @@ all-checked? (every? :enabled all-exports) all-unchecked? (every? (complement :enabled) all-exports) - enabled-exports (into [] (filter :enabled) all-exports) + enabled-exports (into [] + (comp (filter :enabled) + (map #(dissoc % :shape :enabled))) + all-exports) cancel-fn (fn [event] @@ -45,9 +48,8 @@ (dom/prevent-default event) (st/emit! (modal/hide) (de/request-multiple-export - {:filename filename - :exports enabled-exports - :query-name query-name}))) + {:exports enabled-exports + :cmd cmd}))) on-toggle-enabled (fn [index] @@ -145,25 +147,23 @@ (mf/defc export-shapes-dialog {::mf/register modal/components ::mf/register-as :export-shapes} - [{:keys [exports filename]}] + [{:keys [exports]}] (let [title (tr "dashboard.export-shapes.title")] [:& export-multiple-dialog {:exports exports - :filename filename :title title - :query-name :export-shapes-multiple + :cmd :export-shapes :no-selection shapes-no-selection}])) (mf/defc export-frames {::mf/register modal/components ::mf/register-as :export-frames} - [{:keys [exports filename]}] + [{:keys [exports]}] (let [title (tr "dashboard.export-frames.title")] [:& export-multiple-dialog {:exports exports - :filename filename :title title - :query-name :export-frames-multiple}])) + :cmd :export-frames}])) (mf/defc export-progress-widget {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs deleted file mode 100644 index e731d4555..000000000 --- a/frontend/src/app/main/ui/render.cljs +++ /dev/null @@ -1,203 +0,0 @@ -;; 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.main.ui.render - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] - [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] - [app.main.data.fonts :as df] - [app.main.render :as render] - [app.main.repo :as repo] - [app.main.store :as st] - [app.main.ui.context :as muc] - [app.main.ui.shapes.embed :as embed] - [app.main.ui.shapes.filters :as filters] - [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.fontfaces :as ff] - [app.util.dom :as dom] - [beicon.core :as rx] - [cuerdas.core :as str] - [rumext.alpha :as mf])) - -(defn calc-bounds - [object objects] - (let [xf-get-bounds (comp (map #(get objects %)) (map #(calc-bounds % objects))) - padding (filters/calculate-padding object) - obj-bounds (-> (filters/get-filters-bounds object) - (update :x - (:horizontal padding)) - (update :y - (:vertical padding)) - (update :width + (* 2 (:horizontal padding))) - (update :height + (* 2 (:vertical padding))))] - - (cond - (and (= :group (:type object)) - (:masked-group? object)) - (calc-bounds (get objects (first (:shapes object))) objects) - - (= :group (:type object)) - (->> (:shapes object) - (into [obj-bounds] xf-get-bounds) - (gsh/join-rects)) - - :else - obj-bounds))) - -(mf/defc object-svg - {::mf/wrap [mf/memo]} - [{:keys [objects object-id zoom render-texts? embed?] - :or {zoom 1 embed? false} - :as props}] - (let [object (get objects object-id) - frame-id (if (= :frame (:type object)) - (:id object) - (:frame-id object)) - - modifier (-> (gpt/point (:x object) (:y object)) - (gpt/negate) - (gmt/translate-matrix)) - - mod-ids (cons frame-id (cph/get-children-ids objects frame-id)) - updt-fn #(-> %1 - (assoc-in [%2 :modifiers :displacement] modifier) - (update %2 gsh/transform-shape)) - - objects (reduce updt-fn objects mod-ids) - object (get objects object-id) - - object (cond-> object - (:hide-fill-on-export object) - (assoc :fills [])) - - all-children (cph/get-children objects object-id) - - {:keys [x y width height] :as bs} (calc-bounds object objects) - [_ _ width height :as coords] (->> [x y width height] (map #(* % zoom))) - - vbox (str/join " " coords) - - frame-wrapper - (mf/with-memo [objects] - (render/frame-wrapper-factory objects)) - - group-wrapper - (mf/with-memo [objects] - (render/group-wrapper-factory objects)) - - shape-wrapper - (mf/with-memo [objects] - (render/shape-wrapper-factory objects)) - - is-text? (fn [shape] (= :text (:type shape))) - - text-shapes (sequence (comp (map second) (filter is-text?)) objects) - - render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] - - (mf/with-effect [width height] - (dom/set-page-style! - {:size (dm/str (mth/ceil width) "px " - (mth/ceil height) "px")})) - - [:& (mf/provider embed/context) {:value embed?} - [:svg {:id "screenshot" - :view-box vbox - :width width - :height height - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - ;; Fix Chromium bug about color of html texts - ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 - :style {:-webkit-print-color-adjust :exact}} - - [:& ff/fontfaces-style {:shapes all-children}] - - (case (:type object) - :frame [:& frame-wrapper {:shape object :view-box vbox}] - :group [:> shape-container {:shape object} - [:& group-wrapper {:shape object}]] - [:& shape-wrapper {:shape object}])] - - ;; Auxiliary SVG for rendering text-shapes - (when render-texts? - (for [object text-shapes] - [:& (mf/provider muc/text-plain-colors-ctx) {:value true} - [:svg {:id (str "screenshot-text-" (:id object)) - :view-box (str "0 0 " (:width object) " " (:height object)) - :width (:width object) - :height (:height object) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink"} - [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))])) - -(defn- adapt-root-frame - [objects object-id] - (if (uuid/zero? object-id) - (let [object (get objects object-id) - shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes) - object (merge object (select-keys srect [:x :y :width :height])) - object (gsh/transform-shape object) - object (assoc object :fill-color "#f0f0f0")] - (assoc objects (:id object) object)) - objects)) - -(mf/defc render-object - [{:keys [file-id page-id object-id render-texts? embed?] :as props}] - (let [objects (mf/use-state nil)] - - (mf/with-effect [file-id page-id object-id] - (->> (rx/zip - (repo/query! :font-variants {:file-id file-id}) - (repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id})) - (rx/subs - (fn [[fonts {:keys [data]}]] - (when (seq fonts) - (st/emit! (df/fonts-fetched fonts))) - (let [objs (get-in data [:pages-index page-id :objects]) - objs (adapt-root-frame objs object-id)] - (reset! objects objs))))) - (constantly nil)) - - (when @objects - [:& object-svg {:objects @objects - :object-id object-id - :embed? embed? - :render-texts? render-texts? - :zoom 1}]))) - -(mf/defc render-sprite - [{:keys [file-id component-id] :as props}] - (let [file (mf/use-state nil)] - - (mf/with-effect [file-id] - (->> (repo/query! :file {:id file-id}) - (rx/subs - (fn [result] - (reset! file result)))) - (constantly nil)) - - (when @file - [:* - [:& render/components-sprite-svg {:data (:data @file) :embed true} - - (when (some? component-id) - [:use {:x 0 :y 0 - :xlinkHref (str "#" component-id)}])] - - (when-not (some? component-id) - [:ul - (for [[id data] (get-in @file [:data :components])] - (let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)] - [:li [:a {:href url} (:name data)]]))])]))) - diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index efc66c02c..a456f7bd4 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -61,7 +61,6 @@ ["/debug/icons-preview" :debug-icons-preview]) ;; Used for export - ["/render-object/:file-id/:page-id/:object-id" :render-object] ["/render-sprite/:file-id" :render-sprite] ["/dashboard/team/:team-id" diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs index 00691e236..0ae24a525 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs @@ -11,6 +11,7 @@ [app.util.code-gen :as cg] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [cuerdas.core :as str] [rumext.alpha :as mf])) (defn has-image? [shape] @@ -34,12 +35,10 @@ [:div.attributes-value (-> shape :metadata :height) "px"] [:& copy-button {:data (cg/generate-css-props shape :height)}]] - (let [mtype (-> shape :metadata :mtype) - name (:name shape) + (let [mtype (-> shape :metadata :mtype) + name (:name shape) extension (dom/mtype->extension mtype)] [:a.download-button {:target "_blank" - :download (if extension - (str name "." extension) - name) + :download (cond-> name extension (str/concat extension)) :href (cfg/resolve-file-media (-> shape :metadata))} (tr "handoff.attributes.image.download")])]))) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index c7390b6a7..89f4c2c05 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -390,34 +390,3 @@ :bool [:> bool-container {:shape shape :frame frame :objects objects}] :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) -(mf/defc frame-svg - {::mf/wrap [mf/memo]} - [{:keys [objects frame zoom] :or {zoom 1} :as props}] - (let [modifier (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) - - update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) - - frame-id (:id frame) - modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id)) - objects (reduce update-fn objects modifier-ids) - frame (assoc-in frame [:modifiers :displacement] modifier) - width (* (:width frame) zoom) - height (* (:height frame) zoom) - - vbox (str "0 0 " (:width frame 0) - " " (:height frame 0)) - wrapper (mf/use-memo - (mf/deps objects) - #(frame-container-factory objects))] - - [:svg {:view-box vbox - :width width - :height height - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& wrapper {:shape frame - :view-box vbox}]])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 27bb3fd6a..e0d58295a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -30,7 +30,7 @@ state (mf/deref refs/export) in-progress? (:in-progress state) - filename (when (seqable? exports) + sname (when (seqable? exports) (let [shapes (wsh/lookup-shapes @st/state ids) sname (-> shapes first :name) suffix (-> exports first :suffix)] @@ -56,13 +56,13 @@ ;; separatelly by the export-modal. (let [defaults {:page-id page-id :file-id file-id - :name filename + :name sname :object-id (first ids)} exports (mapv #(merge % defaults) exports)] (if (= 1 (count exports)) (let [export (first exports)] - (st/emit! (de/request-simple-export {:export export :filename (:name export)}))) - (st/emit! (de/request-multiple-export {:exports exports :filename filename}))))))) + (st/emit! (de/request-simple-export {:export export}))) + (st/emit! (de/request-multiple-export {:exports exports}))))))) ;; TODO: maybe move to specific events for avoid to have this logic here? add-export diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index b40802fb9..38b61f544 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -7,27 +7,38 @@ (ns app.render "The main entry point for UI part needed by the exporter." (:require - [app.common.logging :as log] + [app.common.logging :as l] + [app.common.math :as mth] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] - [app.main.ui.render :as render] + [app.main.data.fonts :as df] + [app.main.render :as render] + [app.main.repo :as repo] + [app.main.store :as st] [app.util.dom :as dom] [app.util.globals :as glob] + [beicon.core :as rx] [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [garden.core :refer [css]] [rumext.alpha :as mf])) -(log/initialize!) -(log/set-level! :root :warn) -(log/set-level! :app :info) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SETUP +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare reinit) +(l/initialize!) +(l/set-level! :root :warn) +(l/set-level! :app :info) -(declare ^:private render-object) +(declare ^:private render-single-object) +(declare ^:private render-components) +(declare ^:private render-objects) -(log/info :hint "Welcome to penpot (Export)" - :version (:full @cf/version) - :public-uri (str cf/public-uri)) +(l/info :hint "Welcome to penpot (Export)" + :version (:full @cf/version) + :public-uri (str cf/public-uri)) (defn- parse-params [loc] @@ -38,7 +49,8 @@ [] (when-let [params (parse-params glob/location)] (when-let [component (case (:route params) - "render-object" (render-object params) + "objects" (render-objects params) + "components" (render-components params) nil)] (mf/mount component (dom/get-element "app"))))) @@ -55,23 +67,225 @@ [] (reinit)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; COMPONENTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; ---- SINGLE OBJECT + +(defn use-resource + "A general purpose hook for retrieve or subscribe to remote changes + using the reactive-streams mechanism mechanism. + + It receives a function to execute for retrieve the stream that will + be used for creating the subscription. The function should be + stable, so is the responsability of the user of this hook to + properly memoize it. + + TODO: this should be placed in some generic hooks namespace but his + right now is pending of refactor and it will be done later." + [f] + (let [[state ^js update-state!] (mf/useState {:loaded? false})] + (mf/with-effect [f] + (update-state! (fn [prev] (assoc prev :refreshing? true))) + (let [on-value (fn [data] + (update-state! #(-> % + (assoc :refreshing? false) + (assoc :loaded? true) + (merge data)))) + subs (rx/subscribe (f) on-value)] + #(rx/dispose! subs))) + state)) + +(mf/defc object-svg + [{:keys [page-id file-id object-id render-embed? render-texts?]}] + (let [fetch-state (mf/use-fn + (mf/deps file-id page-id object-id) + (fn [] + (->> (rx/zip + (repo/query! :font-variants {:file-id file-id}) + (repo/query! :page {:file-id file-id + :page-id page-id + :object-id object-id + :prune-thumbnails true})) + (rx/tap (fn [[fonts]] + (when (seq fonts) + (st/emit! (df/fonts-fetched fonts))))) + (rx/map (comp :objects second)) + (rx/map (fn [objects] + (let [objects (render/adapt-objects-for-shape objects object-id) + bounds (render/get-object-bounds objects object-id) + object (get objects object-id)] + {:objects objects + :object (merge object bounds)})))))) + + {:keys [objects object]} (use-resource fetch-state)] + + ;; Set the globa CSS to assign the page size, needed for PDF + ;; exportation process. + (mf/with-effect [object] + (when object + (dom/set-page-style! + {:size (str/concat + (mth/ceil (:width object)) "px " + (mth/ceil (:height object)) "px")}))) + + (when objects + [:& render/object-svg + {:objects objects + :object object + :render-embed? render-embed? + :render-texts? render-texts? + :zoom 1}]))) + +(mf/defc objects-svg + [{:keys [page-id file-id object-ids render-embed? render-texts?]}] + (let [fetch-state (mf/use-fn + (mf/deps file-id page-id) + (fn [] + (->> (rx/zip + (repo/query! :font-variants {:file-id file-id}) + (repo/query! :page {:file-id file-id + :page-id page-id + :prune-thumbnails true})) + (rx/tap (fn [[fonts]] + (when (seq fonts) + (st/emit! (df/fonts-fetched fonts))))) + (rx/map (comp :objects second))))) + + objects (use-resource fetch-state)] + + (when objects + (for [object-id object-ids] + (let [objects (render/adapt-objects-for-shape objects object-id) + bounds (render/get-object-bounds objects object-id) + object (merge (get objects object-id) bounds)] + [:& render/object-svg + {:objects objects + :key (str object-id) + :object object + :render-embed? render-embed? + :render-texts? render-texts? + :zoom 1}]))))) + (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) -(s/def ::object-id ::us/uuid) +(s/def ::object-id + (s/or :single ::us/uuid + :multiple (s/coll-of ::us/uuid))) (s/def ::render-text ::us/boolean) (s/def ::embed ::us/boolean) -(s/def ::render-object-params +(s/def ::render-objects (s/keys :req-un [::file-id ::page-id ::object-id] - :opt-un [::render-text ::embed])) + :opt-un [::render-text ::render-embed])) -(defn- render-object +(defn- render-objects [params] - (let [{:keys [page-id file-id object-id render-texts embed]} (us/conform ::render-object-params params)] + (let [{:keys [file-id + page-id + render-embed + render-texts] + :as params} + (us/conform ::render-objects params) + + [type object-id] (:object-id params)] + + (case type + :single + (mf/html + [:& object-svg + {:file-id file-id + :page-id page-id + :object-id object-id + :render-embed? render-embed + :render-texts? render-texts}]) + + :multiple + (mf/html + [:& objects-svg + {:file-id file-id + :page-id page-id + :object-ids (into #{} object-id) + :render-embed? render-embed + :render-texts? render-texts}])))) + +;; ---- COMPONENTS SPRITE + +(mf/defc components-sprite-svg + [{:keys [file-id embed] :as props}] + (let [fetch (mf/use-fn + (mf/deps file-id) + (fn [] (repo/query! :file {:id file-id}))) + + file (use-resource fetch) + state (mf/use-state nil)] + + (when file + [:* + [:style + (css [[:body + {:margin 0 + :overflow "hidden" + :width "100vw" + :height "100vh"}] + + [:main + {:overflow "auto" + :display "flex" + :justify-content "center" + :align-items "center" + :height "calc(100vh - 200px)"} + [:svg {:width "50%" + :height "50%"}]] + [:.nav + {:display "flex" + :margin 0 + :padding "10px" + :flex-direction "column" + :flex-wrap "wrap" + :height "200px" + :list-style "none" + :overflow-x "scroll" + :border-bottom "1px dotted #e6e6e6"} + [:a {:cursor :pointer + :text-overflow "ellipsis" + :white-space "nowrap" + :overflow "hidden" + :text-decoration "underline"}] + [:li {:display "flex" + :width "150px" + :padding "5px" + :border "0px solid black"}]]])] + + [:ul.nav + (for [[id data] (get-in file [:data :components])] + (let [on-click (fn [event] + (dom/prevent-default event) + (swap! state assoc :component-id id))] + [:li {:key (str id)} + [:a {:on-click on-click} (:name data)]]))] + + [:main + [:& render/components-sprite-svg + {:data (:data file) + :embed embed} + + (when-let [component-id (:component-id @state)] + [:use {:x 0 :y 0 :xlinkHref (str "#" component-id)}])]] + + ]))) + +(s/def ::component-id ::us/uuid) +(s/def ::render-components + (s/keys :req-un [::file-id] + :opt-un [::embed ::component-id])) + +(defn render-components + [params] + (let [{:keys [file-id component-id embed]} (us/conform ::render-components params)] (mf/html - [:& render/render-object + [:& components-sprite-svg {:file-id file-id - :page-id page-id - :object-id object-id - :embed? embed - :render-texts? render-texts}]))) + :component-id component-id + :embed embed}]))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 64ba990dc..695cd2ed1 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -403,16 +403,16 @@ (defn mtype->extension [mtype] ;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types (case mtype - "image/apng" "apng" - "image/avif" "avif" - "image/gif" "gif" - "image/jpeg" "jpg" - "image/png" "png" - "image/svg+xml" "svg" - "image/webp" "webp" - "application/zip" "zip" - "application/penpot" "penpot" - "application/pdf" "pdf" + "image/apng" ".apng" + "image/avif" ".avif" + "image/gif" ".gif" + "image/jpeg" ".jpg" + "image/png" ".png" + "image/svg+xml" ".svg" + "image/webp" ".webp" + "application/zip" ".zip" + "application/penpot" ".penpot" + "application/pdf" ".pdf" nil)) (defn set-attribute! [^js node ^string attr value] @@ -464,11 +464,11 @@ (defn trigger-download-uri [filename mtype uri] - (let [link (create-element "a") + (let [link (create-element "a") extension (mtype->extension mtype) - filename (if extension - (str filename "." extension) - filename)] + filename (if (and extension (not (str/ends-with? filename extension))) + (str/concat filename "." extension) + filename)] (obj/set! link "href" uri) (obj/set! link "download" filename) (obj/set! (.-style ^js link) "display" "none") diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 1718cbbdb..1e47d68bc 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -135,7 +135,7 @@ (rx/map #(assoc % :file-id file-id)) (rx/flat-map (fn [media] - (let [file-path (str file-id "/media/" (:id media) "." (dom/mtype->extension (:mtype media)))] + (let [file-path (str/concat file-id "/media/" (:id media) (dom/mtype->extension (:mtype media)))] (->> (http/send! {:uri (cfg/resolve-file-media media) :response-type :blob diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index feeab2ee0..cd45b6781 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -48,7 +48,7 @@ :typographies (str file-id "/typographies.json") :media-list (str file-id "/media.json") :media (let [ext (dom/mtype->extension (:mtype media))] - (str file-id "/media/" id "." ext)) + (str/concat file-id "/media/" id ext)) :components (str file-id "/components.svg")) parse-svg? (and (not= type :media) (str/ends-with? path "svg")) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index ddd657643..30455109c 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -56,15 +56,16 @@ :uri (u/join (cfg/get-public-uri) path) :credentials "include" :query params}] + (->> (http/send! request) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) (defn- render-thumbnail - [{:keys [data file-id revn] :as params}] - (let [elem (if-let [frame (:thumbnail-frame data)] - (mf/element render/frame-svg #js {:objects (:objects data) :frame frame}) - (mf/element render/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}))] + [{:keys [page file-id revn] :as params}] + (let [elem (if-let [frame (:thumbnail-frame page)] + (mf/element render/frame-svg #js {:objects (:objects page) :frame frame}) + (mf/element render/page-svg #js {:data page :thumbnails? true}))] {:data (rds/renderToStaticMarkup elem) :fonts @fonts/loaded :file-id file-id @@ -81,6 +82,7 @@ :uri (u/join (cfg/get-public-uri) path) :credentials "include" :body (http/transit-data params)}] + (->> (http/send! request) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)