├── 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 | --------------------------------------------------------------------------------