├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.adoc ├── build.gradle ├── gradle ├── publish.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── me │ └── champeau │ └── deck2pdf │ ├── Browser.java │ ├── GroovyProfile.java │ ├── JSProfile.java │ ├── Main.java │ ├── Profile.java │ ├── ProfileLoader.java │ └── writer │ ├── GenericImageSlideWriter.java │ ├── JpegSlideWriter.java │ ├── MultiFileSlideWriter.java │ ├── PdfSlideWriter.java │ ├── SlideExportException.java │ └── SlideWriter.java └── resources ├── deckjs.properties ├── dzslides.groovy ├── flowtimejs.groovy ├── googlehtml5.groovy ├── impressjs.groovy ├── remarkjs.groovy ├── revealjs.groovy ├── ruban.groovy └── spf.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | build 3 | out 4 | *~ 5 | .gradle 6 | *.iml 7 | *.ipr 8 | *.iws 9 | .idea 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | script: ./gradlew test distZip -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | */ 15 | 16 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = deck2pdf 2 | :toc: 3 | :numbered: 4 | :icons: font 5 | :linkcss!: 6 | 7 | `deck2pdf` is a simple application that will convert your http://imakewebthings.com/deck.js/[deck.js], 8 | http://lab.hakim.se/reveal-js[reveal.js], http://bartaz.github.io/impress.js[impress.js], 9 | http://flowtime-js.marcolago.com/[Flowtime.js], 10 | http://code.google.com/p/html5slides/[Google html5slides], 11 | https://github.com/briancavalier/slides[Slide Presentation Framework], 12 | https://github.com/loicfrering/ruban[ruban] 13 | or https://github.com/paulrouget/dzslides[DZSlides] slide deck into a PDF file. Deck2PDF is also capable 14 | of handling other HTML5 libraries as long as you feed it <>. 15 | 16 | This application relies on a JavaFX application, so you need at least a Java 7 installation that bundles JavaFX (which should be the case for all newer installs of Java). 17 | 18 | == Download 19 | 20 | Pre-built binaries for JDK 8 can be download from https://bintray.com/melix/generic-maven/deck2pdf/0.3.0/view#files[Bintray]. 21 | 22 | If you use Gradle (or any Maven-compatible dependency resolution engine), artifacts are available on JCenter: 23 | 24 | ```groovy 25 | repositories { 26 | jcenter() 27 | } 28 | dependencies { 29 | compile 'me.champeau.deck2pdf:deck2pdf:0.3.0' 30 | } 31 | ``` 32 | 33 | == Install from source 34 | 35 | image::https://travis-ci.org/melix/deck2pdf.png?branch=master[Build Status, link="https://travis-ci.org/melix/deck2pdf"] 36 | 37 | Alternatively, you can install deck2pdf from sources. 38 | 39 | ---- 40 | ./gradlew distZip 41 | ---- 42 | 43 | Will generate a distribution into `build/distributions/` that you can unzip wherever you want. 44 | 45 | == Changelog 46 | 47 | === 0.3.0 48 | 49 | * Change `group` from `com.github.melix` to `me.champeau.deck2pdf` 50 | * Add ability to export to PNG or JPEG 51 | * Require Java 8 52 | 53 | == Usage 54 | 55 | ---- 56 | deck2pdf --profile=deckjs 57 | ---- 58 | 59 | By default, deck2pdf assumes that you are using deck.js, but there more profiles supported: 60 | 61 | * deckjs, for http://imakewebthings.com/deck.js/[deck.js] 62 | * revealjs, for http://lab.hakim.se/reveal-js[reveal.js] 63 | * impressjs, for http://bartaz.github.io/impress.js[impress.js] 64 | * dzslides, for https://github.com/paulrouget/dzslides[DZSlides] 65 | * flowtimejs, for http://flowtime-js.marcolago.com/[Flowtime.js] 66 | * googlehtml5, for http://code.google.com/p/html5slides/[Google html5slides] 67 | * spf, for https://github.com/briancavalier/slides[Slide Presentation Framework] 68 | * remarkjs, for https://github.com/gnab/remark[remark.js] 69 | * ruban, for https://github.com/loicfrering/ruban[ruban]. Note that the ruban profile expects to find the Ruban object in the global variable `ruban`. The minimum version of Ruban is 0.3.2. 70 | 71 | For example, to convert a Deck.js slidedeck into PDF: 72 | 73 | ---- 74 | deck2pdf slides.html slides.pdf 75 | ---- 76 | 77 | Or for a reveal.js slidedeck: 78 | 79 | ---- 80 | deck2pdf --profile=revealjs slides.html slides.pdf 81 | ---- 82 | 83 | Optionally, you can specify width and height: 84 | 85 | ---- 86 | deck2pdf --width=1024 --height=768 slides.html slides.pdf 87 | ---- 88 | 89 | To export the slides as multiple images (PNG or JPG), change the file extension of the export file to .png or .jpg. 90 | 91 | ---- 92 | deck2pdf slides.html slides.png 93 | ---- 94 | 95 | You can use a number pattern in the file name (e.g., `%03d`) to have the slide numbers padded so they have a fixed length: 96 | 97 | ---- 98 | deck2pdf slides.html slides-%03d.png 99 | ---- 100 | 101 | You can also specify a quality option for JPG (default: 0.95): 102 | 103 | ---- 104 | deck2pdf --quality=75 slides.html slides.jpg 105 | ---- 106 | 107 | WARNING: The JPG export is not available when using OpenJDK. 108 | You must use the Oracle JDK instead. 109 | 110 | For a remark.js slideshow, make sure you use `window.slideshow = remark.create();` to initialize the slideshow 111 | 112 | == Profile specific options 113 | 114 | Some profiles have specific command line options that allow further configuration of the export. This section summarizes 115 | the options available for such profiles. 116 | 117 | === reveal.js 118 | 119 | [cols="1,3,1,1",options="header,footer"] 120 | |======================================================================= 121 | |Option|Behavior|Default|Example 122 | |skipFragments|If `true`, fragments will be ignored, otherwise each fragment will generate an individual slide|false|`deck2pdf --skipFragments=true revealjs.html` 123 | |======================================================================= 124 | 125 | === ruban 126 | 127 | [cols="1,3,1,1",options="header,footer"] 128 | |======================================================================= 129 | |Option|Behavior|Default|Example 130 | |skipSteps|If `true`, instead of generating one PDF page per step, generates one PDF slide per section, with the last step of each section.|false|`deck2pdf --skipSteps=true index.html` 131 | |======================================================================= 132 | 133 | 134 | [[CustomProfiles]] 135 | == Support for other HTML5 slideshows 136 | 137 | `deck2pdf` is capable of handling many HTML5 slideshows. The current distribution supports several profiles out of the 138 | box but it is easy to add yours. Depending on the complexity of the the framework, you can use either a simple 139 | properties file to describe your framework or a more complex Groovy file. Both profile types rely on the fact that 140 | you communicate with the browser with Javascript. This means that `deck2js` should be compatible with any HTML5 141 | slideshow that exposes a Javascript API. 142 | 143 | === Simple profile using properties 144 | 145 | The easiest way to define a new profile is to create a properties file. For example, here is the file that the 146 | `deck.js` export uses: 147 | 148 | ---- 149 | include::src/main/resources/deckjs.properties[] 150 | ---- 151 | 152 | The properties files consists of two entries: 153 | 154 | * `totalSlides` is a Javascript snippet which will compute the total number of slides of the deck 155 | * `nextSlide` is the Javascript code which needs to be called to jump to the next slide 156 | 157 | Properties files are very simple, so are only capable of handling decks for which the number of slides is known in 158 | advance and the command to jump from one slide to another is always the same. For more complex slide shows, you can 159 | use the http://groovy.codehaus.org[Groovy] profiles. 160 | 161 | === Advanced profiles 162 | ==== Example 163 | Advanced profiles are written using the http://groovy.codehaus.org[Groovy] scripting language. However, they are pretty 164 | simple too and they expose a simple API that should fit most situations. 165 | 166 | Here is an example of profile as it is defined for `impress.js`: 167 | 168 | [source,groovy] 169 | ---- 170 | include::src/main/resources/impressjs.groovy[] 171 | ---- 172 | 173 | The file name for a properties profile *must* end with `.properties`. 174 | 175 | ==== Groovy API 176 | 177 | The http://groovy.codehaus.org[Groovy] allows you to define more complex interactions with the slide deck. It also allows 178 | you to export slide decks for which you don't know the number of slides in advance, as long as you are capable of telling 179 | that you've reached the end of the slide deck. 180 | 181 | Calling javascript in the browser from the profile is done using the `js` function: 182 | 183 | [source,groovy] 184 | ---- 185 | js 'var slideCount = slides.length;' 186 | ---- 187 | 188 | Then the http://groovy.codehaus.org[Groovy] profile exposes some hooks that you can use: 189 | 190 | * `setup` is called once the slide deck is loaded and ready for export. It gives you a chance to perform an initial 191 | customization before exporting the slides. For example: 192 | 193 | [source,groovy] 194 | ---- 195 | setup = { 196 | // disable controls for better rendering 197 | js 'Reveal.configure({controls: false, progress: false});' 198 | } 199 | ---- 200 | 201 | * `isLastSlide` is called after a slide has been exported. If the method returns true, then export is complete. For 202 | example: 203 | 204 | [source,groovy] 205 | ---- 206 | isLastSlide = { 207 | js ''' 208 | var totalSlides = Dz.slides.length; 209 | var cSlide = Dz.idx; 210 | cSlide==totalSlides && Dz.step==Dz.slides[cSlide - 1].$$('.incremental > *').length 211 | ''' 212 | } 213 | ---- 214 | 215 | * `totalSlides` can be used to determine the total number of slides (if a slide contains a transition, each transition 216 | is counted as a slide). It is optional, as long as you define the `isLastSlide` hook. Providing it would add a counter 217 | to the log output. 218 | 219 | [source,groovy] 220 | ---- 221 | totalSlides = { 222 | js (/$$(".step", byId('impress')).length/) 223 | } 224 | ---- 225 | 226 | * `nextSlide` must be implemented so that `deck2pdf` knows how to jump from one slide to another (or one transition 227 | to another). For example: 228 | 229 | [source,groovy] 230 | ---- 231 | nextSlide = { 232 | js('api.next()') 233 | } 234 | ---- 235 | 236 | As the Groovy profile is stateful, you can for example keep track of the slide number and have a distinct operation 237 | depending on the slide number: 238 | 239 | [source,groovy] 240 | ---- 241 | int curSlide = 0 242 | nextSlide = { 243 | curSlide++ 244 | switch (curSlide) { 245 | case 1: 246 | js 'api.next()' 247 | break; 248 | case 2: 249 | js 'api.goto(2)' 250 | break; 251 | default: 252 | js "api.goto($curSlide)" 253 | } 254 | } 255 | ---- 256 | 257 | * `pause` lets you change the duration before next slide is exported, in milliseconds. Currently, you are not allowed 258 | to set a distinct pause for each slide, the change is global. For example: 259 | 260 | [source,groovy] 261 | ---- 262 | pause = 2000 263 | ---- 264 | 265 | The file name for a Groovy profile *must* end with `.groovy`. 266 | 267 | ===== Accessing command line options 268 | 269 | It is possible for a Groovy profile to access command line options. This can be useful if you want the profile to be 270 | configurable from command line. Accessing the command line options is easy, since the named parameters from the 271 | command line are directly exported in the script through the `options` variable: 272 | 273 | [source,groovy] 274 | ---- 275 | setup { 276 | if (Boolean.valueOf(options.verbose)) { 277 | // --verbose=true on command line 278 | println "Hello, I'm in verbose mode!" 279 | } 280 | String mode = options.mode?:'auto' 281 | switch (mode) { 282 | case 'auto': 283 | ... 284 | break; 285 | case 'skip': 286 | ... 287 | break; 288 | } 289 | 290 | } 291 | ---- 292 | 293 | === Using a custom profile 294 | 295 | Once you've written a new profile, you can use it with the `--profile` switch: 296 | 297 | ---- 298 | deck2pdf --profile=/path/to/profile.groovy slides.html slides.pdf 299 | ---- 300 | 301 | Of course, you can submit a pull request so that we include your profile into the distribution! 302 | 303 | == Custom fonts 304 | 305 | You'll need to read this section if you have custom fonts in your presenation. 306 | 307 | === JavaFX 3.0: CSS3 @font-face 308 | 309 | In order to use custom fonts in your presentation, as defined using the CSS3 +@font-face@, you need to use at least JavaFX 3.0 (available in JDK 8). Otherwise, the embedded browser will fall back to using the default system font. 310 | 311 | Here's an example custom font sourced from http://www.google.com/fonts[Google Fonts]: 312 | 313 | [source,html] 314 | ---- 315 | 316 | ---- 317 | 318 | This link element imports the following CSS: 319 | 320 | [source,css] 321 | ---- 322 | @font-face { 323 | font-family: 'Yanone Kaffeesatz'; 324 | font-style: normal; 325 | font-weight: 400; 326 | src: local('Yanone Kaffeesatz Regular'), local('YanoneKaffeesatz-Regular'), url(http://themes.googleusercontent.com/static/fonts/yanonekaffeesatz/v4/YDAoLskQQ5MOAgvHUQCcLbvy90DtE_Pg_qiF9bHvTzw.ttf) format('truetype'); 327 | } 328 | @font-face { 329 | font-family: 'Yanone Kaffeesatz'; 330 | font-style: normal; 331 | font-weight: 700; 332 | src: local('Yanone Kaffeesatz Bold'), local('YanoneKaffeesatz-Bold'), url(http://themes.googleusercontent.com/static/fonts/yanonekaffeesatz/v4/We_iSDqttE3etzfdfhuPRUgbSk09ekaEjkXjfj0Ujx8.ttf) format('truetype'); 333 | } 334 | ---- 335 | 336 | You can now use these two combinations of +font-family+, +font-style+ and +font-weight+ in your presentation. Unlike a normal browser (e.g., Chrome, Firefox, etc), you cannot use a font combination that is not represented here (or on your system). If you attempt to use a combination not represented here (such as a combination that include `font-style: italic;`), the embedded browser will fall back to using the default font. 337 | 338 | Aside from that exception, custom fonts should just work when using JavaFX 3.0. 339 | 340 | === JavaFX 2.2: Explicit font loading 341 | 342 | If you you're using JavaFX 2.2 (available in JDK 7), you can still use custom fonts, but you must load them explicitly before the embedded browser loads the presentation. 343 | 344 | First, download the TTF file for each font you want to use and copy it to a directory of your choice. The files should be named using the hyphenated form shown in the CSS above. For example: 345 | 346 | * YanoneKaffeesatz-Regular.ttf 347 | * YanoneKaffeesatz-Bold.ttf 348 | 349 | Now you can use the `fontsdir` command line option to tell `deck2pdf` where to load the fonts from: 350 | 351 | ---- 352 | deck2pdf --fontsdir=/path/to/ttf/files slides.html slides.pdf 353 | ---- 354 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.jfrog.bintray" version "1.2" 3 | } 4 | 5 | apply plugin: 'java' 6 | apply plugin: 'idea' 7 | apply plugin: 'application' 8 | apply plugin: 'maven-publish' 9 | 10 | group = 'me.champeau.deck2pdf' 11 | version = '0.3.1-SNAPSHOT' 12 | 13 | repositories { 14 | jcenter() 15 | } 16 | 17 | def javafxHome = System.getenv('JAVA_HOME') 18 | 19 | dependencies { 20 | compile files("$javafxHome/jre/lib/jfxrt.jar") 21 | compile 'com.itextpdf:itextpdf:5.5.1' 22 | compile 'org.codehaus.groovy:groovy:2.4.4' 23 | } 24 | 25 | sourceCompatibility = 1.8 26 | targetCompatibility = 1.8 27 | 28 | mainClassName = 'me.champeau.deck2pdf.Main' 29 | 30 | task sourcesJar(type: Jar, dependsOn: classes) { 31 | classifier = 'sources' 32 | from sourceSets.main.allSource 33 | } 34 | 35 | task javadocJar(type: Jar, dependsOn: javadoc) { 36 | classifier = 'javadoc' 37 | from javadoc.destinationDir 38 | } 39 | 40 | apply from: 'gradle/publish.gradle' 41 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | publishing { 2 | publications { 3 | mavenJava(MavenPublication) { 4 | from components.java 5 | artifact(sourcesJar) { 6 | classifier "sources" 7 | } 8 | artifact(javadocJar) { 9 | classifier "javadoc" 10 | } 11 | } 12 | } 13 | } 14 | 15 | bintray { 16 | user = 'melix' 17 | key = System.getenv('BINTRAY_KEY')?:'secret but wrong' 18 | publications = ['mavenJava'] 19 | filesSpec { 20 | from distZip.archivePath 21 | into "${distZip.archiveName}" 22 | } 23 | pkg { 24 | repo = 'generic-maven' 25 | name = 'deck2pdf' 26 | licenses = ['Apache-2.0'] 27 | vcsUrl = 'https://github.com/melix/deck2pdf' 28 | version { 29 | name = "${project.version}" 30 | desc = "deck2pdf ${project.version}" 31 | released = new Date() 32 | vcsTag = "RELEASE_${project.version.replace('.','_')}" 33 | } 34 | } 35 | } 36 | 37 | bintrayUpload { 38 | onlyIf { !version.endsWith('-SNAPSHOT') } 39 | dependsOn distZip, javadocJar, sourcesJar 40 | } 41 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melix/deck2pdf/e54c105fccda064eb559cfac5dc4559cc90ad35c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 03 20:17:02 CET 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-2.5-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'deck2pdf' -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/Browser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf; 17 | 18 | import javafx.animation.PauseTransition; 19 | import javafx.application.Platform; 20 | import javafx.concurrent.Worker; 21 | import javafx.embed.swing.SwingFXUtils; 22 | import javafx.geometry.HPos; 23 | import javafx.geometry.VPos; 24 | import javafx.scene.image.WritableImage; 25 | import javafx.scene.layout.Region; 26 | import javafx.scene.text.FontSmoothingType; 27 | import javafx.scene.web.WebEngine; 28 | import javafx.scene.web.WebView; 29 | import javafx.util.Duration; 30 | import me.champeau.deck2pdf.writer.SlideExportException; 31 | import me.champeau.deck2pdf.writer.SlideWriter; 32 | 33 | import java.awt.image.BufferedImage; 34 | import java.util.concurrent.atomic.AtomicInteger; 35 | 36 | /* 37 | * Licensed under the Apache License, Version 2.0 (the "License"); 38 | * you may not use this file except in compliance with the License. 39 | * You may obtain a copy of the License at 40 | * 41 | * http://www.apache.org/licenses/LICENSE-2.0 42 | * 43 | * Unless required by applicable law or agreed to in writing, software 44 | * distributed under the License is distributed on an "AS IS" BASIS, 45 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | * See the License for the specific language governing permissions and 47 | * limitations under the License. 48 | * 49 | */ 50 | 51 | /** 52 | * This class is responsible for converting a slide deck into a PDF file. It performs the following: 53 | *

54 | *

  • opens a web view with the slide deck
  • captures each slide after one second
  • puts the capture 55 | * into a PDF file
  • exits the VM
56 | * 57 | * @author Cédric Champeau 58 | */ 59 | class Browser extends Region { 60 | 61 | private final WebView browser = new WebView(); 62 | private final WebEngine webEngine = browser.getEngine(); 63 | private final String exportFile; 64 | private final int width; 65 | private final int height; 66 | private final float quality; 67 | 68 | public Browser(String rootURL, String exportFile, int width, int height, float quality) { 69 | //apply the styles 70 | getStyleClass().add("browser"); 71 | // load the web page 72 | webEngine.load(rootURL); 73 | //add the web view to the scene 74 | getChildren().add(browser); 75 | browser.setFontSmoothingType(FontSmoothingType.GRAY); 76 | this.width = width; 77 | this.height = height; 78 | this.quality = quality; 79 | this.exportFile = exportFile; 80 | } 81 | 82 | public WebEngine getEngine() { 83 | return webEngine; 84 | } 85 | 86 | private void handleError(Exception e) { 87 | throw new RuntimeException("Unable to export slide deck", e); 88 | } 89 | 90 | public void doExport(final Profile profile, final int width, final int height) { 91 | final PauseTransition pt = new PauseTransition(); 92 | pt.setDuration(Duration.millis(profile.getPause())); 93 | final AtomicInteger slideCounter = new AtomicInteger(); 94 | final AtomicInteger numSlidesCache = new AtomicInteger(-1); 95 | final SlideWriter writer; 96 | try { 97 | writer = SlideWriter.of(profile, exportFile, width, height, quality); 98 | } catch (SlideExportException e) { 99 | handleError(e); 100 | return; 101 | } 102 | pt.setOnFinished(actionEvent -> { 103 | WritableImage snapshot = browser.snapshot(null, null); 104 | // Remove alpha-channel from buffered image to reduce size and enable jpg export 105 | BufferedImage image = new BufferedImage((int) snapshot.getWidth(), (int) snapshot.getHeight(), BufferedImage.OPAQUE); 106 | SwingFXUtils.fromFXImage(snapshot, null).copyData(image.getRaster()); 107 | 108 | int numSlides = numSlidesCache.get(); 109 | if (numSlides == -1) { 110 | numSlides = profile.getSlideCount(); 111 | numSlidesCache.set(numSlides); 112 | } 113 | int current = slideCounter.incrementAndGet(); 114 | try { 115 | writer.writeSlide(image, numSlides, current); 116 | System.out.printf("Exported slide %d%s%n", current, numSlides > 0 ? "/" + numSlides : ""); 117 | if (!profile.isLastSlide(current)) { 118 | profile.nextSlide(); 119 | pt.setDuration(Duration.millis(profile.getPause())); 120 | pt.play(); 121 | } else { 122 | profile.finish(); 123 | writer.close(); 124 | System.out.println("Export complete!"); 125 | Platform.exit(); 126 | } 127 | } catch (SlideExportException e) { 128 | handleError(e); 129 | } 130 | }); 131 | webEngine.getLoadWorker().stateProperty().addListener( 132 | (ov, oldState, newState) -> { 133 | if (newState == Worker.State.SUCCEEDED) { 134 | profile.setup(); 135 | profile.ready(pt::play); 136 | } 137 | }); 138 | } 139 | 140 | @Override 141 | protected void layoutChildren() { 142 | double w = getWidth(); 143 | double h = getHeight(); 144 | layoutInArea(browser, 0, 0, w, h, 0, HPos.CENTER, VPos.CENTER); 145 | } 146 | 147 | @Override 148 | protected double computePrefWidth(double height) { 149 | return width; 150 | } 151 | 152 | @Override 153 | protected double computePrefHeight(double width) { 154 | return height; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/GroovyProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf; 17 | 18 | import com.itextpdf.text.Document; 19 | import groovy.lang.Binding; 20 | import groovy.lang.Closure; 21 | import groovy.lang.GroovyShell; 22 | import groovy.lang.MissingPropertyException; 23 | import javafx.scene.web.WebEngine; 24 | import netscape.javascript.JSObject; 25 | import org.codehaus.groovy.runtime.DefaultGroovyMethods; 26 | import org.codehaus.groovy.runtime.MethodClosure; 27 | 28 | import java.io.Reader; 29 | import java.util.Map; 30 | 31 | /** 32 | * The Groovy provide can be used for complex interactions with the web engine. The Groovy script consists 33 | * minimally of 2 variables: totalSlides and nextSlide. The script exposes two additional variables: 34 | *
    35 | *
  • engine, of type {@link WebEngine}
  • 36 | *
  • js, a function that will call javascript code using the engine (shortcut for engine.executeScript()
  • 37 | *
38 | * For example, a deck.js profile can be written like this: 39 | * 40 | * totalSlides = { js("\$.deck('getSlides').length") } 41 | * nextSlide = { js("\$.deck('next')") } 42 | * 43 | */ 44 | public class GroovyProfile extends Profile { 45 | private final Binding binding; 46 | protected GroovyProfile(final WebEngine engine, final Map options, final Reader script) { 47 | super(engine, options); 48 | binding = new NullBinding(); 49 | binding.setVariable("engine", engine); 50 | binding.setVariable("js", new MethodClosure(this, "executeJS")); 51 | binding.setVariable("options", options); 52 | new GroovyShell(binding).evaluate(script); 53 | } 54 | 55 | public Object getVariable(final String name) { 56 | return binding.getVariable(name); 57 | } 58 | 59 | public void setVariable(final String name, final Object value) { 60 | binding.setVariable(name, value); 61 | } 62 | 63 | public Object executeJS(String code) { 64 | try { 65 | return engine.executeScript(code); 66 | } catch (netscape.javascript.JSException e) { 67 | System.err.println("Error while executing the following code:"); 68 | System.err.println(code); 69 | throw e; 70 | } 71 | } 72 | 73 | @Override 74 | public void setDocument(final Document document) { 75 | super.setDocument(document); 76 | binding.setVariable("document", document); 77 | } 78 | 79 | @Override 80 | @SuppressWarnings("unchecked") 81 | public boolean isLastSlide(final int slideIdx) { 82 | Closure fun = (Closure) binding.getVariable("isLastSlide"); 83 | if (fun==null) { 84 | return getSlideCount()==slideIdx; 85 | } 86 | return fun.call(); 87 | } 88 | 89 | @Override 90 | public void nextSlide() { 91 | ((Closure)binding.getVariable("nextSlide")).call(); 92 | } 93 | 94 | @Override 95 | public int getPause() { 96 | Object pause = binding.getVariable("pause"); 97 | if (pause==null) { 98 | return super.getPause(); 99 | } else if (pause instanceof Closure) { 100 | return (Integer)((Closure)pause).call(); 101 | } else if (pause instanceof Integer) { 102 | return (Integer) pause; 103 | } else { 104 | throw new RuntimeException("'pause' function returned an unexpected value"); 105 | } 106 | } 107 | 108 | @Override 109 | @SuppressWarnings("unchecked") 110 | public int getSlideCount() { 111 | Closure fun = (Closure) binding.getVariable("totalSlides"); 112 | if (fun==null){ 113 | return super.getSlideCount(); 114 | } 115 | return fun.call(); 116 | } 117 | 118 | @Override 119 | public void setup() { 120 | prepareEngine(); 121 | Closure fun = (Closure) binding.getVariable("setup"); 122 | if (fun!=null){ 123 | fun.call(); 124 | } 125 | } 126 | 127 | private void prepareEngine() { 128 | JSObject window = (JSObject) engine.executeScript("window"); 129 | window.setMember("exportProfile", this); 130 | 131 | // uncomment this for debugging 132 | // engine.executeScript("if (!document.getElementById('FirebugLite')){E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');E['setAttribute']('id', 'FirebugLite');E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');E['setAttribute']('FirebugLite', '4');(document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);E = new Image;E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');}"); 133 | } 134 | 135 | @Override 136 | public void finish() { 137 | Closure fun = (Closure) binding.getVariable("finish"); 138 | if (fun!=null){ 139 | fun.call(); 140 | } 141 | } 142 | 143 | @Override 144 | public void ready(Runnable r) { 145 | Closure fun = (Closure) binding.getVariable("ready"); 146 | if (fun!=null){ 147 | fun.call(r); 148 | } else { 149 | super.ready(r); 150 | } 151 | } 152 | 153 | private static class NullBinding extends Binding { 154 | @Override 155 | public Object getVariable(final String name) { 156 | Object variable = null; 157 | try { 158 | variable = super.getVariable(name); 159 | } catch (MissingPropertyException e) { 160 | return null; 161 | } 162 | return variable; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/JSProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf; 17 | 18 | import javafx.scene.web.WebEngine; 19 | 20 | import java.util.Map; 21 | 22 | /** 23 | * Base class for profiles that leverage Javascript to get the total number of slides 24 | * and jump to the next slide. 25 | * 26 | * This profile also allows overriding the default pause delay. 27 | * 28 | * @author Cédric Champeau 29 | */ 30 | public class JSProfile extends Profile { 31 | 32 | private String slideCountJS; 33 | private String nextSlideJS; 34 | private int pause = DEFAULT_PAUSE_MILLIS; 35 | 36 | public JSProfile( 37 | WebEngine engine, 38 | Map options, 39 | String slideCountJS, 40 | String nextSlideJS) { 41 | super(engine, options); 42 | this.slideCountJS = slideCountJS; 43 | this.nextSlideJS = nextSlideJS; 44 | } 45 | 46 | /** 47 | * Sets the javascript code that will be called to display the next slide. 48 | * @param nextSlideJS javascript code to be displayed for the next slide 49 | */ 50 | public void setNextSlideJS(final String nextSlideJS) { 51 | this.nextSlideJS = nextSlideJS; 52 | } 53 | 54 | /** 55 | * Sets the javascript code that will be used to retrieve the 56 | * total number of slides of this deck. 57 | * @param slideCountJS a javascript code that returns a integer 58 | */ 59 | public void setSlideCountJS(final String slideCountJS) { 60 | this.slideCountJS = slideCountJS; 61 | } 62 | 63 | public void setPause(final int pause) { 64 | this.pause = pause; 65 | } 66 | 67 | public int getPause() { 68 | return pause; 69 | } 70 | 71 | @Override 72 | public int getSlideCount() { 73 | return (Integer) engine.executeScript(slideCountJS); 74 | } 75 | 76 | @Override 77 | public boolean isLastSlide(final int slideIdx) { 78 | return slideIdx==getSlideCount(); 79 | } 80 | 81 | @Override 82 | public void nextSlide() { 83 | engine.executeScript(nextSlideJS); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/Main.java: -------------------------------------------------------------------------------- 1 | package me.champeau.deck2pdf; 2 | 3 | import javafx.application.Application; 4 | import javafx.scene.Scene; 5 | import javafx.scene.text.Font; 6 | import javafx.stage.Stage; 7 | 8 | import javafx.scene.paint.Color; 9 | 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileNotFoundException; 13 | import java.net.MalformedURLException; 14 | import java.net.URL; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /* 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | */ 32 | 33 | /** 34 | * Entry point for the application. 35 | * 36 | * @author Cédric Champeau 37 | */ 38 | public class Main extends Application { 39 | 40 | private final static String FONTS_DIRECTORY_OPTION = "fontsdir"; 41 | 42 | public static final int WIDTH = 1500; 43 | public static final int HEIGHT = 1000; 44 | // JPG or PNG compression level / quality 45 | public static final float COMPRESSION_QUALITY = 95.0f; 46 | 47 | private Scene scene; 48 | @Override public void start(Stage stage) { 49 | Map opts = getParameters().getNamed(); 50 | int width = parseArgumentAsInt(opts, "width", WIDTH); 51 | int height = parseArgumentAsInt(opts, "height", HEIGHT); 52 | float quality = parseArgumentAsFloat(opts, "quality", COMPRESSION_QUALITY); 53 | // The format option is used when no export file is specified. 54 | // Otherwise, the format is derived from the name of the export file. 55 | String format = opts.get("format"); 56 | if (format == null) { 57 | format = "pdf"; 58 | } 59 | 60 | stage.setTitle("PDF Export Web View"); 61 | List unnamed = getParameters().getUnnamed(); 62 | if (unnamed.isEmpty()) { 63 | System.err.println("You must provide at least the name of the file to convert"); 64 | System.exit(-1); 65 | } 66 | String path = null; 67 | String firstArg = unnamed.get(0); 68 | try { 69 | URL url = new URL(firstArg); 70 | path = url.toString(); 71 | } catch (MalformedURLException e) { 72 | String url = null; 73 | try { 74 | path = new File(firstArg).toURI().toURL().toString(); 75 | } catch (MalformedURLException e2) { 76 | System.err.println("Unable to load source file:" + e2.getMessage()); 77 | System.exit(-1); 78 | } 79 | } 80 | 81 | String exportFile = "output." + format; 82 | if (unnamed.size()>1) { 83 | exportFile = unnamed.get(1); 84 | } 85 | 86 | loadCustomFonts(opts); 87 | 88 | Browser browser = new Browser(path, exportFile, width, height, quality); 89 | scene = new Scene(browser, width, height, Color.web("#666970")); 90 | stage.setScene(scene); 91 | stage.show(); 92 | Profile profile = ProfileLoader.loadProfile(opts.get("profile"), browser.getEngine(), opts); 93 | browser.doExport(profile, width, height); 94 | } 95 | 96 | 97 | /** 98 | * JavaFX 2.2 does not support CSS3 @font-face so this is a workaround that allows loading 99 | * custom fonts from a directory specified on command line. The key for the command line 100 | * option is 'fontsdir' 101 | * @param opts the command line options 102 | */ 103 | private static void loadCustomFonts(final Map opts) { 104 | String fontsDir = opts.get(FONTS_DIRECTORY_OPTION); 105 | if (fontsDir!=null) { 106 | File dir = new File(fontsDir); 107 | for (File font : dir.listFiles()) { 108 | if (font.isFile()) { 109 | try { 110 | Font.loadFont(new FileInputStream(font), -1); 111 | System.out.println("Loaded font " + font); 112 | } catch (FileNotFoundException e) { 113 | System.err.println("Unable to load font from file "+font); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | private int parseArgumentAsInt(final Map opts, String key, int defaultValue) { 121 | return opts.get(key)!=null?Integer.valueOf(opts.get(key)):defaultValue; 122 | } 123 | 124 | private float parseArgumentAsFloat(final Map opts, String key, float defaultValue) { 125 | return opts.get(key) != null ? Float.valueOf(opts.get(key)) :defaultValue; 126 | } 127 | 128 | public static void main(String[] args){ 129 | launch(args); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/Profile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf; 17 | 18 | import com.itextpdf.text.Document; 19 | import javafx.scene.web.WebEngine; 20 | 21 | import java.util.Map; 22 | 23 | /** 24 | * A profile describes how the browser will interact with the slide deck 25 | * in order to switch from one slide to another and eventually finish the 26 | * export. 27 | * 28 | * Profiles are used to implement exporting for various HTML5 slide decks. 29 | * 30 | * @author Cédric Champeau 31 | */ 32 | public abstract class Profile { 33 | 34 | protected static final int DEFAULT_PAUSE_MILLIS = 1000; 35 | protected final WebEngine engine; 36 | protected final Map options; 37 | 38 | protected Document document; 39 | 40 | protected Profile(final WebEngine engine, final Map options) { 41 | this.engine = engine; 42 | this.options = options; 43 | } 44 | 45 | public void setDocument(final Document document) { 46 | this.document = document; 47 | } 48 | 49 | /** 50 | * Implement this method if the slide deck provides a way to determine the total 51 | * number of slides. 52 | * @return -1 if the number of slides is not known, or a positive number if it is. 53 | */ 54 | public int getSlideCount() { 55 | return -1; 56 | } 57 | 58 | /** 59 | * Given a slide number, tells if the slide is the last one. 60 | * @param slideIdx the slide number, starting from 1 (first slide has index 1, not 0) 61 | * @return true if this slide is the last one 62 | */ 63 | public abstract boolean isLastSlide(int slideIdx); 64 | 65 | /** 66 | * Implement this method to jump to the next slide. 67 | */ 68 | public abstract void nextSlide(); 69 | 70 | /** 71 | * Returns the time to wait before going to the next slide. 72 | * @return Time to wait, in milliseconds. 73 | */ 74 | public int getPause() { 75 | return DEFAULT_PAUSE_MILLIS; 76 | } 77 | 78 | /** 79 | * Called before the slides capture starts. 80 | */ 81 | public void setup() {} 82 | 83 | /** 84 | * Called before the export is closed, gives the profile chances to cleanup 85 | * extra resources or perform additional tasks before the pdf file gets closed. 86 | */ 87 | public void finish() {} 88 | 89 | 90 | /** 91 | * This method is called once setup is complete and deck2pdf is ready to export. 92 | * The action is a {@link Runnable} that will start the export of slides. 93 | * Advanced profiles may capture the action instead if directly running it, for 94 | * example if the slide deck defers loading of slides with javascript, in which 95 | * case export should be triggered from javascript instead of from deck2pdf itself. 96 | * @param action the action that will start the export 97 | */ 98 | public void ready(Runnable action) { 99 | action.run(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/ProfileLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf; 17 | 18 | import javafx.scene.web.WebEngine; 19 | 20 | import java.io.BufferedInputStream; 21 | import java.io.FileInputStream; 22 | import java.io.FileNotFoundException; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.InputStreamReader; 26 | import java.util.Map; 27 | import java.util.Properties; 28 | 29 | public class ProfileLoader { 30 | private static final String DEFAULT_PROFILE = "deckjs"; 31 | 32 | public static Profile loadProfile(final String profile, final WebEngine engine, final Map options) { 33 | if (profile==null) { 34 | return loadProfile(DEFAULT_PROFILE, engine, options); 35 | } 36 | Profile result = null; 37 | ClassLoader loader = ProfileLoader.class.getClassLoader(); 38 | InputStream resource = loader.getResourceAsStream(profile + ".properties"); 39 | if (resource!=null) { 40 | result = loadProfileFromPropertiesFile(resource, engine, options); 41 | } else { 42 | resource = loader.getResourceAsStream(profile + ".groovy"); 43 | if (resource!=null) { 44 | result = loadProfileFromGroovy(resource, engine, options); 45 | } 46 | } 47 | try { 48 | if (profile.endsWith(".properties")) { 49 | resource = new BufferedInputStream(new FileInputStream(profile)); 50 | result = loadProfileFromPropertiesFile(resource, engine, options); 51 | } else if (profile.endsWith(".groovy")) { 52 | resource = new BufferedInputStream(new FileInputStream(profile)); 53 | result = loadProfileFromGroovy(resource, engine, options); 54 | } 55 | } catch (FileNotFoundException e) { 56 | result = null; 57 | } 58 | if (result==null) { 59 | throw new RuntimeException("Cannot find a profile named '"+profile); 60 | } 61 | return result; 62 | } 63 | 64 | private static GroovyProfile loadProfileFromGroovy(final InputStream resource, final WebEngine engine, final Map options) { 65 | return new GroovyProfile( 66 | engine, 67 | options, 68 | new InputStreamReader(resource) 69 | ); 70 | } 71 | 72 | private static Profile loadProfileFromPropertiesFile(final InputStream resource, final WebEngine engine, final Map options) { 73 | Properties props = new Properties(); 74 | try { 75 | props.load(resource); 76 | } catch (IOException e) { 77 | throw new RuntimeException(e); // ugly but blame Java checked exceptions! 78 | } 79 | String totalSlides = findProperty(props, "totalSlides"); 80 | String nextSlide = findProperty(props,"nextSlide"); 81 | String pause = props.getProperty("pause"); 82 | 83 | JSProfile result = new JSProfile(engine, options, totalSlides, nextSlide); 84 | if (pause!=null) { 85 | result.setPause(Integer.valueOf(pause)); 86 | } 87 | return result; 88 | } 89 | 90 | private static String findProperty(final Properties props, final String key) { 91 | String value = props.getProperty(key); 92 | if (value==null) { 93 | throw new RuntimeException("Profile doesn't define the "+key+" property"); 94 | } 95 | return value; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/GenericImageSlideWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | import javax.imageio.ImageIO; 19 | import java.awt.image.BufferedImage; 20 | import java.io.IOException; 21 | 22 | public class GenericImageSlideWriter extends MultiFileSlideWriter { 23 | private final String format; 24 | 25 | public GenericImageSlideWriter(String exportFile, String format) { 26 | super(exportFile); 27 | this.format = format; 28 | } 29 | 30 | @Override 31 | public void writeSlide(final BufferedImage export, final int numSlides, final int current) throws SlideExportException { 32 | try { 33 | ImageIO.write(export, format, getOutputFile(numSlides, current)); 34 | } catch (IOException e) { 35 | throw new SlideExportException(e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/JpegSlideWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | import javax.imageio.IIOImage; 19 | import javax.imageio.ImageIO; 20 | import javax.imageio.ImageWriteParam; 21 | import javax.imageio.ImageWriter; 22 | import javax.imageio.stream.ImageOutputStream; 23 | import java.awt.image.BufferedImage; 24 | import java.io.IOException; 25 | 26 | public class JpegSlideWriter extends MultiFileSlideWriter { 27 | private final ImageWriter imageWriter; 28 | private final ImageWriteParam imageWriteParams; 29 | 30 | public JpegSlideWriter(String exportFile, float quality) { 31 | super(exportFile); 32 | imageWriter = ImageIO.getImageWritersByFormatName("jpeg").next(); 33 | imageWriteParams = imageWriter.getDefaultWriteParam(); 34 | if (quality > 0 && quality <= 100) { 35 | imageWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 36 | imageWriteParams.setCompressionQuality(quality / 100.0f); 37 | } 38 | } 39 | 40 | @Override 41 | public void writeSlide(final BufferedImage export, final int numSlides, final int current) throws SlideExportException { 42 | try { 43 | ImageOutputStream ios = ImageIO.createImageOutputStream(getOutputFile(numSlides, current)); 44 | imageWriter.setOutput(ios); 45 | try { 46 | imageWriter.write(null, new IIOImage(export, null, null), imageWriteParams); 47 | } finally { 48 | ios.flush(); 49 | ios.close(); 50 | } 51 | } catch (IOException e) { 52 | throw new SlideExportException(e); 53 | } 54 | } 55 | 56 | @Override 57 | public void close() { 58 | imageWriter.dispose(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/MultiFileSlideWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | import java.io.File; 19 | import java.util.regex.Pattern; 20 | 21 | public abstract class MultiFileSlideWriter extends SlideWriter { 22 | // ex. %04d 23 | private static final Pattern NUMBER_FORMAT_PATTERN = Pattern.compile("%0?[1-9]\\d*d"); 24 | private static final String IMAGE_EXT_REGEX = "\\.(png|jp(?:e)?g)$"; 25 | 26 | protected final String exportFile; 27 | 28 | protected MultiFileSlideWriter(final String exportFile) { 29 | this.exportFile = exportFile; 30 | } 31 | 32 | protected File getOutputFile(final int numSlides, final int current) { 33 | File slideFile; 34 | if (NUMBER_FORMAT_PATTERN.matcher(exportFile).find()) { 35 | slideFile = new File(String.format(exportFile, current)); 36 | } else { 37 | // QUESTION should we enforce a minimum length of the digit for consistency? 38 | //int totalCols = Math.max(3, ((int) Math.log10(numSlides)) + 1); 39 | int totalCols = ((int) Math.log10(numSlides)) + 1; 40 | String formattedSlideNum = String.format("%0" + totalCols + "d", current); 41 | slideFile = new File(exportFile.replaceFirst(IMAGE_EXT_REGEX, "-" + formattedSlideNum + ".$1")); 42 | } 43 | if (slideFile.exists()) { 44 | slideFile.delete(); 45 | } 46 | return slideFile; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/PdfSlideWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | import com.itextpdf.text.Document; 19 | import com.itextpdf.text.DocumentException; 20 | 21 | import java.awt.image.BufferedImage; 22 | import java.io.IOException; 23 | 24 | public class PdfSlideWriter extends SlideWriter { 25 | private final Document document; 26 | 27 | public PdfSlideWriter(final Document document) { 28 | this.document = document; 29 | } 30 | 31 | @Override 32 | public void writeSlide(final BufferedImage image, final int numSlides, final int current) throws SlideExportException { 33 | try { 34 | com.itextpdf.text.Image image2 = 35 | com.itextpdf.text.Image.getInstance(image, null); 36 | double scaler = ((document.getPageSize().getWidth() - document.leftMargin() 37 | - document.rightMargin()) / image.getWidth()) * 100; 38 | image2.scalePercent((float) scaler); 39 | document.add(image2); 40 | document.newPage(); 41 | } catch (IOException | DocumentException e) { 42 | throw new SlideExportException(e); 43 | } 44 | } 45 | 46 | @Override 47 | public void close() { 48 | document.close(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/SlideExportException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | public class SlideExportException extends Exception { 19 | public SlideExportException(final Throwable cause) { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/champeau/deck2pdf/writer/SlideWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2003-2012 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package me.champeau.deck2pdf.writer; 17 | 18 | import com.itextpdf.text.Document; 19 | import com.itextpdf.text.DocumentException; 20 | import com.itextpdf.text.Rectangle; 21 | import com.itextpdf.text.pdf.PdfWriter; 22 | import me.champeau.deck2pdf.Profile; 23 | 24 | import java.awt.image.BufferedImage; 25 | import java.io.FileNotFoundException; 26 | import java.io.FileOutputStream; 27 | 28 | public abstract class SlideWriter { 29 | private enum ExportFormat { 30 | pdf, 31 | png, 32 | jpeg, 33 | unknown; 34 | 35 | public boolean isPdf() { 36 | return this == pdf; 37 | } 38 | 39 | public boolean isJpeg() { 40 | return this == jpeg; 41 | } 42 | 43 | public boolean isPng() { 44 | return this == png; 45 | } 46 | 47 | static ExportFormat of(String fileName) { 48 | String fn = fileName.toLowerCase(); 49 | if (fn.endsWith(".pdf")) { 50 | return pdf; 51 | } 52 | if (fn.endsWith(".png")) { 53 | return png; 54 | } 55 | if (fn.endsWith(".jpeg") || fn.endsWith(".jpg")) { 56 | return jpeg; 57 | } 58 | return unknown; 59 | } 60 | } 61 | 62 | public abstract void writeSlide(BufferedImage export, final int numSlides, final int current) throws SlideExportException; 63 | 64 | public void close() { 65 | } 66 | 67 | public static SlideWriter of(Profile profile, String exportFile, int width, int height, float quality) throws SlideExportException { 68 | ExportFormat format = ExportFormat.of(exportFile); 69 | if (format.isPdf()) { 70 | Document document = new Document(new Rectangle(width, height), 0, 0, 0, 0); 71 | try { 72 | PdfWriter.getInstance(document, new FileOutputStream(exportFile)); 73 | } catch (DocumentException | FileNotFoundException e) { 74 | throw new SlideExportException(e); 75 | } 76 | document.open(); 77 | profile.setDocument(document); 78 | return new PdfSlideWriter(document); 79 | } else if (format.isJpeg()) { 80 | return new JpegSlideWriter(exportFile, quality); 81 | 82 | } else { 83 | return new GenericImageSlideWriter(exportFile, format.toString()); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/resources/deckjs.properties: -------------------------------------------------------------------------------- 1 | totalSlides=$.deck('getSlides').length 2 | nextSlide=$.deck('next') 3 | -------------------------------------------------------------------------------- /src/main/resources/dzslides.groovy: -------------------------------------------------------------------------------- 1 | isLastSlide = { 2 | js ''' 3 | var totalSlides = Dz.slides.length; 4 | var cSlide = Dz.idx; 5 | cSlide==totalSlides && Dz.step==Dz.slides[cSlide - 1].$$('.incremental > *').length 6 | ''' 7 | } 8 | 9 | nextSlide = { 10 | js 'Dz.forward();' 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/flowtimejs.groovy: -------------------------------------------------------------------------------- 1 | setup = { 2 | js '''Flowtime.showProgress(false) 3 | Flowtime.gotoEnd(); 4 | var endPage=Flowtime.getPageIndex(); 5 | var endSection=Flowtime.getSectionIndex(); 6 | Flowtime.gotoHome(); 7 | var complete = false 8 | ''' 9 | } 10 | 11 | nextSlide = { 12 | js 'Flowtime.next();' 13 | } 14 | 15 | isLastSlide = { 16 | js 'endPage == Flowtime.getPageIndex() && endSection==Flowtime.getSectionIndex()' 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/googlehtml5.groovy: -------------------------------------------------------------------------------- 1 | // profile for html5slides (http://html5slides.googlecode.com/) 2 | 3 | isLastSlide= { 4 | js 'curSlide == slideEls.length-1' 5 | } 6 | 7 | nextSlide= { 8 | js 'nextSlide();' 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/impressjs.groovy: -------------------------------------------------------------------------------- 1 | setup = { 2 | js 'var api = impress();' 3 | js '''var $$ = function ( selector, context ) { 4 | context = context || document; 5 | return context.querySelectorAll(selector); 6 | };''' 7 | js '''var byId = function ( id ) { 8 | return document.getElementById(id); 9 | };''' 10 | } 11 | 12 | nextSlide = { 13 | js('api.next()') 14 | } 15 | 16 | totalSlides = { 17 | js (/$$(".step", byId('impress')).length/) 18 | } 19 | 20 | // longer pause because of transitions 21 | pause = 2000 -------------------------------------------------------------------------------- /src/main/resources/remarkjs.groovy: -------------------------------------------------------------------------------- 1 | isLastSlide = { 2 | js 'window.slideshow.getCurrentSlideIndex() >= window.slideshow.getSlideCount() - 1;' 3 | } 4 | 5 | nextSlide = { 6 | js 'window.slideshow.gotoNextSlide();' 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/revealjs.groovy: -------------------------------------------------------------------------------- 1 | isLastSlide = { 2 | js 'Reveal.isLastSlide();' 3 | } 4 | 5 | nextSlide = { 6 | js 'Reveal.next();' 7 | } 8 | 9 | setup = { 10 | // disable controls for better rendering 11 | js 'Reveal.configure({controls: false, progress: false});' 12 | 13 | if (Boolean.valueOf(options.skipFragments)) { 14 | js 'Reveal.configure({fragments: false});' 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/resources/ruban.groovy: -------------------------------------------------------------------------------- 1 | setup = { 2 | js 'ruban.disableTransitions();' 3 | } 4 | 5 | isLastSlide = { 6 | if (Boolean.valueOf(options.skipSteps)) { 7 | js 'ruban.isLastSlide()' 8 | } 9 | else { 10 | js 'ruban.isLastSlide() && ruban.isLastStep()' 11 | } 12 | } 13 | 14 | nextSlide = { 15 | if (Boolean.valueOf(options.skipSteps)) { 16 | js ''' 17 | ruban.nextSlide(); 18 | if (ruban.hasSteps()) { 19 | while (!ruban.isLastStep()) { 20 | ruban.next(); 21 | } 22 | } 23 | ''' 24 | } 25 | else { 26 | js 'ruban.next();' 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/resources/spf.groovy: -------------------------------------------------------------------------------- 1 | 2 | ready = { start -> 3 | action = start // store start action into the binding for use from javascript 4 | } 5 | 6 | setup = { 7 | js ''' 8 | var slideView; 9 | var slideCount = 1; 10 | curl(['wire!slides-spec']).then(function (wire) { 11 | slideView = wire.view; 12 | var start = function(error) { 13 | // fetch "action" from binding, then call it 14 | exportProfile.getVariable('action').run(); // start export 15 | }; 16 | 17 | // count the number of slides (unfortunately, no API for this) 18 | var counter = function(slidedef) { 19 | if (slidedef) { 20 | slideCount++; 21 | wire.model.get(slideCount).then(counter, start); 22 | } 23 | } 24 | wire.model.get(slideCount).then(counter, start); 25 | }); 26 | ''' 27 | } 28 | 29 | totalSlides = { 30 | js 'slideCount' 31 | } 32 | 33 | nextSlide = { 34 | js 'slideView.next()' 35 | } --------------------------------------------------------------------------------