Goal
This post is on developing a Adobe CC HTML Extension to login to AEM 62, search and download AEM assets (content fragments, images) and place them on a new InDesign document page, effectively creating PDF catalogs with the assets uploaded (and managed in AEM); CC HTML Extensions run on Common Extensibility Platform (CEP) of CC products like InDesign, Photoshop, Illustrator...
Check this post for detailed instructions on how to create and debug CEP panels (MAC and Windows) and these CEP samples
For integrating AEM with InDesign Server to extract media etc. check documentation; Catalog Producer to generate product catalogs in AEM Assets is documented here
Signing toolkit ZXPSignCmd can be downloaded at Adobe Labs (windows version available in the extension source)
Download the Extension Manager command line tool for installing ZXP extensions (Adobe Extension Manager CC no longer supported in CC 2015)
Windows - https://www.adobeexchange.com/ExManCmd_win.zip
MAC - https://www.adobeexchange.com/ExManCmd_mac.zip
Some of the logic in this post was coded by me, some copied shamelessly (written by various Adobe colleagues)
Demo | InDesign AEM ZXP | AEM Sling Referrer Package Install | Source Code
Sling Referrer Filter "Allow Empty" set to "true"
Configuration - http://localhost:4502/system/console/configMgr/org.apache.sling.security.impl.ReferrerFilter
ZXP Installation
Self-Signed Certificates - For dev purposes use OpenSSL and generate a self-signed certificate for signing the ZXP
openssl req -x509 -days 3650 -newkey rsa:2048 -keyout experience-aem-cep-key.pem -out experience-aem-cep-cert.pem
openssl pkcs12 -export -in experience-aem-cep-cert.pem -inkey experience-aem-cep-key.pem -out experience-aem-cep.p12
Install - ExManCmd.exe /install "C:\dev\code\projects\cq62-extensions\eaem-aem-assets-on-indesign-page\temp\eaem-aem-assets-on-indesign-page.zxp"
Installed to location (windows) - C:\Program Files (x86)\Common Files\Adobe\CEP\extensions
Remove - ExManCmd.exe /remove com.experience.aem.cep.idsn
Place AEM Assets Panel - Login
Place AEM Assets Panel - Search, Download and & Place
Assets selected are downloaded to C:\Users\<user>\Documents\eaem folder (created if not exists) on windows; The downloaded assets are placed on new InDesign page
Place AEM Assets Panel - Generate & Upload PDF
PDF generated and saved to C:\Users\<user>\Documents\eaem folder (created if not exists) on windows
PDF Catalog in AEM
Solution
1) Folder structure:
For creating InDesign CEP extensions (html panels), the following is a simple source code folder structure
eaem-aem-assets-on-indesign-page
css
style.css
CSXS
manifest.xml
html
place-aem-assets.html
img
txt.png
js
aem-service.js
init-service.js
place-controller.js
CSInterface-5.2.js
jsx
place-assets.jsx
lib
exmancmd_win
experience-aem-cep.p12
ZXPSignCmd
.debug
build.xml
2) Debugging Panels:
a) Debug url for the panel Place AEM Assets as specified in .debug file is http://localhost:8098/ (available only when the panel is open in InDesign).
Check this post for detailed instructions on how to code & debug CEP panels (MAC and Windows)
<?xml version="1.0" encoding="UTF-8"?> <ExtensionList> <Extension Id="com.experience.aem.cep.idsn.place"> <HostList> <Host Name="IDSN" Port="8098"/> </HostList> </Extension> </ExtensionList>
b) Logging available in C:\Users\<user>\AppData\Local\Temp\CEP6-IDSN.log. Default is INFO, for debugging make sure you have the PlayerDebugMode set to 1 and LogLevel 4 (DEBUG) in
MAC - /Users/<user>/Library/Preferences/com.adobe.CSXS.6.plist
Windows registry key - Computer\HKEY_CURRENT_USER\Software\Adobe\CSXS.6
c) ZXP when installed using ExManCmd gets installed to C:\Program Files (x86)\Common Files\Adobe\CEP\extensions (Windows)
3) CSXS/manifest.xml file is required for every extension and provides necessary configuration information for the extension
<Resources> <MainPath>./html/place-aem-assets.html</MainPath> <ScriptPath>./jsx/place-assets.jsx</ScriptPath> <CEFCommandLine> <Parameter>--enable-nodejs</Parameter> <Parameter>--mixed-context</Parameter> </CEFCommandLine> </Resources>
a) #2 specifies the home page/splash screen of the CEP html extension
b) #3 contains the path to file containing extendscript logic for interacting with the host, here InDesign (to create document and place assets on page)
c) #4 to #7 direct CEP engine to make nodejs module available for the extension panel (used for upload/download of relatively large assets in chunks eg. to download a 1 GB image from AEM)
<!DOCTYPE html> <html lang="en" ng-app="SearchAEM"> <head> <meta charset="utf-8"> <title>Place AEM Assets</title> <link rel="stylesheet" href="../css/style.css"> </head> <body> <div ng-controller="placeController"> <div class="sign-in" ng-show="showLogin"> <div> <h3>DIGITAL ASSET MANAGEMENT</h3> </div> <div> <label>User Name</label> <input type="text" ng-model="j_username" ng-enter="login()"/> </div> <div> <label>Password</label> <input type="password" ng-model="j_password" ng-enter="login()"/> </div> <div> <label>DAM Host</label> <input type="text" value="{{damHost}}" ng-model="damHost" ng-enter="login()"/> </div> <button type="button" ng-click="login()">Sign In</button> </div> <div ng-show="!showLogin"> <div class="top"> <div class="top-left"> <input type="text" placeholder="AEM PDF Upload Path" ng-model="uploadPath"/> <input type="text" placeholder="Search text" ng-model="term" ng-enter="search()"/> </div> <div class="top-right"> <button ng-click="search()">Search</button> </div> </div> <div class="results"> <div class="result-block" ng-class="{ selected : result.selected } " ng-repeat="result in results" ng-click="select(result)"> <div> <img ng-src="{{result.imgPath}}"/> </div> <div> {{result.name}} </div> </div> </div> <div class="bottom"> <div class="bottom-left" ng-show="results.length > 0"> <button ng-click="place()">Place</button> </div> <div class="bottom-right"> <button ng-click="generatePDFAndUpload()">Generate PDF & Upload</button> </div> </div> </div> </div> <script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script> <script src="../js/CSInterface-5.2.js"></script> <script src="../js/init-service.js"></script> <script src="../js/aem-service.js"></script> <script src="../js/place-controller.js"></script> </body> </html>
5) The necessary modules and services are defined in js/init-service.js
'use strict'; (function () { var underscore = angular.module('underscore', []); underscore.factory('_', function () { return window._; }); var cep = angular.module('cep', []); cep.service('csi', CSInterface); cep.factory('cep', ['$window', function ($window) { return $window.cep; }]); cep.factory('fs', ['cep', function (cep) { return cep.fs; }]); cep.factory('nfs', ['cep', function (cep) { return require("fs"); }]); cep.factory('nhttp', function () { return require("http"); }); cep.factory('nqs', function () { return require('querystring') }); cep.factory('nbuffer', function () { return require('buffer'); }); }());
6) js/aem-service.js defines all the necessary services and functions for interacting with AEM (login, search, download, upload etc.)
a) Use CSInterface.evalScript() function to execute extend script functions in the host engine (InDesign) eg. #42 EAEM.placeAssets(), #59 EAEM.exportAsPDF() ES functions
b) #95 loginWithNJS() for AEM login, read the login_token from cookies (#122), get the csrfToken (#132) and make them available for other services (search, download, upload etc)
by setting in angular $rootScope (#130)
c) #159 downloadWithNJS() for downloading large files in chunks (tested with files upto 10GB)
d) #220 uploadWithNJS() to upload generated PDFs in 5MB chunks (utilizing AEM chunk upload feature)
e) #307 SearchService() with necessary functions to search for assets in AEM using Query Builder
'use strict'; (function () { var aem = angular.module('aem', ['underscore', 'cep']); aem.service('aemService', AEMService); aem.factory('searchService', SearchService); AEMService.$inject = [ '_', 'csi', 'fs', '$http', '$rootScope' ,'nhttp', 'nqs', 'nfs', 'nbuffer', '$q' ]; function AEMService(_, csi, fs, $http, $rootScope, nhttp, nqs, nfs, nbuffer, $q){ return { getFilename: getFilename, loginWithNJS: loginWithNJS, appendLoginToken: appendLoginToken, getDownloadPath: getDownloadPath, downloadWithNJS: downloadWithNJS, uploadWithNJS: uploadWithNJS, placeAssets: placeAssets, generatePDF: generatePDF }; function isCEPError(error){ return _.isEmpty(error) || (error.toUpperCase() == "ERROR"); } function placeAssets(filePaths) { if (_.isEmpty(filePaths)) { return $q.when({}); } var deferred = $q.defer(); function handler(result) { if (isCEPError(result)) { deferred.reject("Error placing assets"); return; } deferred.resolve(result); } csi.evalScript("EAEM.placeAssets('" + _.values(filePaths).join(",") + "')", handler); return deferred.promise; } function generatePDF() { var deferred = $q.defer(); function handler(result) { if (isCEPError(result)) { deferred.reject("Error generating PDF"); return; } deferred.resolve(result); } csi.evalScript("EAEM.exportAsPDF()", handler); return deferred.promise; } function getFilename(path) { path = path ? path.replace(/\\/g, '/') : ''; return path.substring(path.lastIndexOf('/') + 1); } function getDownloadPath(){ var folderPath = csi.getSystemPath(SystemPath.MY_DOCUMENTS) + "/eaem"; fs.makedir(folderPath); return folderPath; } function getHostPort(host){ var arr = []; if(host.indexOf("/") >= 0){ host = host.substring(host.lastIndexOf("/") + 1); } if(host.indexOf(":") < 0){ arr.push(host); arr.push("80") }else{ arr.push(host.split(":")[0]); arr.push(host.split(":")[1]); } return arr; } //login with nodejs to capture login cookie function loginWithNJS(username, password, damHost){ var hp = getHostPort(damHost); if(!_.isEmpty($rootScope.dam)){ return $q.when($rootScope.dam); } var deferred = $q.defer(), dam = { host : damHost }; var options = { hostname: hp[0], port: hp[1], path: "/libs/granite/core/content/login.html/j_security_check", headers: { 'Content-type': 'application/x-www-form-urlencoded' }, method: 'POST' }; var req = nhttp.request(options, function(res) { var cookies = res.headers["set-cookie"]; _.each(cookies, function(cookie){ if(cookie.indexOf("login-token") == -1){ return; } dam.loginToken = cookie.split('login-token=')[1]; }); if(_.isEmpty(dam.loginToken)){ deferred.reject("Trouble logging-in, Invalid Credentials?"); return; } $rootScope.dam = dam; $http.get( appendLoginToken(dam.host + '/libs/granite/csrf/token.json')).then(function(data){ dam.csrfToken = data.data.token; deferred.resolve(dam); }) }); req.on('error', function(e) { deferred.reject("Trouble logging-in, Invalid Credentials?"); }); var data = nqs.stringify({ _charset_: "UTF-8", j_username: username, j_password: password, j_validate: true }); req.write(data); req.end(); return deferred.promise; } function appendLoginToken(url){ return url + (url.indexOf("?") == -1 ? "?" : "&") + "j_login_token=" + $rootScope.dam.loginToken; } function downloadWithNJS(damPaths){ if(_.isEmpty(damPaths)){ return $q.when({}); } damPaths = _.uniq(damPaths); var deferred = $q.defer(), filePaths = {}, count = damPaths.length, dam = $rootScope.dam; _.each(damPaths, handler); return deferred.promise; function handler(damPath){ damPath = decodeURIComponent(damPath); var url = appendLoginToken(dam.host + damPath), filePath = getDownloadPath() + "/" + getFilename(damPath); if (nfs.existsSync(filePath)) { nfs.unlinkSync(filePath); } var file = nfs.openSync(filePath, 'w'); var req = nhttp.get(url, function(res) { if(res.statusCode == 404){ handle404(damPath); return; } res.on('data', function(chunk) { nfs.writeSync(file, chunk, 0, chunk.length); }); res.on('end', function() { nfs.closeSync(file); count--; filePaths[damPath] = filePath; if(count != 0){ return; } deferred.resolve(filePaths); }); }); req.on('error', function(e) { deferred.reject("Error downloading file"); }); } function handle404(damPath){ alert("Asset Not Found - " + damPath); } } function uploadWithNJS(localPath, damFolderPath){ if(_.isEmpty(localPath) || _.isEmpty(damFolderPath)){ return $q.when( { "error" : "Empty paths"} ); } var BUFFER_SIZE = 5 * 1024 * 1024, // 5MB dam = $rootScope.dam, uploadPath = appendLoginToken(dam.host + damFolderPath + ".createasset.html"), file = nfs.openSync(localPath, 'r'), deferred = $q.defer(); readNextBytes(0); return deferred.promise; function readNextBytes(offset){ var buffer = nbuffer.Buffer(BUFFER_SIZE, 'base64'), bytes = nfs.readSync(file, buffer, 0, BUFFER_SIZE, null), complete = false; if (bytes < BUFFER_SIZE) { buffer = buffer.slice(0, bytes); complete = true; } uploadBlob(getBlob(buffer), offset, complete); } function uploadBlob(blob, offset, complete) { var fd = new FormData(); fd.append('file', blob); fd.append("fileName", getFilename(localPath)); fd.append("file@Offset", offset); fd.append("file@Length", 0); fd.append("file@Completed", complete); fd.append("_charset_", "utf-8"); return $http.post(uploadPath, fd, { transformRequest: angular.identity, //no transformation return data as-is headers: { 'CSRF-Token' : dam.csrfToken, 'Content-Type': undefined //determine based on file type } }).then(function () { if (complete) { nfs.closeSync(file); deferred.resolve(damFolderPath + "/" + getFilename(localPath)); return; } readNextBytes(offset + BUFFER_SIZE); }, failure); function failure() { nfs.closeSync(file); alert("Error upoading"); } } function getBlob(fileOrBytes){ var bytes = fileOrBytes.data ? atob(decodeURIComponent(escape(fileOrBytes.data)).replace(/\s/g, '')) : fileOrBytes; var bArrays = []; var SLICE_LEN = 1024, end, slice, nums; for (var offset = 0; offset < bytes.length; offset = offset + SLICE_LEN) { end = offset + SLICE_LEN; slice = bytes.slice(offset, end < bytes.length ? end : bytes.length); nums = new Array(slice.length); for (var i = 0; i < slice.length; i++) { nums[i] = fileOrBytes.data ? slice.charCodeAt(i) : slice[i]; } bArrays.push(new Uint8Array(nums)); } return new Blob(bArrays, { type: "application/octet-binary" }); } } } SearchService.$inject = [ '_', '$http', '$rootScope' ]; function SearchService(_, $http, $rootScope){ return function (defaults) { this.aem = "http://localhost:4502"; this.params = _.extend( { j_login_token : $rootScope.dam.loginToken }, defaults); this.numPredicates = 0; this.host = function(aem){ this.aem = aem; return this; }; this.fullText = function (value) { if (!value) { return this; } this.params[this.numPredicates + '_fulltext'] = value; this.numPredicates++; return this; }; this.http = function(){ var builder = this; return $http({ method: 'GET', url: builder.aem + "/bin/querybuilder.json", params: builder.params }); } } } }());
7) Angular controller placeController for binding the logic to html page is defined in js/place-controller.js
'use strict'; (function () { var app = angular.module('SearchAEM', ['aem']); app.directive('ngEnter', ngEnterFn); app.controller('placeController', PlaceController); function ngEnterFn(){ return function(scope, element, attrs) { element.bind("keydown keypress", function(event) { if (event.which === 13) { scope.$apply(function() { scope.$eval(attrs.ngEnter); }); event.preventDefault(); } }); }; } PlaceController.$inject = [ '$scope', 'aemService', 'searchService', '$http', 'csi', 'cep' ]; function PlaceController($scope, aemService, searchService, $http, csi, cep){ $scope.damHost = "localhost:4502"; $scope.showLogin = true; var searchDefaults = { 'path': "/content/dam", 'type': 'dam:Asset', 'orderby': '@jcr:content/jcr:lastModified', 'orderby.sort': 'desc', 'p.hits': 'full', 'p.nodedepth': 2, 'p.limit': 25, 'p.offset': 0 }; $scope.login = login; $scope.search = search; $scope.select = select; $scope.place = place; $scope.generatePDFAndUpload = generatePDFAndUpload; function login() { if (!$scope.j_username || !$scope.j_password || !$scope.damHost) { alert("Enter credentials"); return; } $scope.damHost = $scope.damHost.trim(); if ($scope.damHost.indexOf("http://") == -1) { $scope.damHost = "http://" + $scope.damHost; } function success(){ $scope.showLogin = false; } function error(message){ alert(message); } aemService.loginWithNJS($scope.j_username, $scope.j_password, $scope.damHost) .then(success, error); } function search(){ if (!$scope.term) { alert("Enter search term"); return; } $scope.results = []; var mapHit = function(hit) { var result; result = {}; result.selected = false; result.name = aemService.getFilename(hit["jcr:path"]); result.path = hit["jcr:path"]; result.imgPath = aemService.appendLoginToken($scope.damHost + hit["jcr:path"] + "/jcr:content/renditions/cq5dam.thumbnail.140.100.png"); result.format = hit["jcr:content"]["metadata"]["dc:format"]; if(result.format == "text/html"){ result.imgPath = "../img/txt.png"; } return result; }; new searchService(searchDefaults).host($scope.damHost) .fullText($scope.term) .http() .then(function(resp) { $scope.results = _.compact(_.map(resp.data.hits, mapHit)); }); } function select(result){ result.selected = !result.selected; } function place(){ var toDownload = _.reject($scope.results, function(result){ return !result.selected; }); aemService.downloadWithNJS(_.pluck(toDownload, 'path')) .then(setDownloadedPaths) .then(aemService.placeAssets) .then(aemService.uploadWithNJS) } function setDownloadedPaths(filePaths){ _.each($scope.results, function(result){ result.localPath = filePaths[result.path] || ''; }); return filePaths; } function generatePDFAndUpload(){ if(_.isEmpty($scope.uploadPath)){ alert("Enter PDF upload location in AEM"); return; } aemService.generatePDF() .then(upload) .then(function(damFilePath){ alert("Uploaded - " + damFilePath); }); function upload(pdfPath){ return aemService.uploadWithNJS(pdfPath, $scope.uploadPath); } } } }());
8) InDesign extend script logic for creating a new document and place the downloaded assets on pages is defined in jsx/place-assets.jsx
(function () { if (typeof EAEM == "undefined") { EAEM = { COLUMNS_PER_SPREAD: 3, ROWS_PER_SPREAD: 2 }; } function collectionToArray(theCollection) { return (theCollection instanceof Array) ? theCollection.slice(0) : theCollection.everyItem().getElements().slice(0); } function getContainerAssetCount(spreadOrGroup){ var pageItems = collectionToArray(spreadOrGroup.pageItems), count = 0; for (var pageItemIdx = 0; pageItemIdx < pageItems.length; pageItemIdx++) { var pageItem = pageItems[pageItemIdx]; if (pageItem instanceof Group) { count = count + getContainerAssetCount(pageItem); }else { count++; } } return count; } function getPlaceSpread(document){ var lastSpread = document.spreads.lastItem(); var count = getContainerAssetCount(lastSpread), spread; if (count < (EAEM.COLUMNS_PER_SPREAD * EAEM.ROWS_PER_SPREAD)) { spread = lastSpread; }else{ spread = document.spreads.add(); } return spread; } function getNextGridPos(spread) { var gridPos = { row: 0, column: 0 }; var count = getContainerAssetCount(spread); if(count > 0){ gridPos.row = Math.floor(count / EAEM.COLUMNS_PER_SPREAD); gridPos.column = count % EAEM.COLUMNS_PER_SPREAD; } return gridPos; } function createPageItem(spread) { var rect = spread.textFrames.add(); var y1 = 0; // upper left Y-Coordinate var x1 = 0; // upper left X-Coordinate var y2 = 275; // lower right Y-Coordinate var x2 = 160; // lower right X-Coordinate rect.geometricBounds = [ y1 , x1 , y2 , x2 ]; return rect; } function movePageItem(document, spread, gridPos, rect){ var marginTop = document.marginPreferences.top; var marginBottom = document.marginPreferences.bottom; var marginLeft = document.marginPreferences.left; var marginRight = document.marginPreferences.right; var spreadLeftTop = spread.pages.firstItem().resolve( AnchorPoint.TOP_LEFT_ANCHOR, CoordinateSpaces.SPREAD_COORDINATES)[0]; var spreadRightBottom = spread.pages.lastItem().resolve( AnchorPoint.BOTTOM_RIGHT_ANCHOR, CoordinateSpaces.SPREAD_COORDINATES)[0]; var spreadWidth = spreadRightBottom[0] - spreadLeftTop[0] - marginLeft - marginRight; var spreadHeight = spreadRightBottom[1] - spreadLeftTop[1] - marginTop - marginBottom; var stepH = spreadWidth / EAEM.COLUMNS_PER_SPREAD; var stepV = spreadHeight / EAEM.ROWS_PER_SPREAD; var xPos = spreadLeftTop[0] + gridPos.column * stepH + marginLeft + 10; var yPos = spreadLeftTop[1] + gridPos.row * stepV + marginTop + 25; var rectTop = rect.resolve(AnchorPoint.TOP_LEFT_ANCHOR, CoordinateSpaces.SPREAD_COORDINATES)[0]; var deltaX = xPos - rectTop[0]; var deltaY = yPos - rectTop[1]; rect.move(null,[deltaX, deltaY]); } function placeImage(rect, pdfPath){ rect.contents = ""; rect.contentType = ContentType.UNASSIGNED; rect.place(pdfPath); rect.fit(FitOptions.PROPORTIONALLY); } EAEM.placeAssets = function(commaSepPaths){ var result = "ERROR", document, units = app.scriptPreferences.measurementUnit; try{ app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS; if(app.documents.length == 0){ document = app.documents.add(); }else{ document = app.activeDocument; } var assetsArray = commaSepPaths.split(","); for(var i = 0; i < assetsArray.length; i++){ var spread = getPlaceSpread(document); var gridPos = getNextGridPos(spread); var rect = createPageItem(spread); movePageItem(document, spread, gridPos, rect); placeImage(rect, assetsArray[i]); } result = "SUCCESS"; }catch(err){ result = "ERROR"; } app.scriptPreferences.measurementUnit = units; return result; }; EAEM.exportAsPDF = function(){ var document, result = "ERROR"; try{ if(app.documents.length == 0){ document = app.documents.add(); }else{ document = app.activeDocument; } var pdfPath = Folder.myDocuments.fsName.replace(/\\/g, '/') + "/eaem/" + document.name + ".pdf"; document.exportFile(ExportFormat.pdfType, new File(pdfPath), false, app.pdfExportPresets.item("[High Quality Print]")); result = pdfPath; }catch(err){ result = "ERROR"; } return result; }; })();
This comment has been removed by a blog administrator.
ReplyDeleteI have one question, that is If I will click logout in AEM panel, Links panel shows the image path is missing or $placeholder at the beginning instead of URL?
ReplyDelete