├── resources
├── public
│ └── stylesheets
│ │ ├── test.css
│ │ └── test_1.css
└── templates
│ └── test.html
├── .gitignore
├── test
└── pdfkit_clj
│ └── core_test.clj
├── project.clj
├── README.md
└── src
└── pdfkit_clj
└── core.clj
/resources/public/stylesheets/test.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 5em;
3 | }
4 |
--------------------------------------------------------------------------------
/resources/public/stylesheets/test_1.css:
--------------------------------------------------------------------------------
1 | html {
2 | text-decoration: underline;
3 | }
4 |
--------------------------------------------------------------------------------
/resources/templates/test.html:
--------------------------------------------------------------------------------
1 |
Ugly Joe Nobody!
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /lib
3 | /classes
4 | /checkouts
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | .lein-deps-sum
10 | .lein-failures
11 | .lein-plugins
12 | .lein-repl-history
13 | .lein-env
14 | .idea
15 |
--------------------------------------------------------------------------------
/test/pdfkit_clj/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns pdfkit-clj.core-test
2 | (:require [clojure.test :refer :all]
3 | [pdfkit-clj.core :refer :all]))
4 |
5 | (deftest a-test
6 | (testing "FIXME, I fail."
7 | (is (= 0 1))))
8 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject pdfkit-clj "0.1.7"
2 | :description "Generates PDFs using wkhtmltopdf"
3 | :url "https://github.com/banzai-inc/pdfkit-clj"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.5.1"]
7 | [enlive "1.1.2"]
8 | [clj-http "3.7.0"]
9 | [clj-time "0.6.0"]
10 | [org.apache.commons/commons-lang3 "3.1"]])
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pdfkit-clj
2 |
3 | A Clojure library for generating PDFs from HTML. Uses wkhtmltopdf, and insipred by Ruby's [pdfkit](https://github.com/pdfkit/pdfkit) and [ring-wicked-pdf](https://github.com/gberenfield/ring-wicked-pdf).
4 |
5 | ### Install
6 |
7 | Leiningen installation:
8 |
9 | ```
10 | [pdfkit-clj "0.1.7"]
11 | ```
12 |
13 | ### Usage
14 |
15 | ```clojure
16 | (require '[pdfkit-clj.core :refer :all])
17 | (def html "Hello!")
18 |
19 | (gen-pdf html)
20 |
21 | #
22 | ```
23 |
24 | You can also convert your file to an InputStream, ready for consumption by a browser (helpful for Ring applications):
25 |
26 | ```clojure
27 | (as-stream (gen-pdf html))
28 |
29 | #
30 | ```
31 |
32 | pdfkit-clj's `gen-pdf` can also accept HTML nodes (e.g. Enlive):
33 |
34 | ```clojure
35 | (defsnippet my-snippet
36 | ...)
37 |
38 | (gen-pdf (my-snippet) ...)
39 | ```
40 |
41 | ### Options:
42 |
43 | ```clojure
44 | (gen-pdf html
45 | :asset-path "public" ; Relative to your "resources" directory
46 | :stylesheets ["stylesheets/main.css" ; Local file
47 | "stylesheets/invoices.css"
48 | "https://public.yourdomain.com/stylesheets/main.css"] ; External asset
49 | :path "bin/wkhtmltopdf-amd64"
50 | :margin {:top 20 :right 15 :bottom 50 :left 15}
51 | :page-size "A4"
52 | :tmp "other/tmp")
53 | ```
54 |
55 | #### Defaults:
56 |
57 | ```clojure
58 | :path "wkhtmltopdf"
59 | :tmp "/tmp"
60 | :asset-path "resources/public"
61 | :margin {:top 10 :right 10 :bottom 10 :left 10} ;; in mm
62 | :page-size "A4"
63 | ```
64 |
65 | ### Images
66 |
67 | Right now, pdfkit-clj requires your image tags reference an absolute URL or URI on disk. Simply upload your image to S3, for example, and wkhtmltopdf will have access to it via the file's full URL.
68 |
69 | ### Heroku
70 |
71 | If you're like me, everything must work on Heroku. Here's the setup:
72 |
73 | #### 1. Download wkhtmltopdf to the `./bin` directory of your Leiningen project.
74 |
75 | ```
76 | mkdir bin
77 | cd bin
78 | wget https://wkhtmltopdf.googlecode.com/files/wkhtmltopdf-0.9.9-static-amd64.tar.bz2
79 | ```
80 |
81 | Finally, Unzip the file and rename it to `wkhtmltopdf`.
82 |
83 | #### 2. Call `gen-pdf` with appropriate paths:
84 |
85 | ```clojure
86 | (gen-pdf html :path "bin/wkhtmltopdf")
87 | ```
88 |
89 | ### License
90 |
91 | Copyright © 2018 Banzai Inc.
92 |
93 | Distributed under the Eclipse Public License, the same as Clojure.
94 |
--------------------------------------------------------------------------------
/src/pdfkit_clj/core.clj:
--------------------------------------------------------------------------------
1 | (ns pdfkit-clj.core
2 | (:require [clojure.java.io :as io]
3 | [clojure.java.shell :refer :all]
4 | [clj-http.client :as http]
5 | [clj-time.local :as local]
6 | [clj-time.format :as fmt]
7 | [clojure.string :as string]
8 | [net.cgrand.enlive-html :as e])
9 | (:import [org.apache.commons.lang3 StringEscapeUtils]))
10 |
11 | (def ^{:private true} defaults {:tmp "/tmp"
12 | :path "wkhtmltopdf"
13 | :asset-path "public"
14 | :margin {:top 10
15 | :right 10
16 | :bottom 10
17 | :left 10}
18 | :orientation "Portrait"
19 | :page-size "A4"})
20 |
21 | (defn- rand-tmp-file-name
22 | [tmp-dir]
23 | (str tmp-dir "/"
24 | "pdfkit-"
25 | (string/replace
26 | (fmt/unparse (fmt/formatters :basic-date-time)
27 | (local/local-now)) #"\." "")
28 | ".pdf"))
29 |
30 | (def uri-regex #"^http(s?)://")
31 |
32 | (defn- concat-styles
33 | "Takes a list of files and produces a single stylesheet."
34 | [asset-path http-opts stylesheets]
35 | (apply str (map (fn [s]
36 | (if (re-find uri-regex s)
37 | (-> (http/get s http-opts) :body)
38 | (slurp (io/resource (str asset-path "/" s))))) stylesheets)))
39 |
40 | (defn- append-styles
41 | "Appends stylesheets to the HTML's head tag."
42 | [html asset-path http-opts stylesheets]
43 | (let [styles (concat-styles asset-path http-opts stylesheets)]
44 | (e/at html
45 | [:head] (e/append (e/html [:style styles])))))
46 |
47 | (defmulti html-as-nodes class)
48 |
49 | (defmethod html-as-nodes String
50 | [html]
51 | (e/html-snippet html))
52 |
53 | (defmethod html-as-nodes :default [html] html)
54 |
55 | (defn- html-as-string
56 | [html]
57 | (StringEscapeUtils/unescapeXml
58 | (StringEscapeUtils/escapeHtml4
59 | (apply str (e/emit* html)))))
60 |
61 | (defn- top* [margin] (str (:top margin)))
62 | (defn- right* [margin] (str (:right margin)))
63 | (defn- bottom* [margin] (str (:bottom margin)))
64 | (defn- left* [margin] (str (:left margin)))
65 |
66 | (defn gen-pdf
67 | "Produces a PDF file given an html string."
68 | [html & {:keys [path tmp asset-path stylesheets margin orientation page-size cmd-args http-opts]
69 | :or {path (:path defaults)
70 | tmp (:tmp defaults)
71 | asset-path (:asset-path defaults)
72 | margin {}
73 | orientation (:orientation defaults)
74 | page-size (:page-size defaults)}}]
75 |
76 | (let [margin (merge (:margin defaults) margin)
77 | tmp-file-name (rand-tmp-file-name tmp)
78 | html (-> html
79 | (html-as-nodes)
80 | (append-styles asset-path http-opts stylesheets)
81 | (html-as-string))
82 | args
83 | (concat
84 | cmd-args
85 | ["-T" (top* margin) "-R" (right* margin)
86 | "-B" (bottom* margin) "-L" (left* margin)
87 | "-O" orientation
88 | "-s" page-size
89 | "-" tmp-file-name :in html])]
90 |
91 | (apply sh path args)
92 | (io/as-file tmp-file-name)))
93 |
94 | (defn as-stream
95 | "Given a file, returns PDF as stream. Helpful for Ring applications."
96 | [f]
97 | (io/input-stream f))
98 |
99 | ;(def html "Ugly Joe Nobody!™")
100 | ; (sh "open" (str (gen-pdf html
101 | ; :stylesheets ["stylesheets/test.css" "stylesheets/test_1.css" "https://www.example.com/test_2.css"]
102 | ; :margin {:top 50 :left 30}
103 | ; :cmd-args ["--zoom" "15"])))
104 |
--------------------------------------------------------------------------------