// Copyright 2007. Adobe Systems, Incorporated. All rights reserved. // This script will apply each comp and then export to a PDF Presentation // Written by Naoki Hada // ZStrings and auto layout by Tom Ruark /* @@@BUILDINFO@@@ Layer Comps To PDF.jsx 1.0.0.13 */ /* // BEGIN__HARVEST_EXCEPTION_ZSTRING $$$/JavaScripts/LayerCompsToPDF/Menu=Layer Comps to PDF... scriptexport export B20FB700-B96A-4C10-B666-8C9B9DEF594E PSHOP_DocHasLayerComps >] >> >> ]]> // END__HARVEST_EXCEPTION_ZSTRING */ // enable double clicking from the Macintosh Finder or the Windows Explorer #target photoshop // debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning) // $.level = 0; // debugger; // launch debugger on next line // on localized builds we pull the $$$/Strings from a .dat file, see documentation for more details $.localize = true; // enable double clicking from the Macintosh Finder or the Windows Explorer #target photoshop // debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning) // $.level = 0; // debugger; // launch debugger on next line // on localized builds we pull the $$$/Strings from a .dat file, see documentation for more details $.localize = true; //================================================================= // Globals //================================================================= // ok and cancel button var runButtonID = 1; var cancelButtonID = 2; // UI strings to be localized var strTitle = localize("$$$/JavaScripts/LayerCompsToPDF/Title=Layer Comps To PDF"); var strButtonRun = localize("$$$/JavaScripts/LayerCompsToPDF/Run=Run"); var strButtonCancel = localize("$$$/JavaScripts/LayerCompsToPDF/Cancel=Cancel"); var strHelpText = localize("$$$/JavaScripts/LayerCompsToPDF/HelpText=Please specify the location where flat image files should be saved. Once Photoshop has saved these files, it will launch PDF Presentation in order to convert each file into a PDF page."); var strLabelDestination = localize("$$$/JavaScripts/LayerCompsToPDF/Destination=Destination:"); var strButtonBrowse = localize("$$$/JavaScripts/LayerCompsToPDF/Browse=&Browse..."); var strCheckboxSelectionOnly = localize("$$$/JavaScripts/LayerCompsToPDF/Selected=&Selected Layer Comps Only"); var strPanelSlideShowOptions = localize("$$$/JavaScripts/LayerCompsToPDF/SlideShow=Slideshow Options:"); var strLabelAdvaceEvery = localize("$$$/JavaScripts/LayerCompsToPDF/AdvanceEvery=&Advance Every"); var strLabelSecond = localize("$$$/JavaScripts/LayerCompsToPDF/Seconds=Seconds"); var strLabelLoopAfterLastPage = localize("$$$/JavaScripts/LayerCompsToPDF/Loop=&Loop after last page"); var strAlertSpecifyDestination = localize("$$$/JavaScripts/LayerCompsToPDF/SpecifyDestination=Please specify destination."); var strTitleSelectDestination = localize("$$$/JavaScripts/LayerCompsToPDF/SelectDestination=Select Destination"); var strAlertDocumentMustBeOpened = localize("$$$/JavaScripts/LayerCompsToPDF/OneDocument=You must have a document open to export!"); var strAlertNoLayerCompsFound = localize("$$$/JavaScripts/LayerCompsToPDF/NoComps=No layer comps found in document!"); var strAlertNoLayerCompsSelected = localize("$$$/JavaScripts/LayerCompsToPDF/NoSelectedComps=No layer comps selected in document!"); var strAlertWasSuccessful = localize("$$$/JavaScripts/LayerCompsToPDF/Success= was successful."); var strAlertFailed = localize("$$$/JavaScripts/LayerCompsToPDF/Fail= failed."); var strMessage = localize("$$$/JavaScripts/LayerCompsToPDF/Message=Layer Comps To PDF action settings"); var strEditTextDestinationLength = localize("$$$/locale_specific/JavaScripts/LayerCompsToPDF/EditTextDestinationLength=160" ); var strEditTextSecondsLength = localize("$$$/locale_specific/JavaScripts/LayerCompsToPDF/EditTextSecondsLength=25" ); var strAlertIncorrectDestination = localize("$$$/JavaScripts/LayerCompsToPDF/IncorrectDestination=Unable to export. PDF destination does not exist on system."); var strAlertSaveDocument= localize("$$$/JavaScripts/LayerCompsToPDF/Filenotsaved= Active document has not been saved. Please save before exporting to PDF."); main(); /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // Function: settingDialog // Usage: create the main dialog // Input: settings to initialize the dialog with , ExportInfo object // Return: true if ok, false if cancel /////////////////////////////////////////////////////////////////////////////// function settingDialog(exportInfo) { dlgMain = new Window("dialog", strTitle); dlgMain.orientation = 'column'; dlgMain.alignChildren = 'left'; // -- top of the dialog, first line dlgMain.add("statictext", undefined, strLabelDestination); // -- two groups, one for left and one for right ok, cancel dlgMain.grpTop = dlgMain.add("group"); dlgMain.grpTop.orientation = 'row'; dlgMain.grpTop.alignChildren = 'top'; dlgMain.grpTop.alignment = 'fill'; // -- group contains four lines dlgMain.grpTopLeft = dlgMain.grpTop.add("group"); dlgMain.grpTopLeft.orientation = 'column'; dlgMain.grpTopLeft.alignChildren = 'left'; dlgMain.grpTopLeft.alignment = 'fill'; // -- the second line in the dialog dlgMain.grpSecondLine = dlgMain.grpTopLeft.add("group"); dlgMain.grpSecondLine.orientation = 'row'; dlgMain.grpSecondLine.alignChildren = 'center'; dlgMain.etDestination = dlgMain.grpSecondLine.add("edittext", undefined, exportInfo.destination); dlgMain.etDestination.preferredSize.width = StrToIntWithDefault( strEditTextDestinationLength, 160 ); dlgMain.btnBrowse = dlgMain.grpSecondLine.add("button", undefined, strButtonBrowse); dlgMain.btnBrowse.onClick = btnBrowseOnClick; // -- the third line in the dialog dlgMain.cbSelection = dlgMain.grpTopLeft.add("checkbox", undefined, strCheckboxSelectionOnly); dlgMain.cbSelection.value = exportInfo.selectionOnly; // -- the fourth line is a panel dlgMain.pnlSlideShow = dlgMain.grpTopLeft.add("panel"); dlgMain.pnlSlideShow.orientation = 'column'; dlgMain.pnlSlideShow.alignChildren = 'left'; dlgMain.pnlSlideShow.alignment = 'fill'; dlgMain.pnlSlideShow.text = strPanelSlideShowOptions; // -- the fifth line, three things in row orientation dlgMain.grpFifthLine = dlgMain.pnlSlideShow.add("group"); dlgMain.grpFifthLine.orientation = 'row'; dlgMain.grpFifthLine.alignChildren = 'center'; dlgMain.grpFifthLine.alignment = 'left'; dlgMain.cbAdvance = dlgMain.grpFifthLine.add( "checkbox", undefined, strLabelAdvaceEvery ); dlgMain.cbAdvance.value = exportInfo.ssoAdvance; dlgMain.etSeconds = dlgMain.grpFifthLine.add("edittext", undefined, exportInfo.ssoSeconds.toString()); dlgMain.etSeconds.preferredSize.width = StrToIntWithDefault( strEditTextSecondsLength, 25 ); dlgMain.grpFifthLine.add("statictext", undefined, strLabelSecond); dlgMain.cbLoop = dlgMain.pnlSlideShow.add( "checkbox", undefined, strLabelLoopAfterLastPage ); dlgMain.cbLoop.value = exportInfo.ssoLoop; // the right side of the dialog, the ok and cancel buttons dlgMain.grpTopRight = dlgMain.grpTop.add("group"); dlgMain.grpTopRight.orientation = 'column'; dlgMain.grpTopRight.alignChildren = 'fill'; dlgMain.btnRun = dlgMain.grpTopRight.add("button", undefined, strButtonRun ); dlgMain.btnRun.onClick = btnRunOnClick; dlgMain.btnCancel = dlgMain.grpTopRight.add("button", undefined, strButtonCancel ); dlgMain.btnCancel.onClick = function() { var d = this; while (d.type != 'dialog') { d = d.parent; } d.close(cancelButtonID); } dlgMain.defaultElement = dlgMain.btnRun; dlgMain.cancelElement = dlgMain.btnCancel; // the bottom of the dialog dlgMain.grpBottom = dlgMain.add("group"); dlgMain.grpBottom.orientation = 'column'; dlgMain.grpBottom.alignChildren = 'left'; dlgMain.grpBottom.alignment = 'fill'; dlgMain.pnlHelp = dlgMain.grpBottom.add("panel"); dlgMain.pnlHelp.alignment = 'fill'; dlgMain.etHelp = dlgMain.pnlHelp.add("statictext", undefined, strHelpText, {multiline:true}); dlgMain.etHelp.alignment = 'fill'; // do not allow anything except for numbers 0-9 //dlgMain.etSeconds.addEventListener ('keydown', NumericEditKeyboardHandler); // in case we double clicked the file app.bringToFront(); dlgMain.center(); var result = dlgMain.show(); if (cancelButtonID == result) { return result; } // get setting from dialog exportInfo.destination = dlgMain.etDestination.text; exportInfo.selectionOnly = dlgMain.cbSelection.value; exportInfo.ssoAdvance = dlgMain.cbAdvance.value; exportInfo.ssoSeconds = dlgMain.etSeconds.text; exportInfo.ssoLoop = dlgMain.cbLoop.value; var pdf = ".pdf"; if ((File.fs === "Macintosh" ) && (exportInfo.destination.toLowerCase().lastIndexOf("/") == -1)) { exportInfo.destination = Folder.myDocuments + "/" + exportInfo.destination; } if ((exportInfo.destination.length - pdf.length) != exportInfo.destination.toLowerCase().lastIndexOf(pdf )) { exportInfo.destination += pdf; // add ".pdf" if there is no PDF extension } return result; } /////////////////////////////////////////////////////////////////////////////// // Function: btnRunOnClick // Usage: routine is assigned to the onClick method of the run button // Input: checks the dialog params and closes with the dialog with true // Return: , dialog is closed with true on success /////////////////////////////////////////////////////////////////////////////// function btnRunOnClick() { // check if the setting is properly var destination = dlgMain.etDestination.text; if (destination.length == 0) { alert(strAlertSpecifyDestination); return; } var destPath = Folder(destination).parent; if (!destPath.exists) { alert(strAlertIncorrectDestination + "\n" +destPath ); return; } // find the dialog in this auto layout mess var d = this; while (d.type != 'dialog') { d = d.parent; } d.close(runButtonID); } /////////////////////////////////////////////////////////////////////////////// // Function: btnBrowseOnClick // Usage: routine is assigned to the onClick method of the browse button // Input: pop the selectDialog, and get a folder // Return: , sets the destination edit text /////////////////////////////////////////////////////////////////////////////// function btnBrowseOnClick() { var selFile = File.saveDialog(strTitleSelectDestination); if ( selFile != null ) { dlgMain.etDestination.text = selFile.fsName; } dlgMain.defaultElement.active = true; return; } /////////////////////////////////////////////////////////////////////////////// // Function: ExportInfo // Usage: object for holding the dialog parameters // Input: // Return: object holding the export info /////////////////////////////////////////////////////////////////////////////// function ExportInfo() { this.destination = app.activeDocument.path + "/" + app.activeDocument.name.substr(0,app.activeDocument.name.lastIndexOf("."))+".pdf" ; this.selectionOnly = false; this.ssoAdvance = true; this.ssoSeconds = 5; this.ssoLoop = false; } /////////////////////////////////////////////////////////////////////////////// // Function: setTempFolder // Usage: create a temp folder using random numbers // Input: export info object // Return: tempLocation of the object is set /////////////////////////////////////////////////////////////////////////////// function setTempFolder(exportInfo) { var tempfolder = Folder.userData; while(true) { exportInfo.tempLocation = tempfolder.path + "/temp_psLCexp"; var testFolder = new Folder(exportInfo.tempLocation); if (!testFolder.exists) { testFolder.create(); break; } break; } } /////////////////////////////////////////////////////////////////////////////// // Function: zeroSuppress // Usage: create a string padded with 0's // Input: num and number of digits to pad // Return: zero padded num /////////////////////////////////////////////////////////////////////////////// function zeroSuppress (num, digit) { var tmp = num.toString(); while(tmp.length < digit) tmp = "0" + tmp; return tmp; } function logToHeadLights(eventRecord) { var headlightsActionID = stringIDToTypeID("headlightsLog"); var desc = new ActionDescriptor(); desc.putString(stringIDToTypeID("subcategory"), "Export"); desc.putString(stringIDToTypeID("eventRecord"), eventRecord); executeAction(headlightsActionID, desc, DialogModes.NO); } /////////////////////////////////////////////////////////////////////////////// // Function: main // Usage: the main routine for this JavaScript // Input: // Return: /////////////////////////////////////////////////////////////////////////////// function main() { if ( app.documents.length <= 0 ) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert( strAlertDocumentMustBeOpened ); } return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } // check to make sure document is saved before running exportInfo try { app.activeDocument.path } catch (e) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert(strAlertSaveDocument ); this.destination = false; } return 'cancel'; } var exportInfo = new ExportInfo(); //~ // look for last used params via Photoshop registry, getCustomOptions will throw if none exist //~ try { //~ var d = app.getCustomOptions("f2e27792-1ef0-4f6f-a157-3a6ad8f6edf0"); //~ descriptorToObject(exportInfo, d, strMessage); //~ } //~ catch(e) { //~ // it's ok if we don't have any options, continue with defaults //~ } // see if I am getting descriptor parameters descriptorToObject(exportInfo, app.playbackParameters, strMessage); if ( DialogModes.ALL == app.playbackDisplayDialogs ) { if (cancelButtonID == settingDialog(exportInfo)) { return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } } try { var docName = app.activeDocument.name; setTempFolder(exportInfo); var compsName = new String("none"); var compsCount = app.activeDocument.layerComps.length; if ( compsCount <= 0 ) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert ( strAlertNoLayerCompsFound ); } return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } else if ( selectedCompsConfigError(app.activeDocument.layerComps, exportInfo) ) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert ( strAlertNoLayerCompsSelected ); } return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } else { app.activeDocument = app.documents[docName]; docRef = app.activeDocument; var tempFileList = new Array(); // array of File var exportFileCount = 0; for ( compsIndex = 0; compsIndex < compsCount; compsIndex++ ) { var compRef = docRef.layerComps[compsIndex]; if (exportInfo.selectionOnly && !compRef.selected) continue; // selected only compRef.apply(); var duppedDocument = app.activeDocument.duplicate(); if (duppedDocument.bitsPerChannel == BitsPerChannelType.THIRTYTWO) duppedDocument.bitsPerChannel = BitsPerChannelType.SIXTEEN; var fileNameBody = zeroSuppress(compsIndex, 4); fileNameBody += "_" + compRef.name; if (null != compRef.comment) fileNameBody += "_" + compRef.comment; fileNameBody = fileNameBody.replace(/[:\/\\*\?\"\<\>\|\\\r\\\n]/g, "_"); // '/\:*?"<>|\r\n' -> '_' if (fileNameBody.length > 120) fileNameBody = fileNameBody.substring(0,120); var tempFile = File.encode(exportInfo.tempLocation + "/" + fileNameBody + ".psd"); tempFile = new File( tempFile ); tempFileList[exportFileCount] = tempFile; duppedDocument.saveAs( tempFile ); duppedDocument.close(); exportFileCount++; } if (exportFileCount == 0) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert(strTitle + strAlertFailed ); } return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } // run PDF Presentation var presentationOptions = new PresentationOptions(); presentationOptions.presentation = true; presentationOptions.view = true; presentationOptions.autoAdvance = exportInfo.ssoAdvance; presentationOptions.interval = exportInfo.ssoSeconds; presentationOptions.loop = exportInfo.ssoLoop; app.makePDFPresentation(tempFileList, File(exportInfo.destination), presentationOptions); // delete temporary files for ( compsIndex = 0; compsIndex < exportFileCount; compsIndex++ ) { tempFileList[compsIndex].remove(); } // delete temprary folder var tempFolder = new Folder(exportInfo.tempLocation); tempFolder.remove(); var d = objectToDescriptor(exportInfo, strMessage); app.putCustomOptions("f2e27792-1ef0-4f6f-a157-3a6ad8f6edf0", d); var dd = objectToDescriptor(exportInfo, strMessage); app.playbackParameters = dd; if ( DialogModes.ALL == app.playbackDisplayDialogs ) { alert(strTitle + strAlertWasSuccessful + "\n" + exportInfo.destination); } app.playbackDisplayDialogs = DialogModes.ALL; logToHeadLights("Layer Comp to PDF count: " + exportFileCount); } } catch (e) { if ( DialogModes.NO != app.playbackDisplayDialogs ) { alert(e); } return 'cancel'; // quit, returning 'cancel' (dont localize) makes the actions palette not record our script } } /////////////////////////////////////////////////////////////////////////////// // Function: objectToDescriptor // Usage: create an ActionDescriptor from a JavaScript Object // Input: JavaScript Object (o) // object unique string (s) // Pre process converter (f) // Return: ActionDescriptor // NOTE: Only boolean, string, number and UnitValue are supported, use a pre processor // to convert (f) other types to one of these forms. // REUSE: This routine is used in other scripts. Please update those if you // modify. I am not using include or eval statements as I want these // scripts self contained. /////////////////////////////////////////////////////////////////////////////// function objectToDescriptor (o, s, f) { if (undefined != f) { o = f(o); } var d = new ActionDescriptor; var l = o.reflect.properties.length; d.putString( app.charIDToTypeID( 'Msge' ), s ); for (var i = 0; i < l; i++ ) { var k = o.reflect.properties[i].toString(); if (k == "__proto__" || k == "__count__" || k == "__class__" || k == "reflect") continue; var v = o[ k ]; k = app.stringIDToTypeID(k); switch ( typeof(v) ) { case "boolean": d.putBoolean(k, v); break; case "string": d.putString(k, v); break; case "number": d.putDouble(k, v); break; default: { if ( v instanceof UnitValue ) { var uc = new Object; uc["px"] = charIDToTypeID("#Rlt"); // unitDistance uc["%"] = charIDToTypeID("#Prc"); // unitPercent d.putUnitDouble(k, uc[v.type], v.value); } else { throw( new Error("Unsupported type in objectToDescriptor " + typeof(v) ) ); } } } } return d; } /////////////////////////////////////////////////////////////////////////////// // Function: descriptorToObject // Usage: update a JavaScript Object from an ActionDescriptor // Input: JavaScript Object (o), current object to update (output) // Photoshop ActionDescriptor (d), descriptor to pull new params for object from // object unique string (s) // JavaScript Function (f), post process converter utility to convert // Return: Nothing, update is applied to passed in JavaScript Object (o) // NOTE: Only boolean, string, number and UnitValue are supported, use a post processor // to convert (f) other types to one of these forms. // REUSE: This routine is used in other scripts. Please update those if you // modify. I am not using include or eval statements as I want these // scripts self contained. /////////////////////////////////////////////////////////////////////////////// function descriptorToObject (o, d, s, f) { var l = d.count; if (l) { var keyMessage = app.charIDToTypeID( 'Msge' ); if ( d.hasKey(keyMessage) && ( s != d.getString(keyMessage) )) return; } for (var i = 0; i < l; i++ ) { var k = d.getKey(i); // i + 1 ? var t = d.getType(k); strk = app.typeIDToStringID(k); switch (t) { case DescValueType.BOOLEANTYPE: o[strk] = d.getBoolean(k); break; case DescValueType.STRINGTYPE: o[strk] = d.getString(k); break; case DescValueType.DOUBLETYPE: o[strk] = d.getDouble(k); break; case DescValueType.UNITDOUBLE: { var uc = new Object; uc[charIDToTypeID("#Rlt")] = "px"; // unitDistance uc[charIDToTypeID("#Prc")] = "%"; // unitPercent uc[charIDToTypeID("#Pxl")] = "px"; // unitPixels var ut = d.getUnitDoubleType(k); var uv = d.getUnitDoubleValue(k); o[strk] = new UnitValue( uv, uc[ut] ); } break; case DescValueType.INTEGERTYPE: case DescValueType.ALIASTYPE: case DescValueType.CLASSTYPE: case DescValueType.ENUMERATEDTYPE: case DescValueType.LISTTYPE: case DescValueType.OBJECTTYPE: case DescValueType.RAWTYPE: case DescValueType.REFERENCETYPE: default: throw( new Error("Unsupported type in descriptorToObject " + t ) ); } } if (undefined != f) { o = f(o); } } /////////////////////////////////////////////////////////////////////////// // Function: StrToIntWithDefault // Usage: convert a string to a number, first stripping all characters // Input: string and a default number // Return: a number /////////////////////////////////////////////////////////////////////////// function StrToIntWithDefault( s, n ) { var onlyNumbers = /[^0-9]/g; var t = s.replace( onlyNumbers, "" ); t = parseInt( t ); if ( ! isNaN( t ) ) { n = t; } return n; } /////////////////////////////////////////////////////////////////////////////// // Function: NumericEditKeyboardHandler // Usage: Do not allow anything except for numbers 0-9 // Input: ScriptUI keydown event // Return: key is rejected and beep is sounded if invalid /////////////////////////////////////////////////////////////////////////////// function NumericEditKeyboardHandler (event) { try { var keyIsOK = KeyIsNumeric (event) || KeyIsDelete (event) || KeyIsLRArrow (event) || KeyIsTabEnterEscape (event); if (! keyIsOK) { // Bad input: tell ScriptUI not to accept the keydown event event.preventDefault(); /* Notify user of invalid input: make sure NOT to put up an alert dialog or do anything which requires user interaction, because that interferes with preventing the 'default' action for the keydown event */ app.beep(); } } catch (e) { ; // alert ("Ack! bug in NumericEditKeyboardHandler: " + e); } } // key identifier functions function KeyHasModifier (event) { return event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; } function KeyIsNumeric (event) { return (event.keyName >= '0') && (event.keyName <= '9') && ! KeyHasModifier (event); } function KeyIsDelete (event) { // Shift-delete is ok return (event.keyName == 'Backspace') && ! (event.ctrlKey); } function KeyIsLRArrow (event) { return ((event.keyName == 'Left') || (event.keyName == 'Right')) && ! (event.altKey || event.metaKey); } function KeyIsTabEnterEscape (event) { return event.keyName == 'Tab' || event.keyName == 'Enter' || event.keyName == 'Escape'; } /////////////////////////////////////////////////////////////////////////////// // Function: selectedCompsConfigError // Usage: if the selected comps only option is on see if we have any selected // Input: layer comps in the document, export settings // Return: true for option on and none selected /////////////////////////////////////////////////////////////////////////////// function selectedCompsConfigError(docLayerComps, exportInfo) { if (exportInfo.selectionOnly) { var compsCount = docLayerComps.length; for ( compsIndex = 0; compsIndex < compsCount; compsIndex++ ) { var compRef = docLayerComps[ compsIndex ]; if (compRef.selected) { return false; // all good, we have work to do } } return true; // none found selected, this is bad, no work to do } return false; // option off } // End Layer Comps To PDF.jsx