Created
July 25, 2020 17:16
-
-
Save bennadel/84a7bd082351ebcf19d97a1efa14b882 to your computer and use it in GitHub Desktop.
Using Apache POI 3.17 To Save InVision Prototypes As Interactive PowerPoints In Lucee CFML 5.3.6.61
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{ | |
"id": 1, | |
"name": "Step 1", | |
"clientFilename": "step-1.png", | |
"width": 600, | |
"height": 531, | |
"hotspots": [ | |
{ | |
"x": 370, | |
"y": 89, | |
"width": 33, | |
"height": 30, | |
"targetScreenID": 1 | |
}, | |
{ | |
"x": 406, | |
"y": 89, | |
"width": 33, | |
"height": 30, | |
"targetScreenID": 2 | |
}, | |
// .... | |
] | |
}, | |
// ... | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfscript> | |
// In Lucee CFML, when we call the createObject() method to instantiate Java objects, | |
// we can pass in an array of JAR files / directories from which Lucee will load the | |
// desired classes. In this demo, usage of this Array will be encapsulated within the | |
// javaNew() User Defined Function (UDF) at the bottom. | |
poiJarPaths = [ | |
expandPath( "../poi-3.17/" ), | |
expandPath( "../poi-3.17/lib" ), | |
expandPath( "../poi-3.17/ooxml-lib" ) | |
]; | |
// Create some POI Class definitions that we'll need to reference below. | |
PictureType = javaNew( "org.apache.poi.sl.usermodel.PictureData$PictureType" ); | |
SlideLayout = javaNew( "org.apache.poi.xslf.usermodel.SlideLayout" ); | |
ShapeType = javaNew( "org.apache.poi.sl.usermodel.ShapeType" ); | |
TextAlign = javaNew( "org.apache.poi.sl.usermodel.TextParagraph$TextAlign" ); | |
// ------------------------------------------------------------------------------- // | |
// ------------------------------------------------------------------------------- // | |
// Read in our InVision prototype data (this would normally come from the database). | |
screens = deserializeJson( fileRead( "./data.json" ) ); | |
// In a PowerPoint (PPT) / Keynote presentation, all of the slides need to be the | |
// same size. As such, we're going to find the max dimensions of all the screens so | |
// that we can choose slide dimensions that can accommodate the largest screen. | |
maxScreenWidth = getMaxProperty( screens, "width" ); | |
maxScreenHeight = getMaxProperty( screens, "height" ); | |
// When calculating the slide dimensions, we're going to add some margin around each | |
// image which will give us some wiggle room and give us a place to add a Title. | |
slideWidth = ( maxScreenWidth + 30 + 30 ); | |
slideHeight = ( maxScreenHeight + 70 + 30 ); | |
// Create our PowerPoint slide-show with the given dimensions. | |
slideShow = javaNew( "org.apache.poi.xslf.usermodel.XMLSlideShow" ) | |
.init() | |
; | |
slideShow.setPageSize( | |
javaNew( "java.awt.Dimension" ) | |
.init( slideWidth, slideHeight ) | |
); | |
// ------------------------------------------------------------------------------- // | |
// ------------------------------------------------------------------------------- // | |
// When creating the PowerPoint slides, we have to perform TWO PASSES: one pass to | |
// create the individual slides and then another pass to add the hotspots / inter- | |
// slide hyperlinks. We have to do this in two passes because the hyperlinks take | |
// Slide instances as their targets. | |
// -- PASS ONE -- Create the slides and map screen IDs to Slide instances. | |
screenToSlideMap = {}; | |
// Each slide is going to use the Title layout, which gives us a single placeholder | |
// into which we will set the Screen name. | |
titleOnlyLayout = slideShow.getSlideMasters() | |
.first() | |
.getLayout( SlideLayout.TITLE_ONLY ) | |
; | |
for ( screen in screens ) { | |
// Create the slide and map it to the screenID at the same time. | |
slide | |
= screenToSlideMap[ screen.id ] | |
= slideShow.createSlide( titleOnlyLayout ) | |
; | |
// Set the title and place it at the top-left of the slide. | |
title = slide.getPlaceholder( 0 ); | |
title.clearText(); | |
titleText = title.addNewTextParagraph(); | |
titleText.setTextAlign( TextAlign.LEFT ); | |
textRun = titleText.addNewTextRun(); | |
textRun.setText( screen.name ); | |
textRun.setFontSize( 22 ); | |
title.setAnchor( | |
javaNew( "java.awt.Rectangle" ) | |
.init( 30, 22, maxScreenWidth, 30 ) | |
); | |
// In a PowerPoint presentation, the images are added to the slide show | |
// separately from their inclusion into a slide. This way, one picture can be | |
// reused (I assume) on several different slides. So, we're going to first add | |
// the screen binary to the slideshow. | |
// -- | |
// NOTE: In a more variable environment, we would have to pass a different | |
// picture type (PNG, JPEG, GIF, etc). However, in this controlled demo, I know | |
// that all of the images are PNGs. | |
pictureData = slideShow.addPicture( | |
fileReadBinary( "./images/" & screen.clientFilename ), | |
pictureType.PNG | |
); | |
// Then, we're going to paint the screen images as shape on the slide, positioned | |
// below the title. | |
pictureShape = slide.createPicture( pictureData ); | |
pictureShape.setAnchor( | |
javaNew( "java.awt.Rectangle" ) | |
.init( 30, 70, screen.width, screen.height ) | |
); | |
} | |
// -- PASS TWO -- Add the hotspots that link to existing Slides. | |
// Now that we've mapped all the screens to Slide instances, we can go back over | |
// the slides and add the hotspots (each of which links to another slide). | |
for ( screen in screens ) { | |
for ( hotspot in screen.hotspots ) { | |
// Get the Slide references for which slide we're on and which Slide we're | |
// about to link-to. | |
slide = screenToSlideMap[ screen.id ]; | |
targetSlide = screenToSlideMap[ hotspot.targetScreenID ]; | |
// Draw the hotspot shape on the slide. | |
hotspotShape = slide.createAutoShape(); | |
hotspotShape.setShapeType( ShapeType.ROUND_RECT ); | |
hotspotShape.setFillColor( colorNew( 0, 185, 255, 50 ) ); | |
hotspotShape.setLineColor( colorNew( 0, 185, 255, 255 ) ); | |
hotspotShape.setLineWidth( 2 ); | |
hotspotShape.setAnchor( | |
javaNew( "java.awt.Rectangle" ) | |
.init( | |
( hotspot.x + 30 ), | |
( hotspot.y + 70 ), | |
hotspot.width, | |
hotspot.height | |
) | |
); | |
// Link the hotspot shape to the target slide. | |
hotspotLink = hotspotShape.createHyperlink(); | |
hotspotLink.linkToSlide( targetSlide ); | |
} | |
} | |
// ------------------------------------------------------------------------------- // | |
// ------------------------------------------------------------------------------- // | |
// Now that we've generated the PowerPoint presentation from the InVision prototype, | |
// we need to save it to disk. | |
outputFile = javaNew( "java.io.File" ) | |
.init( expandPath( "./prototype.ppt" ) ) | |
; | |
outputFileStream = javaNew( "java.io.FileOutputStream" ) | |
.init( outputFile ) | |
; | |
try { | |
slideShow.write( outputFileStream ); | |
echo( "Your InVision prototype has been generated as PPT!" ); | |
} finally { | |
outputFileStream.close(); | |
} | |
// ------------------------------------------------------------------------------- // | |
// ------------------------------------------------------------------------------- // | |
/** | |
* I create a new Color object from the given RGBA values with ranges 0-255. | |
* | |
* @red255 I am the red channel (0-255). | |
* @green255 I am the green channel (0-255). | |
* @blue255 I am the blue channel (0-255). | |
* @alpha255 I am the alpha channel (0-255). | |
*/ | |
public any function colorNew( | |
required numeric red255, | |
required numeric green255, | |
required numeric blue255, | |
required numeric alpha255 | |
) { | |
var color = javaNew( "java.awt.Color" ) | |
.init( | |
( red255 / 255 ), | |
( green255 / 255 ), | |
( blue255 / 255 ), | |
( alpha255 / 255 ) | |
) | |
; | |
return( color ); | |
} | |
/** | |
* I get the max value for the given property within the given collection. | |
* | |
* @collection I am the collection being inspected. | |
* @collectionProperty I am the property being evaluated. | |
*/ | |
public numeric function getMaxProperty( | |
required array collection, | |
required string collectionProperty | |
) { | |
var maxValue = 0; | |
for ( var item in collection ) { | |
maxValue = max( maxValue, item[ collectionProperty ] ); | |
} | |
return( maxValue ); | |
} | |
/** | |
* I am a short-hand for Java class creation that will use the POI JAR paths for | |
* classes that appear to be POI-related. | |
* | |
* @className I am the Java class being created. | |
*/ | |
public any function javaNew( required string className ) { | |
if ( className.find( "org.apache.poi." ) == 1 ) { | |
return( createObject( "java", className, poiJarPaths ) ); | |
} else { | |
return( createObject( "java", className ) ); | |
} | |
} | |
</cfscript> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment