// (c) Copyright 2005. Adobe Systems, Incorporated. All rights reserved. // // MergeToHDR automation in JavaScript // /* @@@BUILDINFO@@@ Merge to HDR.jsx 2.0.0.2 */ /* // BEGIN__HARVEST_EXCEPTION_ZSTRING $$$/JavaScripts/Merge2HDR/Menu=Merge to HDR Pro... automate 9D3174CE-045C-4B87-B7AE-40D8C3319780 // END__HARVEST_EXCEPTION_ZSTRING */ // on localized builds we pull the $$$/Strings from a .dat file $.localize = true; // TODO // - Support registry flag for Camera Raw files // - Use Photoshop "open" // Put header files in a "Stack Scripts Only" folder. The "...Only" tells // PS not to place it in the menu. For that reason, we do -not- localize that // portion of the folder name. var g_StackScriptFolderPath = app.path + "/"+ localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts") + "/" + localize("$$$/private/Exposuremerge/StackScriptOnly=Stack Scripts Only/"); $.evalFile(g_StackScriptFolderPath + "LatteUI.jsx"); $.evalFile(g_StackScriptFolderPath + "StackSupport.jsx"); $.evalFile(g_StackScriptFolderPath + "Geometry.jsx"); $.evalFile(g_StackScriptFolderPath + "CreateImageStack.jsx"); // debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning) // Must leave at zero, otherwise trapping gFileFromBridge fails on QA's debug builds. $.level = 0; // (Window.version.search("d") != -1) ? 1 : 0; // debugger; // launch debugger on next line const kMergeToHDRAlignmentFlag = app.stringIDToTypeID( "MergeToHDRAlignmentFlag" ); const kMergeToHDRCameraCurve = "MToHDRcrv"; const kMergeToHDRUIResponseCurve = app.charIDToTypeID( 'EmCV' ); const kMergeToHDRDeghostParam = app.charIDToTypeID( 'EmDg' ); const kMergeToHDROutputBitDepth = app.charIDToTypeID('EmOD'); const kMergeToHDRACRToning = app.charIDToTypeID('EmAT'); // These must match defs in PResponseCurves.h const kMergeToHDRDeghostOff = -1; const kMergeToHDRDeghostBest = -2; /************************************************************/ // mergeToHDR routines mergeToHDR = new ImageStackCreator( localize("$$$/AdobePlugin/Shared/Exposuremerge/Process/Name=Merge to HDR Pro"), localize('$$$/AdobePlugin/Shared/Exposuremerge/Auto/untitled=Untitled_HDR' ) ); // Set flag for Camera raw asking for linear response files. mergeToHDR.linearizeCamRawFiles = true; // Merge to HDR opens a new output file created by the filter plugin. mergeToHDR.outputClonedFromFirstFile = false; // Set to the deghosting base image index, or to kMergeToHDRDeghostOff or kMergeToHDRDeghostBest mergeToHDR.deghostSetting = null; // If this is uncommented, you'll be given the option of // "Open layered document" in the source pop-up menu if you have a layered document. // The layers on that document are processed as opposed to opening separate files. //mergeToHDR.allowLayeredDocument = true; // "Sticky" options are handled here. mergeToHDR.useAlignment = getPSCustomOption( "MergeToHDRFlags001", "Boolean", kMergeToHDRAlignmentFlag, true ); mergeToHDR.outputBitDepth = getPSCustomOption( "MergeToHDRFlags001", "Integer", kMergeToHDROutputBitDepth, 16 ); mergeToHDR.useACRToning = getPSCustomOption( "MergeToHDRFlags001", "Boolean", kMergeToHDRACRToning, true ); mergeToHDR.customPluginArguments = function( desc ) { // Hack used for windows debug only. var p; if (this.useLayeredDocument) p = app.activeDocument.name; else p = this.stackElements[0].file.fsName; desc.putString( app.charIDToTypeID('EmPt'), p.slice(0,p.lastIndexOf('\\') + 1) ); if (this.deghostSetting != null) desc.putInteger( kMergeToHDRDeghostParam, this.deghostSetting ); if (this.fCameraCurveDesc) desc.putList( kMergeToHDRUIResponseCurve, this.fCameraCurveDesc ); desc.putInteger( kMergeToHDROutputBitDepth, this.outputBitDepth ); desc.putBoolean( kMergeToHDRACRToning, this.useACRToning ); } // Custom version of alignStack that crops to the area covered by // all of the images. mergeToHDR.alignStack = function( stackDoc ) { var cropRect = new TRect( -Infinity, -Infinity, Infinity, Infinity ); function nudge( c, rectSide, op ) { // C++ programmers use #define's and "##" for this sort of voodoo eval( "cropRect." + rectSide + " = Math." + op + "( cropRect." + rectSide + ", c );" ); } selectAllLayers(stackDoc, 2 ); // If the photos are aligned, then trim the photos down // to the rectangular area overlapped by all of them. var i, j, alignInfo = getActiveDocAlignmentInfo( 'Prsp', false, [khighQualityStr] ); if (alignInfo) { // Collect the alignment info into the stackElements var layerList = alignInfo.layerInfo; var baseCorners; alignGroups = new Array(); for (i = 0; i < layerList.length; ++i) { this.stackElements[i].fCorners = layerList[i].corners; this.stackElements[i].fBaseFlag = layerList[i].baseFlag; if (layerList[i].baseFlag) baseCorners = layerList[i].corners; } // Sanity check on the alignment. Seriously under/over exposed images // may get trashed in alignment, and we should weed those out. Anything // with displacement over 30% of the image is obviously crazy... var maxDelta = (baseCorners[2] - baseCorners[0]).vectorLength() * 0.33; var alignErrorMessageShown = false; i = 0; while (i < this.stackElements.length) { if (! this.stackElements[i].fBaseFlag) { var badAlign = false; for (j = 0; (j < 4) && ! badAlign; ++j) { if ((this.stackElements[i].fCorners[j] - baseCorners[j]).vectorLength() > maxDelta) badAlign = true; } if (badAlign) { selectOneLayer( app.activeDocument, this.stackElements[i].fName ); app.activeDocument.layers[this.stackElements[i].fName].remove(); this.stackElements.splice(i,1); // Since fLayerID's actually correspond to the index, update the // remaining IDs to note the removed layer for (var k = i; k < this.stackElements.length; k++) this.stackElements[k].fLayerID--; if (! alignErrorMessageShown) { // This is a pretty vague error message, but it's too late in the game to // get a proper one translated, so this was stolen from Photomerge. alert(localize("$$$/AdobePlugin/Shared/Photomerge/Error/BadPersp1=The perspective distortion for some images is too severe to render.")); alignErrorMessageShown = true; } i--; } } ++i; } if (this.stackElements.length < 2) throw Error( kUserCanceledError ); // "Proabably" won't happen... // Trim the aligned images to the size of their overlapping area for (i in this.stackElements) { if (! this.stackElements[i].fBaseFlag) this.stackElements[i].transform(); var corners = this.stackElements[i].fCorners; // Take the corners and inset cropRect as needed nudge( Math.ceil( corners[0].fX ), "fLeft", "max" ); nudge( Math.ceil( corners[0].fY ), "fTop", "max" ); nudge( Math.floor( corners[1].fX ), "fRight", "min" ); nudge( Math.ceil( corners[1].fY ), "fTop", "max" ); nudge( Math.floor( corners[2].fX ), "fRight", "min" ); nudge( Math.floor( corners[2].fY ), "fBottom", "min" ); nudge( Math.ceil( corners[3].fX ), "fLeft", "max" ); nudge( Math.floor( corners[3].fY ), "fBottom", "min" ); } var cropArgs = [UnitValue( cropRect.fLeft, "px" ), UnitValue( cropRect.fTop, "px" ),UnitValue( cropRect.fRight, "px" ), UnitValue( cropRect.fBottom, "px" )]; stackDoc.crop( cropArgs ); // Reset the element's height & width var newWidth = cropRect.getWidth(), newHeight = cropRect.getHeight(); for (i in this.stackElements) { this.stackElements[i].fWidth = newWidth; this.stackElements[i].fHeight = newHeight; this.stackElements[i].fString = this.stackElements[i].toString(); } } } mergeToHDR.lightroomOpen = function( filename ) { try { status = photoshop.openFromLightroom( filename, null, gLightroomDocID, gBridgeTalkID, gLightroomSaveParams, DialogModes.NO ); // On normal open status has a typeNULL key, but if it fails, it's // empty. This seems to be our only clue you've whacked the escape key. if (status.count == 0) throw Error( kUserCanceledError ); } catch (err) { if (err.number == kErrTempDiskFull) { this.scratchDiskFullAlert(); throw err; } else if (err.number == kUserCanceledError) throw err; return null; } return app.activeDocument; } mergeToHDR.convertActiveLayerToSmartObject = function () { try { var idnewPlacedLayer = stringIDToTypeID( "newPlacedLayer" ); executeAction( idnewPlacedLayer, undefined, DialogModes.NO ); } catch(e) { } } mergeToHDR.invokeACRFilter = function ( showDialog ) { function S(x) { return stringIDToTypeID(x); } function C(x) { return charIDToTypeID(x); } try { app.activeDocument.flatten(); // Avoid spurious alert when before ACR launch // Make the PS UI draw before throwing up a dialog. if (showDialog) app.refresh (); var desc9 = new ActionDescriptor(); desc9.putInteger( S("depth"), 16 ); var desc10 = new ActionDescriptor(); desc10.putString( C("CMod"), """Filter""" ); desc10.putEnumerated( C("Sett"), C("Sett"), C("Defa") ); desc9.putObject( S("with"), S("Adobe Camera Raw Filter"), desc10 ); result = executeAction( S("convertMode"), desc9, showDialog ? DialogModes.ALL : DialogModes.NO ); return result; } catch (err) { if (err.number != kUserCanceledError) // psUserCanceled, as found in CPsError.h, found in the ScriptingSupport plugin sources alert(err, this.pluginName, true ); return 'cancel'; } return 'cancel'; // Should never get here } mergeToHDR.mergeStackElements = function( showDialog ) { // Add the Luminance values to the activeDoc's. function addLuminanceMetadata( luminValue ) { // Extendscript's XML library doesn't like to deal with "unbound" // namespaces, so just hacking the string is easier for now. var xmpData = app.activeDocument.xmpMetadata.rawData.toString(); var insertPos = xmpData.search(//); if (insertPos > 0) { var newTag = "" + luminValue + "\n " xmpData = xmpData.slice(0,insertPos) + newTag + xmpData.slice(insertPos); app.activeDocument.xmpMetadata.rawData = xmpData; } } // Pull out the exposure time, since it presumably // changed while taking the HDR set. function removeExposureMetadata() { var xmpData = app.activeDocument.xmpMetadata.rawData.toString(); xmpData = xmpData.replace (/[\d\/]+<\/exif:ShutterSpeedValue>/, "") xmpData = xmpData.replace (/[\d\/]+<\/exif:ExposureTime>/, "") app.activeDocument.xmpMetadata.rawData = xmpData; } var result, i, stackDoc = null; if (this.useLayeredDocument) stackDoc = app.activeDocument; else stackDoc = this.loadStackLayers(); if (! stackDoc) return 'cancel'; var cameraID = this.stackElements[0].fCameraID; // Different cameras would be Really Strange, but check for it anyway... for (var i = 1; i < this.stackElements.length; ++i) if (this.stackElements[i].fCameraID != cameraID) { alert( this.pluginName + localize("$$$/AdobePlugin/Shared/Exposuremerge/DiffCam= - Images to merge may have been taken with different cameras"), this.pluginName, true ); break; } var desc = null; this.fCameraCurveDesc = null; try { desc = app.getCustomOptions( kMergeToHDRCameraCurve + cameraID ); this.fCameraCurveDesc = desc.getList( kMergeToHDRUIResponseCurve ); } catch (e) { } // The MergeToHDR filter plugin must have an RGB document, HDR is always RGB. // If we get grayscale photos, convert them back after the fact. var mustRestoreGray = (stackDoc.mode == DocumentMode.GRAYSCALE); if (stackDoc.mode != DocumentMode.RGB) stackDoc.changeMode( ChangeMode.RGB ); result = this.invokeFilterPlugin( "AdobeExposureMergeUI", showDialog ); var stackDocResolution = stackDoc.resolution; var saveXMPData = stackDoc.xmpMetadata.rawData; if (this.useLayeredDocument) { // Nuke the "destination" layer that got created (M2HDR holdover) stackDoc.layers[this.pluginName].remove(); } else stackDoc.close(SaveOptions.DONOTSAVECHANGES); if (result) // noErr { var tmpFilePath = result.getString( app.charIDToTypeID('EmTp') ); this.outputBitDepth = result.getInteger( kMergeToHDROutputBitDepth ); this.useACRToning = result.getInteger( kMergeToHDRACRToning ); var exposure = result.getDouble( app.charIDToTypeID('EmEs') ); // This gamma is computed by Ward's code based on the camera response, // but applying it leaves a somewhat counter-intiutive result (two bugs // filed so far). Leaving it out for now. -jp 13-jun-08 //var gamma = result.getDouble( app.charIDToTypeID('EmGm') ); var gamma = 1.0; // When passing off to ACR toning we want the default exposure // and gamma settings to avoid visual differences between what // you see in ACR and what you see in PS afterwards. if (this.useACRToning) { exposure = 0.0; gamma = 1.0; } var whiteLuminance = result.getDouble( app.charIDToTypeID('EmWL') ); this.fCameraCurveDesc = result.hasKey( kMergeToHDRUIResponseCurve ) ? result.getList( kMergeToHDRUIResponseCurve ) : null; // If the camera metadata was corrupted (e.g., by a Camera Raw "correction"), // then don't write it back out to the preferences. if (! this.exposureMetadataValid) this.fCameraCurveDesc = null; var tmpFile = File( tmpFilePath ); var stackElem = new StackElement( tmpFile ); var tmpdoc = stackElem.silentOpen( false, false ); app.activeDocument = tmpdoc; duplicateDocument( this.newDocName() ); var newHDRDoc = app.activeDocument; if ((this.saveColorProfileType == ColorProfile.CUSTOM) && (this.saveColorProfileName)) { // Before we assign the color profile name, we need to make // sure it's one PS already knows about, otherwise a dialog pops up. var availableCustomProfiles = getColorProfileList('rStd'); // ACE_Selector_RGB_Standard availableCustomProfiles // ACE_Selector_RGB_OtherInputCapable = availableCustomProfiles.concat( getColorProfileList('rInp') ); if (availableCustomProfiles.join('|').indexOf( this.saveColorProfileName ) != -1) // This assignment takes care of setting the colorProfileType to "CUSTOM" newHDRDoc.colorProfileName = this.saveColorProfileName; else newHDRDoc.colorProfileType = ColorProfile.WORKING; // Punt - no way to assign random input name } else { // This assignment takes care of assigning the name (or lack thereof for ".NONE") if (this.saveColorProfileType != ColorProfile.CUSTOM) newHDRDoc.colorProfileType = this.saveColorProfileType; } tmpdoc.close(SaveOptions.DONOTSAVECHANGES); tmpFile.remove(); // In a world of Owls and Tabs, closing "tmpdoc" can reset the activeDoc // to another random file that happened to be open. This ensures we're // talking to the right one. PR 1731208 app.activeDocument = newHDRDoc; // If we're invoked from Lightroom, we need add special voodoo to the file // so it has the proper tags to generate the save notification LR needs. if (this.checkForLightroomGlobals()) setLightroomFileParams( gLightroomDocID, gBridgeTalkID, gLightroomSaveParams ) resetFrontmostDocumentFormat(); // Restore the metadata from the original photos, // but pull out the exposure time. app.activeDocument.xmpMetadata.rawData = saveXMPData; removeExposureMetadata(); // Add the HDR luminance metadata addLuminanceMetadata( whiteLuminance ); if (mustRestoreGray) app.activeDocument.changeMode( ChangeMode.GRAYSCALE ); setFrontmostResolution( stackDocResolution ); if (exposure != 0.0) setFrontmostExposure( exposure, gamma ); if (this.outputBitDepth != 32) { try { // convertFromHDR( this.outputBitDepth ); convertFromHDRNoDialog( this.outputBitDepth, result ); } // If the conversion fails (e.g., user cancels) just go ahead and finish up. catch (err) {} } var flagDesc = new ActionDescriptor(); flagDesc.putBoolean( kMergeToHDRAlignmentFlag, mergeToHDR.useAlignment ); flagDesc.putInteger( kMergeToHDROutputBitDepth, mergeToHDR.outputBitDepth ); flagDesc.putInteger( kMergeToHDRACRToning, mergeToHDR.useACRToning ); app.putCustomOptions( "MergeToHDRFlags001", flagDesc, true ); if (this.fCameraCurveDesc) { var curveDesc = new ActionDescriptor(); curveDesc.putList( kMergeToHDRUIResponseCurve, this.fCameraCurveDesc ); // Note the camera name is used in the customOptions name, not in the // Descriptor, because stringIDToTypeID isn'g guarenteed to be the same // across launches. app.putCustomOptions( kMergeToHDRCameraCurve + cameraID, curveDesc, true ); } // Convert to smart object and invoke ACR Filter if ACR Toning is specified // (only applicable to 32 bit). if (this.outputBitDepth == 32 && mergeToHDR.useACRToning) { this.convertActiveLayerToSmartObject (); return this.invokeACRFilter ( showDialog ); } } else { return 'cancel'; } } // "Main" execution of Merge to HDR mergeToHDR.doInteractiveMerge = function () { // Check to make sure the required plugins are available. function CheckPluginOK( key, message ) { var plugFile; if (Folder.fs == "Windows") { plugFile= new File( app.path + "/Required/Plug-Ins/" + key ); } else { key = key.replace(/[.].*$/, ".plugin"); // All mac plugins use the same suffix plugFile = new Folder( Folder.appPackage + "/Contents/Required/Plug-Ins/" + key ); } if (! plugFile.exists) { alert(message); return false; } return true; } if (! (CheckPluginOK( "File Formats/PBM.8BI", localize("$$$/AdobePlugin/Shared/Exposuremerge/Auto/MissingPBM=Merge to HDR: The Portable Bit Map (PBM) Plug-in is missing.")) && CheckPluginOK( "Automate/HDRMergeUI.8BF", localize("$$$/AdobePlugin/Shared/Exposuremerge/Auto/MissingHDRPlug=Merge to HDR: The Merge to HDR Pro (HDRMergeUI) Plug-in is missing.")))) return 'cancel'; this.getFilesFromBridgeOrDialog( localize("$$$/private/Exposuremerge/M2HDRexv=M2HDR.exv") ); if (this.stackElements) return this.mergeStackElements( true ); else return 'cancel'; } // Function to call from scripts mergeToHDR.mergeFilesToHDR = function(filelist, alignFlag, deghostFlag) { if (typeof(alignFlag) == 'boolean') mergeToHDR.useAlignment = alignFlag; if (typeof(deghostFlag) == 'number') mergeToHDR.deghostSetting = deghostFlag; if (filelist.length < 2) { alert(localize("$$$/AdobePlugin/Shared/Exposuremerge/Auto/AtLeast2=Merge to HDR needs at least two files selected."), this.pluginName, true ); return; } var j; this.stackElements = new Array(); for (j in filelist) { var f = filelist[j]; this.stackElements.push( new StackElement( (typeof(f) == 'string') ? File(f) : f ) ); } if (this.stackElements.length > 1) return this.mergeStackElements( false ); } if ((typeof(runMergeToHDRFromScript) == 'undefined') || (runMergeToHDRFromScript == false)) mergeToHDR.doInteractiveMerge();