├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── project
├── build.properties
└── plugins.sbt
├── sbt
├── src
├── main
│ └── scala
│ │ └── io
│ │ └── github
│ │ └── cloudify
│ │ └── scala.spdf
│ │ ├── DestinationDocumentLike.scala
│ │ ├── Exceptions.scala
│ │ ├── PageOrientation.scala
│ │ ├── ParamShow.scala
│ │ ├── Parameter.scala
│ │ ├── Pdf.scala
│ │ ├── PdfConfig.scala
│ │ ├── SourceDocumentLike.scala
│ │ └── WrappedPdf.scala
└── test
│ └── scala
│ └── io
│ └── github
│ └── cloudify
│ └── scala
│ └── spdf
│ ├── CommandOptionSpec.scala
│ ├── DestinationDocumentLikeSpec.scala
│ ├── ParamShowSpec.scala
│ ├── PdfConfigSpec.scala
│ ├── PdfSpec.scala
│ ├── SourceDocumentLikeSpec.scala
│ └── WrappedPdfSpec.scala
└── version.sbt
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | /.lib/
3 |
4 | *~
5 | /.ensime
6 |
7 | /.cache
8 | /.classpath
9 | /.project
10 | /.settings/
11 |
12 | /.idea
13 | /.idea_modules
14 | /bin/
15 | /.DS_Store
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - "2.10.6"
4 | - "2.11.8"
5 | - "2.12.0"
6 | jdk:
7 | - oraclejdk8
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | sPDF is Copyright © 2013 by Federico Feroldi
2 |
3 | Released under the terms of the MIT license.
4 | See http://opensource.org/licenses/MIT for full text.
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sPDF #
2 |
3 | sPDF ( pronounced _speedy-f_ ) is a Scala library that makes it super easy to create complex PDFs from plain old HTML, CSS and Javascript.
4 |
5 | On the backend it uses [wkhtmltopdf](http://wkhtmltopdf.org) which renders HTML using Webkit.
6 |
7 | __sPDF__ is heavily inspired by Ruby's [PdfKit](https://github.com/pdfkit/pdfkit) gem.
8 |
9 | The main features of __sPDF__ are:
10 |
11 | * full support of `wkhtmltopdf` extended parameters (see the source of the `PdfConfig` trait)
12 | * can read HTML from several sources: `java.io.File`, `java.io.InputStream`, `java.net.URL`, `scala.xml.Elem`, and `String`
13 | * can write PDFs to `File` and `OutputStream`
14 |
15 | The source HTML can reference to images and stylesheet files as long as the URLs point to the absolute path of the source file.
16 | It's also possible to embed javascript code in the pages, `wkhtmltopdf` will wait for the document ready event before generating the PDF.
17 |
18 | ## Installation ##
19 |
20 | Add the following to your sbt build for Scala 2.10, 2.11 and 2.12:
21 |
22 | ```scala
23 | libraryDependencies += "io.github.cloudify" %% "spdf" % "1.4.0"
24 | ```
25 |
26 | Add the following to your sbt build for Scala 2.9:
27 |
28 | ```scala
29 | libraryDependencies += "io.github.cloudify" %% "spdf" % "1.3.1"
30 | ```
31 |
32 | ## Usage ##
33 |
34 | ```scala
35 | import io.github.cloudify.scala.spdf._
36 | import java.io._
37 | import java.net._
38 |
39 | // Create a new Pdf converter with a custom configuration
40 | // run `wkhtmltopdf --extended-help` for a full list of options
41 | val pdf = Pdf(new PdfConfig {
42 | orientation := Landscape
43 | pageSize := "Letter"
44 | marginTop := "1in"
45 | marginBottom := "1in"
46 | marginLeft := "1in"
47 | marginRight := "1in"
48 | })
49 |
50 | val page =
Hello World
51 |
52 | // Save the PDF generated from the above HTML into a Byte Array
53 | val outputStream = new ByteArrayOutputStream
54 | pdf.run(page, outputStream)
55 |
56 | // Save the PDF of Google's homepage into a file
57 | pdf.run(new URL("http://www.google.com"), new File("google.pdf"))
58 | ```
59 |
60 | If you want to use sPDF in headless mode on debian you'll need to call to wkhtmltopdf through a virtualizer like xvfb-run.
61 | This is because wkhtmltopdf does not support running in headless mode on debian through the apt package. To use sPDF
62 | in this kind of environment you need to use WrappedPdf instead of Pdf. For Example:
63 |
64 | ```scala
65 | import io.github.cloudify.scala.spdf._
66 | import java.io._
67 | import java.net._
68 |
69 | // Create a new Pdf converter with a custom configuration
70 | // run `wkhtmltopdf --extended-help` for a full list of options
71 | val pdf = WrappedPdf(Seq("xvfb-run", "wkhtmltopdf"), new PdfConfig {
72 | orientation := Landscape
73 | pageSize := "Letter"
74 | marginTop := "1in"
75 | marginBottom := "1in"
76 | marginLeft := "1in"
77 | marginRight := "1in"
78 | })
79 |
80 | val page = Hello World
81 |
82 | // Save the PDF generated from the above HTML into a Byte Array
83 | val outputStream = new ByteArrayOutputStream
84 | pdf.run(page, outputStream)
85 |
86 | // Save the PDF of Google's homepage into a file
87 | pdf.run(new URL("http://www.google.com"), new File("google.pdf"))
88 | ```
89 |
90 | ## Installing wkhtmltopdf ##
91 |
92 | Visit the [wkhtmltopdf downloads page](http://wkhtmltopdf.org/downloads.html) and install the appropriate package for your platform.
93 |
94 | ## Troubleshooting ##
95 |
96 | ### NoExecutableException ###
97 |
98 | Make sure `wkhtmltopdf` is installed and your JVM is running with the correct `PATH` environment variable.
99 |
100 | If that doesn't work you can manually set the path to `wkhtmltopdf` when you create a new `Pdf` instance:
101 |
102 | ```scala
103 |
104 | val pdf = Pdf("/opt/bin/wkhtmltopdf", PdfConfig.default)
105 |
106 | ```
107 |
108 | ### Resources aren't included in the PDF ###
109 |
110 | Images, CSS, or JavaScript does not seem to be downloading correctly in the PDF. This is due to the fact that `wkhtmltopdf` does not know where to find those files. Make sure you are using absolute paths (start with forward slash) to your resources. If you are using PDFKit to generate PDFs from a raw HTML source make sure you use complete paths (either file paths or urls including the domain).
111 |
112 | ## Notes ##
113 |
114 | ### Asynchronous conversion ###
115 |
116 | __sPDF__ relyies on Scala's `scala.sys.process.Process` class to execute `wkhtmltopdf` and pipe input/output data.
117 |
118 | The execution of `wkhtmltopdf` and thus the conversion to PDF is blocking. If you need the processing to be asynchronous you can wrap the call inside a `Future`.
119 |
120 | ```scala
121 |
122 | val pdf = Pdf(PdfConfig.default)
123 |
124 | val result = Future { pdf.run(new URL("http://www.google.com"), new File("google.pdf")) }
125 |
126 | ```
127 |
128 | ## Contributing ##
129 |
130 | * Fork the project.
131 | * Make your feature addition or bug fix.
132 | * Add tests for it. This is important so I don't break it in a future version unintentionally.
133 | * Commit, do not mess with build settings, version, or history.
134 | * Send me a pull request. Bonus points for topic branches.
135 |
136 | ### Release / Publish ###
137 |
138 | * `release cross with-defaults`
139 | * check out released version
140 | * `publishSigned`
141 | * `sonatypeRelease`
142 |
143 | ## Roadmap ##
144 |
145 | - [X] Full support for extended options
146 | - [X] Full support for input types
147 | - [ ] Streaming API (with `scalaz-stream`)
148 | - [ ] Simplified API with implicits
149 | - [ ] Integration with Play for streaming PDFs in HTTP responses
150 |
151 | ## Copyright ##
152 |
153 | Copyright (c) 2013, 2014 Federico Feroldi. See `LICENSE` for details.
154 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 |
2 | name := "sPDF"
3 |
4 | description := "Create PDFs using plain old HTML+CSS. Uses wkhtmltopdf on the back-end which renders HTML using Webkit."
5 |
6 | homepage := Some(url("https://github.com/cloudify/sPDF"))
7 |
8 | startYear := Some(2013)
9 |
10 | licenses := Seq(
11 | ("MIT", url("http://opensource.org/licenses/MIT"))
12 | )
13 |
14 | organization := "io.github.cloudify"
15 |
16 | scalaVersion := "2.12.0"
17 |
18 | crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0")
19 |
20 | releaseCrossBuild := true
21 |
22 | scalacOptions ++= Seq(
23 | "-deprecation",
24 | "-unchecked",
25 | "-encoding", "UTF-8"
26 | )
27 |
28 | fork in Test := true
29 |
30 | parallelExecution in Test := false
31 |
32 | logLevel in compile := Level.Warn
33 |
34 | scmInfo := Some(
35 | ScmInfo(
36 | url("https://github.com/cloudify/sPDF"),
37 | "scm:git:https://github.com/cloudify/sPDF.git",
38 | Some("scm:git:git@github.com:cloudify/sPDF.git")
39 | )
40 | )
41 |
42 | // add dependencies on standard Scala modules, in a way
43 | // supporting cross-version publishing
44 | // taken from: http://github.com/scala/scala-module-dependency-sample
45 | libraryDependencies := {
46 | CrossVersion.partialVersion(scalaVersion.value) match {
47 | case Some((2, scalaMajor)) if scalaMajor >= 11 =>
48 | libraryDependencies.value ++ Seq(
49 | "org.scala-lang.modules" %% "scala-xml" % "1.0.6",
50 | "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4"
51 | )
52 | case _ =>
53 | libraryDependencies.value
54 | }
55 | }
56 |
57 | libraryDependencies ++= Seq (
58 | "org.scalatest" %% "scalatest" % "3.0.0" % "test",
59 | "org.mockito" % "mockito-all" % "1.10.8" % "test"
60 | )
61 |
62 | // publishing
63 | publishMavenStyle := true
64 |
65 | publishTo := {
66 | val nexus = "https://oss.sonatype.org/"
67 | if (version.value.trim.endsWith("SNAPSHOT"))
68 | Some("snapshots" at nexus + "content/repositories/snapshots")
69 | else
70 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
71 | }
72 |
73 | credentials += Credentials(Path.userHome / ".credentials.sonatype")
74 |
75 | publishArtifact in Test := false
76 |
77 | // publishArtifact in (Compile, packageDoc) := false
78 |
79 | // publishArtifact in (Compile, packageSrc) := false
80 |
81 | pomIncludeRepository := { _ => false }
82 |
83 | pomExtra := (
84 |
85 |
86 | cloudify
87 | Federico Feroldi
88 | pix@yahoo.it
89 | http://www.pixzone.com
90 |
91 |
92 | )
93 |
94 | // Josh Suereth's step-by-step guide to publishing on sonatype
95 | // http://www.scala-sbt.org/using_sonatype.html
96 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.13
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1")
2 |
3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
4 |
5 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3")
6 |
--------------------------------------------------------------------------------
/sbt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # A more capable sbt runner, coincidentally also called sbt.
4 | # Author: Paul Phillips
5 |
6 | # todo - make this dynamic
7 | declare -r sbt_release_version=0.12.1
8 | declare -r sbt_snapshot_version=0.13.0-SNAPSHOT
9 |
10 | unset sbt_jar sbt_dir sbt_create sbt_snapshot sbt_launch_dir
11 | unset scala_version java_home sbt_explicit_version
12 | unset verbose debug quiet
13 |
14 | for arg in "$@"; do
15 | case $arg in
16 | -q|-quiet) quiet=1 ;;
17 | *) ;;
18 | esac
19 | done
20 |
21 | build_props_sbt () {
22 | if [[ -f project/build.properties ]]; then
23 | versionLine=$(grep ^sbt.version project/build.properties)
24 | versionString=${versionLine##sbt.version=}
25 | echo "$versionString"
26 | fi
27 | }
28 |
29 | update_build_props_sbt () {
30 | local ver="$1"
31 | local old=$(build_props_sbt)
32 |
33 | if [[ $ver == $old ]]; then
34 | return
35 | elif [[ -f project/build.properties ]]; then
36 | perl -pi -e "s/^sbt\.version=.*\$/sbt.version=${ver}/" project/build.properties
37 | grep -q '^sbt.version=' project/build.properties || echo "sbt.version=${ver}" >> project/build.properties
38 |
39 | echo !!!
40 | echo !!! Updated file project/build.properties setting sbt.version to: $ver
41 | echo !!! Previous value was: $old
42 | echo !!!
43 | fi
44 | }
45 |
46 | sbt_version () {
47 | if [[ -n $sbt_explicit_version ]]; then
48 | echo $sbt_explicit_version
49 | else
50 | local v=$(build_props_sbt)
51 | if [[ -n $v ]]; then
52 | echo $v
53 | else
54 | echo $sbt_release_version
55 | fi
56 | fi
57 | }
58 |
59 | echoerr () {
60 | [[ -z $quiet ]] && echo 1>&2 "$@"
61 | }
62 | vlog () {
63 | [[ $verbose || $debug ]] && echoerr "$@"
64 | }
65 | dlog () {
66 | [[ $debug ]] && echoerr "$@"
67 | }
68 |
69 | # this seems to cover the bases on OSX, and someone will
70 | # have to tell me about the others.
71 | get_script_path () {
72 | local path="$1"
73 | [[ -L "$path" ]] || { echo "$path" ; return; }
74 |
75 | local target=$(readlink "$path")
76 | if [[ "${target:0:1}" == "/" ]]; then
77 | echo "$target"
78 | else
79 | echo "$(dirname $path)/$target"
80 | fi
81 | }
82 |
83 | # a ham-fisted attempt to move some memory settings in concert
84 | # so they need not be dicked around with individually.
85 | get_mem_opts () {
86 | local mem=${1:-1536}
87 | local perm=$(( $mem / 4 ))
88 | (( $perm > 256 )) || perm=256
89 | (( $perm < 1024 )) || perm=1024
90 | local codecache=$(( $perm / 2 ))
91 |
92 | echo "-Xms${mem}m -Xmx${mem}m -XX:MaxPermSize=${perm}m -XX:ReservedCodeCacheSize=${codecache}m"
93 | }
94 |
95 | die() {
96 | echo "Aborting: $@"
97 | exit 1
98 | }
99 |
100 | make_url () {
101 | groupid="$1"
102 | category="$2"
103 | version="$3"
104 |
105 | echo "http://typesafe.artifactoryonline.com/typesafe/ivy-$category/$groupid/sbt-launch/$version/sbt-launch.jar"
106 | }
107 |
108 | declare -r default_jvm_opts="-Dfile.encoding=UTF8"
109 | declare -r default_sbt_opts="-XX:+CMSClassUnloadingEnabled"
110 | declare -r default_sbt_mem=1536
111 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy"
112 | declare -r sbt_opts_file=".sbtopts"
113 | declare -r jvm_opts_file=".jvmopts"
114 | declare -r latest_28="2.8.2"
115 | declare -r latest_29="2.9.2"
116 | declare -r latest_210="2.10.0-SNAPSHOT"
117 |
118 | declare -r script_path=$(get_script_path "$BASH_SOURCE")
119 | declare -r script_dir="$(dirname $script_path)"
120 | declare -r script_name="$(basename $script_path)"
121 |
122 | # some non-read-onlies set with defaults
123 | declare java_cmd=java
124 | declare sbt_launch_dir="$script_dir/.lib"
125 | declare sbt_universal_launcher="$script_dir/lib/sbt-launch.jar"
126 | declare sbt_mem=$default_sbt_mem
127 | declare sbt_jar=$sbt_universal_launcher
128 |
129 | # pull -J and -D options to give to java.
130 | declare -a residual_args
131 | declare -a java_args
132 | declare -a scalac_args
133 | declare -a sbt_commands
134 |
135 | build_props_scala () {
136 | if [[ -f project/build.properties ]]; then
137 | versionLine=$(grep ^build.scala.versions project/build.properties)
138 | versionString=${versionLine##build.scala.versions=}
139 | echo ${versionString%% .*}
140 | fi
141 | }
142 |
143 | execRunner () {
144 | # print the arguments one to a line, quoting any containing spaces
145 | [[ $verbose || $debug ]] && echo "# Executing command line:" && {
146 | for arg; do
147 | if printf "%s\n" "$arg" | grep -q ' '; then
148 | printf "\"%s\"\n" "$arg"
149 | else
150 | printf "%s\n" "$arg"
151 | fi
152 | done
153 | echo ""
154 | }
155 |
156 | exec "$@"
157 | }
158 |
159 | sbt_groupid () {
160 | case $(sbt_version) in
161 | 0.7.*) echo org.scala-tools.sbt ;;
162 | 0.10.*) echo org.scala-tools.sbt ;;
163 | 0.11.[12]) echo org.scala-tools.sbt ;;
164 | *) echo org.scala-sbt ;;
165 | esac
166 | }
167 |
168 | sbt_artifactory_list () {
169 | local version0=$(sbt_version)
170 | local version=${version0%-SNAPSHOT}
171 | local url="http://typesafe.artifactoryonline.com/typesafe/ivy-snapshots/$(sbt_groupid)/sbt-launch/"
172 | dlog "Looking for snapshot list at: $url "
173 |
174 | curl -s --list-only "$url" | \
175 | grep -F $version | \
176 | perl -e 'print reverse <>' | \
177 | perl -pe 's#^/dev/null
191 | dlog "curl returned: $?"
192 | echo "$url"
193 | return
194 | done
195 | }
196 |
197 | jar_url () {
198 | case $(sbt_version) in
199 | 0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;;
200 | *-SNAPSHOT) make_snapshot_url ;;
201 | *) make_release_url ;;
202 | esac
203 | }
204 |
205 | jar_file () {
206 | echo "$sbt_launch_dir/$1/sbt-launch.jar"
207 | }
208 |
209 | download_url () {
210 | local url="$1"
211 | local jar="$2"
212 |
213 | echo "Downloading sbt launcher $(sbt_version):"
214 | echo " From $url"
215 | echo " To $jar"
216 |
217 | mkdir -p $(dirname "$jar") && {
218 | if which curl >/dev/null; then
219 | curl --fail --silent "$url" --output "$jar"
220 | elif which wget >/dev/null; then
221 | wget --quiet -O "$jar" "$url"
222 | fi
223 | } && [[ -f "$jar" ]]
224 | }
225 |
226 | acquire_sbt_jar () {
227 | sbt_url="$(jar_url)"
228 | sbt_jar="$(jar_file $(sbt_version))"
229 |
230 | [[ -f "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar"
231 | }
232 |
233 | usage () {
234 | cat < path to global settings/plugins directory (default: ~/.sbt/)
244 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+)
245 | -ivy path to local Ivy repository (default: ~/.ivy2)
246 | -mem set memory options (default: $sbt_mem, which is
247 | $(get_mem_opts $sbt_mem) )
248 | -no-share use all local caches; no sharing
249 | -offline put sbt in offline mode
250 | -jvm-debug Turn on JVM debugging, open at the given port.
251 | -batch Disable interactive mode
252 |
253 | # sbt version (default: from project/build.properties if present, else latest release)
254 | !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which
255 | !!! contains an sbt.version property is to update the file on disk. That's what this does.
256 | -sbt-version use the specified version of sbt
257 | -sbt-jar use the specified jar as the sbt launcher
258 | -sbt-snapshot use a snapshot version of sbt
259 | -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir)
260 |
261 | # scala version (default: as chosen by sbt)
262 | -28 use $latest_28
263 | -29 use $latest_29
264 | -210 use $latest_210
265 | -scala-home use the scala build at the specified directory
266 | -scala-version use the specified version of scala
267 |
268 | # java version (default: java from PATH, currently $(java -version |& grep version))
269 | -java-home alternate JAVA_HOME
270 |
271 | # jvm options and output control
272 | JAVA_OPTS environment variable holding jvm args, if unset uses "$default_jvm_opts"
273 | SBT_OPTS environment variable holding jvm args, if unset uses "$default_sbt_opts"
274 | .jvmopts if file is in sbt root, it is prepended to the args given to the jvm
275 | .sbtopts if file is in sbt root, it is prepended to the args given to **sbt**
276 | -Dkey=val pass -Dkey=val directly to the jvm
277 | -J-X pass option -X directly to the jvm (-J is stripped)
278 | -S-X add -X to sbt's scalacOptions (-J is stripped)
279 |
280 | In the case of duplicated or conflicting options, the order above
281 | shows precedence: JAVA_OPTS lowest, command line options highest.
282 | EOM
283 | }
284 |
285 | addJava () {
286 | dlog "[addJava] arg = '$1'"
287 | java_args=( "${java_args[@]}" "$1" )
288 | }
289 | addSbt () {
290 | dlog "[addSbt] arg = '$1'"
291 | sbt_commands=( "${sbt_commands[@]}" "$1" )
292 | }
293 | addScalac () {
294 | dlog "[addScalac] arg = '$1'"
295 | scalac_args=( "${scalac_args[@]}" "$1" )
296 | }
297 | addResidual () {
298 | dlog "[residual] arg = '$1'"
299 | residual_args=( "${residual_args[@]}" "$1" )
300 | }
301 | addResolver () {
302 | addSbt "set resolvers in ThisBuild += $1"
303 | }
304 | addDebugger () {
305 | addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"
306 | }
307 | get_jvm_opts () {
308 | # echo "${JAVA_OPTS:-$default_jvm_opts}"
309 | # echo "${SBT_OPTS:-$default_sbt_opts}"
310 |
311 | [[ -f "$jvm_opts_file" ]] && cat "$jvm_opts_file"
312 | }
313 |
314 | process_args ()
315 | {
316 | require_arg () {
317 | local type="$1"
318 | local opt="$2"
319 | local arg="$3"
320 |
321 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then
322 | die "$opt requires <$type> argument"
323 | fi
324 | }
325 | while [[ $# -gt 0 ]]; do
326 | case "$1" in
327 | -h|-help) usage; exit 1 ;;
328 | -v|-verbose) verbose=1 && shift ;;
329 | -d|-debug) debug=1 && shift ;;
330 | -q|-quiet) quiet=1 && shift ;;
331 |
332 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;;
333 | -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;;
334 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;;
335 | -no-share) addJava "$noshare_opts" && shift ;;
336 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;;
337 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;;
338 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;;
339 | -offline) addSbt "set offline := true" && shift ;;
340 | -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;;
341 | -batch) exec 0 )) || echo "Starting $script_name: invoke with -help for other options"
403 |
404 | # verify this is an sbt dir or -create was given
405 | [[ -f ./build.sbt || -d ./project || -n "$sbt_create" ]] || {
406 | cat < destinationDocument
40 |
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/Exceptions.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import java.net.URL
4 |
5 | /**
6 | * Thrown in case the path to wkhtmltopdf is invalid
7 | */
8 | case class NoExecutableException(path: String) extends
9 | Exception("No wkhtmltopdf executable found at %s".format(path))
10 |
11 | /**
12 | * Thrown in case a URL with unsupported protocol is used as source
13 | * @param url
14 | */
15 | case class UnsupportedProtocolException(url: URL) extends
16 | Exception("The protocol is not supported by wkhtmltopdf".format(url.getProtocol))
17 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/PageOrientation.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | sealed trait PageOrientation {
4 | val value: String
5 | }
6 |
7 | object Landscape extends PageOrientation {
8 | override val value = "Landscape"
9 | }
10 |
11 | object Portrait extends PageOrientation {
12 | override val value = "Portrait"
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/ParamShow.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | /**
4 | * Renders a parameter of type T to a sequence of strings that will be
5 | * appended to the command line.
6 | */
7 | trait ParamShow[T] {
8 | def show(name: String, value: T): Iterable[String]
9 | }
10 |
11 | object ParamShow {
12 |
13 | implicit object StringParamShow extends ParamShow[String] {
14 | override def show(name: String, value: String): Iterable[String] =
15 | formatParam(name, Some(value))
16 | }
17 |
18 | implicit object BooleanParamShow extends ParamShow[Boolean] {
19 | override def show(name: String, value: Boolean): Iterable[String] = value match {
20 | case true => formatParam(name, None)
21 | case _ => Iterable.empty
22 | }
23 | }
24 |
25 | implicit object IterableParamShow extends ParamShow[Iterable[String]] {
26 | override def show(name: String, value: Iterable[String]): Iterable[String] = {
27 | value flatMap (x => formatParam(name, Some(x)))
28 | }
29 | }
30 |
31 | implicit object OptionBooleanParamShow extends ParamShow[Option[Boolean]] {
32 | override def show(name: String, valueOpt: Option[Boolean]): Iterable[String] =
33 | valueOpt.toIterable.flatMap { formatParam(name, _) }
34 | }
35 |
36 | implicit object IntParamShow extends ParamShow[Int] {
37 | override def show(name: String, value: Int): Iterable[String] =
38 | formatParam(name, Some(value.toString))
39 | }
40 |
41 | implicit object FloatParamShow extends ParamShow[Float] {
42 | override def show(name: String, value: Float): Iterable[String] =
43 | formatParam(name, Some("%.2f".format(value)))
44 | }
45 |
46 | implicit object PageOrientationParamShow extends ParamShow[PageOrientation] {
47 | override def show(name: String, value: PageOrientation): Iterable[String] =
48 | formatParam(name, Some(value.value))
49 | }
50 |
51 | private def formatParam(name: String, value: Option[String]): Iterable[String] =
52 | Seq(Some("--" + name), value).flatten
53 |
54 | private def formatParam(name: String, value: Boolean): Iterable[String] = if(value) {
55 | Some("--" + name)
56 | } else {
57 | Some("--no-" + name)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/Parameter.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | /**
4 | * Describes a command line option
5 | */
6 | trait Parameter[T] {
7 | /**
8 | * The underlying value of this option
9 | */
10 | private var underlying: Option[T] = None
11 |
12 | /**
13 | * The commandline name for this parameter
14 | */
15 | val name: String
16 |
17 | /**
18 | * The optional default value for this parameter
19 | */
20 | val default: Option[T] = None
21 |
22 | /**
23 | * Sets a new value for this parameter
24 | */
25 | def :=(newValue: T): Unit = underlying = Some(newValue)
26 |
27 | /**
28 | * Converts this option to a sequence of strings to be appended to the
29 | * command line
30 | */
31 | def toParameter(implicit shower: ParamShow[T]): Iterable[String] = value match {
32 | case Some(v) => shower.show(name, v)
33 | case _ => Iterable.empty
34 | }
35 |
36 | /**
37 | * Provides the current value for the option
38 | */
39 | private def value: Option[T] = underlying orElse default
40 |
41 | }
42 |
43 | object Parameter {
44 |
45 | /**
46 | * Creates a new CommandOption with the specified name and default value
47 | */
48 | def apply[T : ParamShow](commandName: String, defaultValue: T): Parameter[T] =
49 | new Parameter[T] {
50 | override val name: String = commandName
51 | override val default: Option[T] = Some(defaultValue)
52 | }
53 |
54 | /**
55 | * Creates a new CommandOption with the specified name
56 | */
57 | def apply[T : ParamShow](commandName: String): Parameter[T] =
58 | new Parameter[T] {
59 | override val name: String = commandName
60 | override val default: Option[T] = None
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/Pdf.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import scala.sys.process._
4 | import java.io.File
5 |
6 | class Pdf(executablePath: String, config: PdfConfig) {
7 | validateExecutable_!(executablePath)
8 |
9 | /**
10 | * Runs the conversion tool to convert sourceDocument HTML into
11 | * destinationDocument PDF.
12 | */
13 | def run[A, B](sourceDocument: A, destinationDocument: B)(implicit sourceDocumentLike: SourceDocumentLike[A], destinationDocumentLike: DestinationDocumentLike[B]): Int = {
14 | val commandLine = toCommandLine(sourceDocument, destinationDocument)
15 | val process = Process(commandLine)
16 | def source = sourceDocumentLike.sourceFrom(sourceDocument) _
17 | def sink = destinationDocumentLike.sinkTo(destinationDocument) _
18 |
19 | (sink compose source)(process).!
20 | }
21 |
22 | /**
23 | * Generates the command line needed to execute `wkhtmltopdf`
24 | */
25 | private def toCommandLine[A: SourceDocumentLike, B: DestinationDocumentLike](source: A, destination: B): Seq[String] =
26 | Seq(executablePath) ++
27 | PdfConfig.toParameters(config) ++
28 | Seq(
29 | "--quiet",
30 | implicitly[SourceDocumentLike[A]].commandParameter(source),
31 | implicitly[DestinationDocumentLike[B]].commandParameter(destination)
32 | )
33 |
34 | /**
35 | * Check whether the executable is actually executable, if it isn't
36 | * a NoExecutableException is thrown.
37 | */
38 | private def validateExecutable_!(executablePath: String): Unit = {
39 | val executableFile = new File(executablePath)
40 | if(!executableFile.canExecute) throw new NoExecutableException(executableFile.getAbsolutePath)
41 | }
42 |
43 | }
44 |
45 | object Pdf {
46 |
47 | /**
48 | * Creates a new instance of Pdf with default configuration
49 | * @return
50 | */
51 | def apply(config: PdfConfig): Pdf = {
52 | val executablePath: String = PdfConfig.findExecutable.getOrElse {
53 | throw new NoExecutableException(System.getenv("PATH"))
54 | }
55 |
56 | apply(executablePath, config)
57 | }
58 |
59 | /**
60 | * Creates a new instance of Pdf with the passed configuration
61 | */
62 | def apply(executablePath: String, config: PdfConfig): Pdf =
63 | new Pdf(executablePath, config)
64 |
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/PdfConfig.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import scala.sys.process._
4 | import ParamShow._
5 |
6 | /**
7 | * Holds the configuration parameters of Pdf Kit
8 | */
9 | trait PdfConfig {
10 |
11 | /**
12 | * Options for `wkhtmltopdf` command
13 | * See `wkhtmltopdf --extended-help` for a description of each option
14 | */
15 |
16 | val allow = Parameter[Iterable[String]]("allow")
17 |
18 | val defaultHeader = Parameter[Boolean]("default-header")
19 |
20 | val disableExternalLinks = Parameter[Boolean]("disable-external-links")
21 |
22 | val disableInternalLinks = Parameter[Boolean]("disable-internal-links")
23 |
24 | val disableJavascript = Parameter[Boolean]("disable-javascript")
25 |
26 | @deprecated("Use noPdfCompression instead", "1.3.1")
27 | val disablePdfCompression = Parameter[Boolean]("disable-pdf-compression")
28 |
29 | val noPdfCompression = Parameter[Boolean]("no-pdf-compression")
30 |
31 | val disableSmartShrinking = Parameter[Boolean]("disable-smart-shrinking")
32 |
33 | val javascriptDelay = Parameter[Int]("javascript-delay")
34 |
35 | val enableForms = Parameter[Boolean]("enable-forms")
36 |
37 | val encoding = Parameter[String]("encoding", "UTF-8")
38 |
39 | val grayScale = Parameter[Boolean]("grayscale")
40 |
41 | val lowQuality = Parameter[Boolean]("lowquality")
42 |
43 | val marginBottom = Parameter[String]("margin-bottom")
44 |
45 | val marginLeft = Parameter[String]("margin-left")
46 |
47 | val marginRight = Parameter[String]("margin-right")
48 |
49 | val marginTop = Parameter[String]("margin-top")
50 |
51 | val minimumFontSize = Parameter[Int]("minimum-font-size")
52 |
53 | val background = Parameter[Option[Boolean]]("background")
54 |
55 | val orientation = Parameter[PageOrientation]("orientation")
56 |
57 | val pageHeight = Parameter[String]("page-height")
58 |
59 | val pageOffset = Parameter[String]("page-offset")
60 |
61 | val pageSize = Parameter[String]("page-size")
62 |
63 | val pageWidth = Parameter[String]("page-width")
64 |
65 | val title = Parameter[String]("title")
66 |
67 | val tableOfContent = Parameter[Boolean]("toc")
68 |
69 | val zoom = Parameter[Float]("zoom")
70 |
71 | val footerCenter = Parameter[String]("footer-center")
72 |
73 | val footerFontName = Parameter[String]("footer-font-name")
74 |
75 | val footerFontSize = Parameter[String]("footer-font-size")
76 |
77 | val footerHtml = Parameter[String]("footer-html")
78 |
79 | val footerLeft = Parameter[String]("footer-left")
80 |
81 | val footerLine = Parameter[Boolean]("footer-line")
82 |
83 | val footerRight = Parameter[String]("footer-right")
84 |
85 | val footerSpacing = Parameter[Float]("footer-spacing")
86 |
87 | val headerCenter = Parameter[String]("header-center")
88 |
89 | val headerFontName = Parameter[String]("header-font-name")
90 |
91 | val headerFontSize = Parameter[String]("header-font-size")
92 |
93 | val headerHtml = Parameter[String]("header-html")
94 |
95 | val headerLeft = Parameter[String]("header-left")
96 |
97 | val headerLine = Parameter[Option[Boolean]]("header-line")
98 |
99 | val headerRight = Parameter[String]("header-right")
100 |
101 | val headerSpacing = Parameter[Float]("header-spacing")
102 |
103 | val tableOfContentDepth = Parameter[Int]("toc-depth")
104 |
105 | val tableOfContentDisableBackLinks = Parameter[Boolean]("toc-disable-back-links")
106 |
107 | val tableOfContentDisableLinks = Parameter[Boolean]("toc-disable-links")
108 |
109 | val tableOfContentFontName = Parameter[String]("toc-font-name")
110 |
111 | val tableOfContentHeaderFontName = Parameter[String]("toc-header-font-name")
112 |
113 | val tableOfContentHeaderFontSize = Parameter[Int]("toc-header-font-size")
114 |
115 | val tableOfContentHeaderText = Parameter[String]("toc-header-text")
116 |
117 | val tableOfContentLevel1FontSize = Parameter[Int]("toc-l1-font-size")
118 |
119 | val tableOfContentLevel1Indentation = Parameter[Int]("toc-l1-indentation")
120 |
121 | val tableOfContentLevel2FontSize = Parameter[Int]("toc-l2-font-size")
122 |
123 | val tableOfContentLevel2Indentation = Parameter[Int]("toc-l2-indentation")
124 |
125 | val tableOfContentLevel3FontSize = Parameter[Int]("toc-l3-font-size")
126 |
127 | val tableOfContentLevel3Indentation = Parameter[Int]("toc-l3-indentation")
128 |
129 | val tableOfContentLevel4FontSize = Parameter[Int]("toc-l4-font-size")
130 |
131 | val tableOfContentLevel4Indentation = Parameter[Int]("toc-l4-indentation")
132 |
133 | val tableOfContentLevel5FontSize = Parameter[Int]("toc-l5-font-size")
134 |
135 | val tableOfContentLevel5Indentation = Parameter[Int]("toc-l5-indentation")
136 |
137 | val tableOfContentLevel6FontSize = Parameter[Int]("toc-l6-font-size")
138 |
139 | val tableOfContentLevel6Indentation = Parameter[Int]("toc-l6-indentation")
140 |
141 | val tableOfContentLevel7FontSize = Parameter[Int]("toc-l7-font-size")
142 |
143 | val tableOfContentLevel7Indentation = Parameter[Int]("toc-l7-indentation")
144 |
145 | val tableOfContentNoDots = Parameter[Boolean]("toc-no-dots")
146 |
147 | val outline = Parameter[Option[Boolean]]("outline")
148 |
149 | val outlineDepth = Parameter[Int]("outline-depth")
150 |
151 | val printMediaType = Parameter[Option[Boolean]]("print-media-type")
152 |
153 | val userStyleSheet = Parameter[String]("user-style-sheet")
154 |
155 | val username = Parameter[String]("username")
156 |
157 | val password = Parameter[String]("password")
158 |
159 | val viewportSize = Parameter[String]("viewport-size")
160 |
161 | val useXServer = Parameter[Boolean]("use-xserver")
162 | }
163 |
164 | object PdfConfig {
165 |
166 | /**
167 | * An instance of the default configuration
168 | */
169 | object default extends PdfConfig
170 |
171 | /**
172 | * Generates a sequence of command line parameters from a `PdfKitConfig`
173 | */
174 | def toParameters(config: PdfConfig): Seq[String] = {
175 | import config._
176 | Seq(
177 | allow.toParameter,
178 | background.toParameter,
179 | defaultHeader.toParameter,
180 | disableExternalLinks.toParameter,
181 | disableInternalLinks.toParameter,
182 | disableJavascript.toParameter,
183 | noPdfCompression.toParameter,
184 | disableSmartShrinking.toParameter,
185 | javascriptDelay.toParameter,
186 | enableForms.toParameter,
187 | encoding.toParameter,
188 | footerCenter.toParameter,
189 | footerFontName.toParameter,
190 | footerFontSize.toParameter,
191 | footerHtml.toParameter,
192 | footerLeft.toParameter,
193 | footerLine.toParameter,
194 | footerRight.toParameter,
195 | footerSpacing.toParameter,
196 | grayScale.toParameter,
197 | headerCenter.toParameter,
198 | headerFontName.toParameter,
199 | headerFontSize.toParameter,
200 | headerHtml.toParameter,
201 | headerLeft.toParameter,
202 | headerLine.toParameter,
203 | headerRight.toParameter,
204 | headerSpacing.toParameter,
205 | lowQuality.toParameter,
206 | marginBottom.toParameter,
207 | marginLeft.toParameter,
208 | marginRight.toParameter,
209 | marginTop.toParameter,
210 | minimumFontSize.toParameter,
211 | orientation.toParameter,
212 | outline.toParameter,
213 | outlineDepth.toParameter,
214 | pageHeight.toParameter,
215 | pageOffset.toParameter,
216 | pageSize.toParameter,
217 | pageWidth.toParameter,
218 | password.toParameter,
219 | printMediaType.toParameter,
220 | tableOfContent.toParameter,
221 | tableOfContentDepth.toParameter,
222 | tableOfContentDisableBackLinks.toParameter,
223 | tableOfContentDisableLinks.toParameter,
224 | tableOfContentFontName.toParameter,
225 | tableOfContentHeaderFontName.toParameter,
226 | tableOfContentHeaderFontSize.toParameter,
227 | tableOfContentHeaderText.toParameter,
228 | tableOfContentLevel1FontSize.toParameter,
229 | tableOfContentLevel1Indentation.toParameter,
230 | tableOfContentLevel2FontSize.toParameter,
231 | tableOfContentLevel2Indentation.toParameter,
232 | tableOfContentLevel3FontSize.toParameter,
233 | tableOfContentLevel3Indentation.toParameter,
234 | tableOfContentLevel4FontSize.toParameter,
235 | tableOfContentLevel4Indentation.toParameter,
236 | tableOfContentLevel5FontSize.toParameter,
237 | tableOfContentLevel5Indentation.toParameter,
238 | tableOfContentLevel6FontSize.toParameter,
239 | tableOfContentLevel6Indentation.toParameter,
240 | tableOfContentLevel7FontSize.toParameter,
241 | tableOfContentLevel7Indentation.toParameter,
242 | tableOfContentNoDots.toParameter,
243 | title.toParameter,
244 | userStyleSheet.toParameter,
245 | username.toParameter,
246 | useXServer.toParameter,
247 | viewportSize.toParameter,
248 | zoom.toParameter
249 | ).flatten
250 | }
251 |
252 | /**
253 | * Attempts to find the `wkhtmltopdf` executable in the system path.
254 | * @return
255 | */
256 | def findExecutable: Option[String] = try {
257 | val os = System.getProperty("os.name").toLowerCase
258 | val cmd = if(os.contains("windows")) "where wkhtmltopdf" else "which wkhtmltopdf"
259 |
260 | Option(cmd.!!.trim).filter(_.nonEmpty)
261 | } catch {
262 | case _: RuntimeException => None
263 | }
264 |
265 | }
266 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/SourceDocumentLike.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import java.io.{ByteArrayInputStream, InputStream, File}
4 | import scala.sys.process._
5 | import java.net.URL
6 | import scala.annotation.implicitNotFound
7 | import scala.xml.Elem
8 |
9 | /**
10 | * Type class that describes the kind of source documents we can read the
11 | * input HTML from.
12 | */
13 | @implicitNotFound(msg = "Cannot find SourceDocumentLike type class for ${A}")
14 | trait SourceDocumentLike[-A] {
15 |
16 | /**
17 | * The source parameter to provide to `wkhtmltopdf`
18 | * Defaults to read from STDIN.
19 | */
20 | def commandParameter(sourceDocument: A): String = "-"
21 |
22 | /**
23 | * Source the input of the process from this sourceDocument
24 | * Defaults to passthrough.
25 | */
26 | def sourceFrom(sourceDocument: A)(process: ProcessBuilder): ProcessBuilder =
27 | process
28 |
29 | }
30 |
31 | object SourceDocumentLike {
32 |
33 | /**
34 | * Pipes the InputStream into the process STDIN
35 | */
36 | implicit object InputStreamSourceDocument extends SourceDocumentLike[InputStream] {
37 |
38 | override def sourceFrom(sourceDocument: InputStream)(process: ProcessBuilder): ProcessBuilder =
39 | process #< sourceDocument
40 |
41 | }
42 |
43 | /**
44 | * Sets the file absolute path as the input parameter
45 | */
46 | implicit object FileSourceDocument extends SourceDocumentLike[File] {
47 |
48 | override def commandParameter(sourceDocument: File): String =
49 | sourceDocument.getAbsolutePath
50 |
51 | }
52 |
53 | /**
54 | * Pipes a UTF-8 string into the process STDIN
55 | */
56 | implicit object StringSourceDocument extends SourceDocumentLike[String] {
57 |
58 | override def sourceFrom(sourceDocument: String)(process: ProcessBuilder) =
59 | process #< toInputStream(sourceDocument)
60 |
61 | private def toInputStream(sourceDocument: String): ByteArrayInputStream =
62 | new ByteArrayInputStream(sourceDocument.getBytes("UTF-8"))
63 |
64 | }
65 |
66 | /**
67 | * Sets the URL as the input parameter
68 | */
69 | implicit object URLSourceDocument extends SourceDocumentLike[URL] {
70 |
71 | override def commandParameter(sourceDocument: URL) = sourceDocument.getProtocol match {
72 | case "https" | "http" | "file" => sourceDocument.toString
73 | case _ => throw new UnsupportedProtocolException(sourceDocument)
74 | }
75 |
76 | }
77 |
78 | /**
79 | * Sets the XML node as the input parameter
80 | */
81 | implicit object XmlSourceDocument extends SourceDocumentLike[Elem] {
82 |
83 | override def sourceFrom(sourceDocument: Elem)(process: ProcessBuilder) =
84 | process #< toInputStream(sourceDocument)
85 |
86 | private def toInputStream(sourceDocument: Elem): ByteArrayInputStream =
87 | new ByteArrayInputStream(sourceDocument.toString().getBytes("UTF-8"))
88 |
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/scala/io/github/cloudify/scala.spdf/WrappedPdf.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import scala.sys.process.Process
4 |
5 | class WrappedPdf(executable: Seq[String], config: PdfConfig) {
6 |
7 | def run[A, B](sourceDocument: A, destinationDocument: B)(implicit sourceDocumentLike: SourceDocumentLike[A], destinationDocumentLike: DestinationDocumentLike[B]): Int = {
8 | val commandLine = toCommandLine(sourceDocument, destinationDocument)
9 | val process = Process(commandLine)
10 |
11 | def source = sourceDocumentLike.sourceFrom(sourceDocument) _
12 | def sink = destinationDocumentLike.sinkTo(destinationDocument) _
13 |
14 | (sink compose source)(process).!
15 | }
16 |
17 | def toCommandLine[A: SourceDocumentLike, B: DestinationDocumentLike](source: A, destination: B): Seq[String] =
18 | executable ++
19 | PdfConfig.toParameters(config) ++
20 | Seq(
21 | "--quiet",
22 | implicitly[SourceDocumentLike[A]].commandParameter(source),
23 | implicitly[DestinationDocumentLike[B]].commandParameter(destination)
24 | )
25 | }
26 |
27 | object WrappedPdf {
28 | def apply(executable: Seq[String], config: PdfConfig): WrappedPdf = new WrappedPdf(executable, config)
29 | }
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/CommandOptionSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import org.scalatest.WordSpec
4 | import org.scalatest.Matchers
5 |
6 | class CommandOptionSpec extends WordSpec with Matchers {
7 |
8 | def instantiatedWith = afterWord("instantiated with")
9 |
10 | trait commandOptionWithDefault {
11 | lazy val commandOption = Parameter("param", "default")
12 | }
13 |
14 | trait commandOptionWithoutDefault {
15 | lazy val commandOption = Parameter[String]("param")
16 | }
17 |
18 | "A CommandOption" when instantiatedWith {
19 |
20 | "a default value" when {
21 |
22 | "not updated" should {
23 | "generate a parameter with the default value" in new commandOptionWithDefault {
24 | commandOption.toParameter should equal(Seq("--param", "default"))
25 | }
26 | }
27 |
28 | "updated with a new value" should {
29 | "generate a parameter with the new value" in new commandOptionWithDefault {
30 | commandOption := "newvalue"
31 | commandOption.toParameter should equal(Seq("--param", "newvalue"))
32 | }
33 | }
34 |
35 | }
36 |
37 | "no default value" when {
38 | "not updated" should {
39 | "do not generate a parameter" in new commandOptionWithoutDefault {
40 | commandOption.toParameter should equal(Iterable.empty)
41 | }
42 | }
43 |
44 | "updated with a new value" should {
45 | "generate a parameter with the new value" in new commandOptionWithoutDefault {
46 | commandOption := "newvalue"
47 | commandOption.toParameter should equal(Seq("--param", "newvalue"))
48 | }
49 | }
50 | }
51 |
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/DestinationDocumentLikeSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import java.io.{OutputStream, ByteArrayOutputStream, File}
4 | import io.github.cloudify.scala.spdf.DestinationDocumentLike.{OutputStreamDestinationDocument, FileDestinationDocument}
5 | import scala.sys.process._
6 | import org.scalatest.WordSpec
7 | import org.scalatest.mockito.MockitoSugar
8 | import org.scalatest.Matchers
9 |
10 | class DestinationDocumentLikeSpec extends WordSpec with Matchers with MockitoSugar {
11 |
12 | trait catProcess {
13 | val process = Process(Seq("cat", "-"))
14 | }
15 |
16 | "DestinationDocumentLike" should {
17 | "set commandParameter to -" in {
18 | new DestinationDocumentLike[Unit] {}.commandParameter(Unit) should equal("-")
19 | }
20 |
21 | "leave process untouched" in new catProcess {
22 | new DestinationDocumentLike[Unit] {}.sinkTo(Unit)(process) should equal(process)
23 | }
24 | }
25 |
26 | "FileDestinationDocument" should {
27 | "set commandParameter to the absolute file path" in {
28 | val file = new File("test")
29 | FileDestinationDocument.commandParameter(file) should equal(file.getAbsolutePath)
30 | }
31 |
32 | "leave process untouched" in new catProcess {
33 | FileDestinationDocument.sinkTo(mock[File])(process) should equal(process)
34 | }
35 | }
36 |
37 | "OutputStreamDestinationDocument" should {
38 | "set commandParameter to -" in {
39 | OutputStreamDestinationDocument.commandParameter(mock[OutputStream]) should equal("-")
40 | }
41 |
42 | "pipe process STDOUT into destination stream" in new catProcess {
43 | // need to fix https://github.com/cloudify/sPDF/issues/36
44 | pending
45 | val destinationDocument = new ByteArrayOutputStream()
46 | val processWithDestination = OutputStreamDestinationDocument.sinkTo(destinationDocument)(process)
47 |
48 | val exitValue = (Seq("echo", "-n", "Hello world") #> processWithDestination).!
49 | // exitValue should equal(0)
50 |
51 | destinationDocument.toString should equal("Hello world")
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/ParamShowSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import io.github.cloudify.scala.spdf.ParamShow.{BooleanParamShow, StringParamShow, IterableParamShow}
4 | import org.scalatest.WordSpec
5 | import org.scalatest.Matchers
6 |
7 | class ParamShowSpec extends WordSpec with Matchers {
8 |
9 | "StringParamShow" should {
10 |
11 | "represent a parameter" in {
12 | StringParamShow.show("param", "value") should equal(Seq("--param", "value"))
13 | }
14 |
15 | }
16 |
17 | "BooleanParamShow" should {
18 |
19 | "represent a parameter when true" in {
20 | BooleanParamShow.show("param", value = true) should equal(Seq("--param"))
21 | }
22 |
23 | "return empty parameter when false" in {
24 | BooleanParamShow.show("param", value = false) should equal(Iterable.empty)
25 | }
26 |
27 | }
28 |
29 | "IterableParamShow" should {
30 |
31 | "represent a repeatable parameter" in {
32 | IterableParamShow.show("param", List("a", "b")) should equal(Seq("--param", "a", "--param", "b"))
33 | }
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/PdfConfigSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import org.scalatest.Matchers
4 | import org.scalatest.WordSpec
5 |
6 | class PdfConfigSpec extends WordSpec with Matchers {
7 |
8 | "PdfConfig" should {
9 |
10 | "have a default config" in {
11 | PdfConfig.toParameters(PdfConfig.default) should equal(Seq("--encoding", "UTF-8"))
12 | }
13 |
14 | "generate parameters from config" in {
15 | val config = new PdfConfig {
16 | enableForms := true
17 | marginBottom := "1in"
18 | minimumFontSize := 3
19 | orientation := Landscape
20 | zoom := 1.23f
21 | }
22 | PdfConfig.toParameters(config) should equal(Seq("--enable-forms", "--encoding", "UTF-8", "--margin-bottom", "1in", "--minimum-font-size", "3", "--orientation", "Landscape", "--zoom", "%.2f".format(1.23f)))
23 | }
24 |
25 | "no pdf compression" in {
26 | val config = new PdfConfig {
27 | noPdfCompression := true
28 | }
29 |
30 | PdfConfig.toParameters(config) should contain("--no-pdf-compression")
31 | }
32 |
33 | "generate the --allow parameter" in {
34 | val config = new PdfConfig {
35 | allow := List("/some/path", "/some/other/path")
36 | }
37 | PdfConfig.toParameters(config) should equal(Seq("--allow", "/some/path", "--allow", "/some/other/path", "--encoding", "UTF-8"))
38 | }
39 |
40 | "print media type" in {
41 | val config = new PdfConfig {
42 | printMediaType := Some(true)
43 | }
44 | PdfConfig.toParameters(config) should contain("--print-media-type")
45 | }
46 |
47 | "no print media type" in {
48 | val config = new PdfConfig {
49 | printMediaType := Some(false)
50 | }
51 | PdfConfig.toParameters(config) should contain("--no-print-media-type")
52 | }
53 |
54 | "use x-server" in {
55 | val config = new PdfConfig {
56 | useXServer := true
57 | }
58 | PdfConfig.toParameters(config) should contain("--use-xserver")
59 | }
60 |
61 | "check for executable in PATH" in {
62 | PdfConfig.findExecutable match {
63 | case Some(path) => path.contains("wkhtmltopdf") should equal(true)
64 | case None => true should equal(true)
65 | }
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/PdfSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import java.io.File
4 | import scala.sys.process._
5 | import org.scalatest.Matchers
6 | import org.scalatest.WordSpec
7 |
8 | class PdfSpec extends WordSpec with Matchers {
9 |
10 | "A Pdf" should {
11 |
12 | "require the executionPath config" in {
13 | val file = new File("notexecutable")
14 | val filePath = file.getAbsolutePath
15 |
16 | assertThrows[NoExecutableException] {
17 | new Pdf(filePath, PdfConfig.default)
18 | }
19 |
20 | assertThrows[NoExecutableException] {
21 | Pdf(filePath, PdfConfig.default)
22 | }
23 |
24 | }
25 |
26 | PdfConfig.findExecutable match {
27 | case Some(_) =>
28 | "generate a PDF file from an HTML string" in {
29 |
30 | val page =
31 | """
32 | |Hello
33 | """.stripMargin
34 |
35 | val file = File.createTempFile("scala.spdf", "pdf")
36 |
37 | val pdf = Pdf(PdfConfig.default)
38 |
39 | pdf.run(page, file)
40 |
41 | Seq("file", file.getAbsolutePath).!! should include("PDF document")
42 | }
43 |
44 | case None =>
45 | "Skipping test, missing wkhtmltopdf binary" in { true should equal(true) }
46 | }
47 |
48 |
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/SourceDocumentLikeSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import scala.sys.process.Process
4 | import io.github.cloudify.scala.spdf.SourceDocumentLike._
5 | import java.io.{ File, ByteArrayInputStream }
6 | import java.net.URL
7 | import org.scalatest.WordSpec
8 | import org.scalatest.Matchers
9 | import org.scalatest.mockito.MockitoSugar
10 |
11 | class SourceDocumentLikeSpec extends WordSpec with Matchers with MockitoSugar {
12 |
13 | val catProcess = Process("cat")
14 |
15 | trait inputStreamInput {
16 | val input = new ByteArrayInputStream("Hello world".getBytes("UTF-8"))
17 | }
18 |
19 | trait stringInput {
20 | val input = "Hello world"
21 | }
22 |
23 | trait xmlInput {
24 | val input = Hello world
25 | }
26 |
27 | trait fileInput {
28 | val input = new File("test")
29 | }
30 |
31 | trait urlInput {
32 | val input = new URL("http://www.google.com")
33 | }
34 |
35 | trait httpsUrlInput {
36 | val input = new URL("https://www.google.com")
37 | }
38 |
39 | trait unsupportedUrlInput {
40 | val input = new URL("ftp://ftp.google.com")
41 | }
42 |
43 | "SourceDocumentLike" should {
44 |
45 | "set commandParameter to -" in {
46 | new SourceDocumentLike[Unit] {}.commandParameter(Unit) should equal("-")
47 | }
48 |
49 | "leave process untouched" in {
50 | new SourceDocumentLike[Unit] {}.sourceFrom(Unit)(catProcess) should equal(catProcess)
51 | }
52 |
53 | }
54 |
55 | "InputStreamSourceDocument" should {
56 |
57 | "have commandParameter to -" in new inputStreamInput {
58 | InputStreamSourceDocument.commandParameter(input) should equal("-")
59 | }
60 |
61 | "pipe stream into process STDIN" in new inputStreamInput {
62 | // need to fix https://github.com/cloudify/sPDF/issues/36
63 | pending
64 | val processWithSource = InputStreamSourceDocument.sourceFrom(input)(catProcess)
65 | processWithSource.!! should equal("Hello world\n")
66 | }
67 |
68 | }
69 |
70 | "FileSourceDocument" should {
71 |
72 | "have commandParameter to the absolute path of the file" in new fileInput {
73 | FileSourceDocument.commandParameter(input) should equal(input.getAbsolutePath)
74 | }
75 |
76 | "leave process untouched" in new fileInput {
77 | FileSourceDocument.sourceFrom(input)(catProcess) should equal(catProcess)
78 | }
79 |
80 | }
81 |
82 | "StringSourceDocument" should {
83 |
84 | "have commandParameter to -" in new stringInput {
85 | StringSourceDocument.commandParameter(input) should equal("-")
86 | }
87 |
88 | "pipe stream into process STDIN" in new stringInput {
89 | // need to fix https://github.com/cloudify/sPDF/issues/36
90 | pending
91 | val processWithSource = StringSourceDocument.sourceFrom(input)(catProcess)
92 | processWithSource.!! should equal("Hello world\n")
93 | }
94 |
95 | }
96 |
97 | "XmlSourceDocument" should {
98 |
99 | "have commandParameter to -" in new xmlInput {
100 | XmlSourceDocument.commandParameter(input) should equal("-")
101 | }
102 |
103 | "pipe stream into process STDIN" in new xmlInput {
104 | // need to fix https://github.com/cloudify/sPDF/issues/36
105 | pending
106 | val processWithSource = XmlSourceDocument.sourceFrom(input)(catProcess)
107 | processWithSource.!! should equal("Hello world\n")
108 | }
109 |
110 | }
111 |
112 | "URLSourceDocument" should {
113 |
114 | "have commandParameter set to URL" in new urlInput {
115 | URLSourceDocument.commandParameter(input) should equal("http://www.google.com")
116 | }
117 |
118 | "leave process untouched" in new urlInput {
119 | URLSourceDocument.sourceFrom(input)(catProcess) should equal(catProcess)
120 | }
121 |
122 | "throw an UnsupportedProtocolException if protocol is not supported" in new unsupportedUrlInput {
123 | assertThrows[UnsupportedProtocolException] {
124 | URLSourceDocument.commandParameter(input)
125 | }
126 | }
127 | }
128 |
129 | "httpsURLSourceDocument" should {
130 |
131 | "have commandParameter set to URL" in new httpsUrlInput {
132 | URLSourceDocument.commandParameter(input) should equal("https://www.google.com")
133 | }
134 |
135 | "leave process untouched" in new httpsUrlInput {
136 | URLSourceDocument.sourceFrom(input)(catProcess) should equal(catProcess)
137 | }
138 |
139 | "throw an UnsupportedProtocolException if protocol is not supported" in new unsupportedUrlInput {
140 | assertThrows[UnsupportedProtocolException] {
141 | URLSourceDocument.commandParameter(input)
142 | }
143 | }
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/src/test/scala/io/github/cloudify/scala/spdf/WrappedPdfSpec.scala:
--------------------------------------------------------------------------------
1 | package io.github.cloudify.scala.spdf
2 |
3 | import java.io.File
4 | import scala.sys.process._
5 | import org.scalatest.Matchers
6 | import org.scalatest.WordSpec
7 |
8 | class WrappedPdfSpec extends WordSpec with Matchers {
9 |
10 | "A Pdf" should {
11 |
12 | PdfConfig.findExecutable match {
13 | case Some(_) =>
14 | "generate a PDF file from an HTML string" in {
15 |
16 | val page =
17 | """
18 | |Hello
19 | """.stripMargin
20 |
21 | val file = File.createTempFile("scala.spdf", "pdf")
22 |
23 | val pdf = WrappedPdf(Seq("wkhtmltopdf"), PdfConfig.default)
24 |
25 | pdf.run(page, file)
26 |
27 | Seq("file", file.getAbsolutePath).!! should include("PDF document")
28 | }
29 |
30 | case None =>
31 | "Skipping test, missing wkhtmltopdf binary" in { true should equal(true) }
32 | }
33 |
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "1.4.1-SNAPSHOT"
2 |
--------------------------------------------------------------------------------