mirror of
https://github.com/penpot/penpot.git
synced 2025-05-14 20:16:38 +02:00
🎉 Export shapes to pdf
This commit is contained in:
parent
e9945235ed
commit
1ee14a76f4
8 changed files with 146 additions and 19 deletions
|
@ -7,6 +7,7 @@
|
||||||
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716).
|
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716).
|
||||||
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719).
|
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719).
|
||||||
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721).
|
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721).
|
||||||
|
- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519).
|
||||||
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718).
|
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718).
|
||||||
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663).
|
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663).
|
||||||
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063).
|
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063).
|
||||||
|
|
|
@ -71,6 +71,20 @@
|
||||||
:type (name type)
|
:type (name type)
|
||||||
:omitBackground omit-background?})))
|
:omitBackground omit-background?})))
|
||||||
|
|
||||||
|
(defn pdf
|
||||||
|
([page] (pdf page nil))
|
||||||
|
([page {:keys [viewport omit-background? prefer-css-page-size?]
|
||||||
|
:or {viewport {}
|
||||||
|
omit-background? true
|
||||||
|
prefer-css-page-size? true}}]
|
||||||
|
(let [viewport (d/merge default-viewport viewport)]
|
||||||
|
(.pdf ^js page #js {:width (:width viewport)
|
||||||
|
:height (:height viewport)
|
||||||
|
:scale (:scale viewport)
|
||||||
|
:omitBackground omit-background?
|
||||||
|
:printBackground (not omit-background?)
|
||||||
|
:preferCSSPageSize prefer-css-page-size?}))))
|
||||||
|
|
||||||
(defn eval!
|
(defn eval!
|
||||||
[frame f]
|
[frame f]
|
||||||
(.evaluate ^js frame f))
|
(.evaluate ^js frame f))
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.renderer.bitmap :as rb]
|
[app.renderer.bitmap :as rb]
|
||||||
[app.renderer.svg :as rs]
|
[app.renderer.svg :as rs]
|
||||||
|
[app.renderer.pdf :as rp]
|
||||||
[app.zipfile :as zip]
|
[app.zipfile :as zip]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -89,7 +90,8 @@
|
||||||
(case (:type params)
|
(case (:type params)
|
||||||
:png (rb/render params)
|
:png (rb/render params)
|
||||||
:jpeg (rb/render params)
|
:jpeg (rb/render params)
|
||||||
:svg (rs/render params)))
|
:svg (rs/render params)
|
||||||
|
:pdf (rp/render params)))
|
||||||
|
|
||||||
(defn- find-filename-candidate
|
(defn- find-filename-candidate
|
||||||
[params used]
|
[params used]
|
||||||
|
@ -101,7 +103,8 @@
|
||||||
(case (:type params)
|
(case (:type params)
|
||||||
:png ".png"
|
:png ".png"
|
||||||
:jpeg ".jpg"
|
:jpeg ".jpg"
|
||||||
:svg ".svg"))]
|
:svg ".svg"
|
||||||
|
:pdf ".pdf"))]
|
||||||
(if (contains? used candidate)
|
(if (contains? used candidate)
|
||||||
(recur (inc index))
|
(recur (inc index))
|
||||||
candidate))))
|
candidate))))
|
||||||
|
|
79
exporter/src/app/renderer/pdf.cljs
Normal file
79
exporter/src/app/renderer/pdf.cljs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
;; 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.pdf
|
||||||
|
"A pdf renderer."
|
||||||
|
(:require
|
||||||
|
[app.browser :as bw]
|
||||||
|
[app.common.exceptions :as ex :include-macros true]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
|
[cljs.spec.alpha :as s]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
|
[lambdaisland.glogi :as log]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
(defn create-cookie
|
||||||
|
[uri token]
|
||||||
|
(let [domain (str (:host uri)
|
||||||
|
(when (:port uri)
|
||||||
|
(str ":" (:port uri))))]
|
||||||
|
{:domain domain
|
||||||
|
:key "auth-token"
|
||||||
|
:value token}))
|
||||||
|
|
||||||
|
(defn pdf-from-object
|
||||||
|
[browser {:keys [file-id page-id object-id token scale type]}]
|
||||||
|
(letfn [(handle [page]
|
||||||
|
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||||
|
uri (-> (u/uri (cf/get :public-uri))
|
||||||
|
(assoc :path "/")
|
||||||
|
(assoc :fragment path))
|
||||||
|
cookie (create-cookie uri token)]
|
||||||
|
(pdf-from page (str uri) cookie)))
|
||||||
|
|
||||||
|
(pdf-from [page uri cookie]
|
||||||
|
(log/info :uri uri)
|
||||||
|
(let [options {:cookie cookie}]
|
||||||
|
(p/do!
|
||||||
|
(bw/configure-page! page options)
|
||||||
|
(bw/navigate! page uri)
|
||||||
|
(bw/wait-for page "#screenshot")
|
||||||
|
(bw/pdf page))))]
|
||||||
|
|
||||||
|
(bw/exec! browser handle)))
|
||||||
|
|
||||||
|
(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 ::filename ::us/string)
|
||||||
|
|
||||||
|
(s/def ::render-params
|
||||||
|
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
|
||||||
|
:opt-un [::filename]))
|
||||||
|
|
||||||
|
(defn render
|
||||||
|
[params]
|
||||||
|
(us/assert ::render-params params)
|
||||||
|
(let [browser @bw/instance]
|
||||||
|
(when-not browser
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :browser-not-ready
|
||||||
|
:hint "browser cluster is not initialized yet"))
|
||||||
|
|
||||||
|
(p/let [content (pdf-from-object browser params)]
|
||||||
|
{:content content
|
||||||
|
:filename (or (:filename params)
|
||||||
|
(str (:name params)
|
||||||
|
(:suffix params "")
|
||||||
|
".pdf"))
|
||||||
|
:length (alength content)
|
||||||
|
:mime-type "application/pdf"})))
|
||||||
|
|
|
@ -44,3 +44,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#screenshot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.math :as mth]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.data.fonts :as df]
|
[app.main.data.fonts :as df]
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
[app.main.ui.shapes.embed :as embed]
|
[app.main.ui.shapes.embed :as embed]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
@ -68,6 +70,11 @@
|
||||||
#(exports/shape-wrapper-factory objects))
|
#(exports/shape-wrapper-factory objects))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps width height)
|
||||||
|
#(dom/set-page-style {:size (str (mth/round width) "px "
|
||||||
|
(mth/round height) "px")}))
|
||||||
|
|
||||||
[:& (mf/provider embed/context) {:value true}
|
[:& (mf/provider embed/context) {:value true}
|
||||||
[:svg {:id "screenshot"
|
[:svg {:id "screenshot"
|
||||||
:view-box vbox
|
:view-box vbox
|
||||||
|
|
|
@ -36,6 +36,11 @@
|
||||||
(not (empty (:suffix (first exports)))))
|
(not (empty (:suffix (first exports)))))
|
||||||
(str (:suffix (first exports))))
|
(str (:suffix (first exports))))
|
||||||
|
|
||||||
|
scale-enabled?
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [export]
|
||||||
|
(#{:png :jpeg} (:type export))))
|
||||||
|
|
||||||
on-download
|
on-download
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps shape)
|
(mf/deps shape)
|
||||||
|
@ -111,6 +116,7 @@
|
||||||
(for [[index export] (d/enumerate exports)]
|
(for [[index export] (d/enumerate exports)]
|
||||||
[:div.element-set-options-group
|
[:div.element-set-options-group
|
||||||
{:key index}
|
{:key index}
|
||||||
|
(when (scale-enabled? export)
|
||||||
[:select.input-select {:on-change (partial on-scale-change index)
|
[:select.input-select {:on-change (partial on-scale-change index)
|
||||||
:value (:scale export)}
|
:value (:scale export)}
|
||||||
[:option {:value "0.5"} "0.5x"]
|
[:option {:value "0.5"} "0.5x"]
|
||||||
|
@ -119,7 +125,7 @@
|
||||||
[:option {:value "1.5"} "1.5x"]
|
[:option {:value "1.5"} "1.5x"]
|
||||||
[:option {:value "2"} "2x"]
|
[:option {:value "2"} "2x"]
|
||||||
[:option {:value "4"} "4x"]
|
[:option {:value "4"} "4x"]
|
||||||
[:option {:value "6"} "6x"]]
|
[:option {:value "6"} "6x"]])
|
||||||
[:input.input-text {:value (:suffix export)
|
[:input.input-text {:value (:suffix export)
|
||||||
:placeholder (tr "workspace.options.export.suffix")
|
:placeholder (tr "workspace.options.export.suffix")
|
||||||
:on-change (partial on-suffix-change index)}]
|
:on-change (partial on-suffix-change index)}]
|
||||||
|
@ -127,7 +133,8 @@
|
||||||
:on-change (partial on-type-change index)}
|
:on-change (partial on-type-change index)}
|
||||||
[:option {:value "png"} "PNG"]
|
[:option {:value "png"} "PNG"]
|
||||||
[:option {:value "jpeg"} "JPEG"]
|
[:option {:value "jpeg"} "JPEG"]
|
||||||
[:option {:value "svg"} "SVG"]]
|
[:option {:value "svg"} "SVG"]
|
||||||
|
[:option {:value "pdf"} "PDF"]]
|
||||||
[:div.delete-icon {:on-click (partial delete-export index)}
|
[:div.delete-icon {:on-click (partial delete-export index)}
|
||||||
i/minus]])
|
i/minus]])
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,18 @@
|
||||||
[title]
|
[title]
|
||||||
(set! (.-title globals/document) title))
|
(set! (.-title globals/document) title))
|
||||||
|
|
||||||
|
(defn set-page-style
|
||||||
|
[style]
|
||||||
|
(let [head (first (.getElementsByTagName ^js globals/document "head"))
|
||||||
|
style-str (str/join "\n"
|
||||||
|
(map (fn [[k v]]
|
||||||
|
(str (name k) ": " v ";"))
|
||||||
|
style))]
|
||||||
|
(.insertAdjacentHTML head "beforeend"
|
||||||
|
(str "<style>"
|
||||||
|
" @page {" style-str "}"
|
||||||
|
"</style>"))))
|
||||||
|
|
||||||
(defn get-element-by-class
|
(defn get-element-by-class
|
||||||
([classname]
|
([classname]
|
||||||
(dom/getElementByClass classname))
|
(dom/getElementByClass classname))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue