#target bridge /* Copyright (C) 2013 David Milligan * * With additions (C) 2014 by Paulo Jan * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ var percentile = 0.7; var evCurveCoefficent = 2 / Math.log(2); var keyframeRating = 1; var iterations = 3; var previewSize = 200; var deflickerThreshold = 3; var cropX = 0; var cropY = 0; var cropWidth = 100; var cropHeight = 100; var usePV2010 = false; var undoLevels = 5; var saveData = false; var rampGradientCorrections = true; var rampRadialCorrections = true; function debugPrint(str) { } function BrRamp() { this.requiredContext = "\tAdobe Bridge must be running.\n\tExecute against Bridge as the Target.\n"; this.rampMenuID = "brRampContextMenu"; this.deflickerMenuID = "deflickerContextMenu"; this.rampAllMenuID = "brRampMultiContextMenu"; this.backupMenuID = "brRampBackupContextMenu"; this.undoMenuID = "brUndoContextMenu"; } function loadXMPLibrary() { // Load the XMP Script library if( xmpLib == undefined ) { if( Folder.fs == "Windows" ) { var pathToLib = Folder.startup.fsName + "/AdobeXMPScript.dll"; } else { var pathToLib = Folder.startup.fsName + "/AdobeXMPScript.framework"; } var libfile = new File( pathToLib ); var xmpLib = new ExternalObject("lib:" + pathToLib ); } } BrRamp.prototype.run = function() { var retval = true; if(!this.canRun()) { retval = false; return retval; } loadXMPLibrary(); // create the menu element var rampCommand = MenuElement.create("command", "Ramp...", "at the end of Thumbnail", this.rampMenuID); var rampMultipleCommand = MenuElement.create("command", "Ramp Multiple...", "after " + this.rampMenuID, this.rampAllMenuID); var deflickerCommand = MenuElement.create("command", "Deflicker...", "at the end of Thumbnail", this.deflickerMenuID); var backupCommand = MenuElement.create("command", "Backup XMP Sidecars...", "at the end of Thumbnail", this.backupMenuID); var undoCommand = MenuElement.create("command", "Undo Ramp/Deflicker", "at the end of Thumbnail", this.undoMenuID); rampCommand.onSelect = function(m) { try { runRamp(); } catch(error) { alert(error); } }; rampMultipleCommand.onSelect = function(m) { try { runRampMultiple(); } catch(error) { alert(error); } }; deflickerCommand.onSelect = function(m) { try { runDeflickerMain(); } catch(error) { alert(error); } }; backupCommand.onSelect = function(m) { try { runBackupXMP(); } catch(error) { alert(error); } }; undoCommand.onSelect = function(m) { try { runUndo(); } catch(error) { alert(error); } }; var onDisplay = function() { try { cntCommand.enabled = true; if(app.document.selections.length > 0) { for(var i = 0; i < app.document.selections.length; i++) { if(app.docuument.selections[i].container) { cntCommand.enabled = false; break; } } } else { cntCommand.enabled = false; } } catch(error){ } }; rampCommand.onDisplay = onDisplay; rampMultipleCommand.onDisplay = onDisplay; deflickerCommand.onDisplay = onDisplay; backupCommand.onDisplay = onDisplay; undoCommand.onDisplay = onDisplay; return retval; } BrRamp.prototype.canRun = function() { return BridgeTalk.appName == "bridge" && ! MenuElement.find(this.menuID); } var commonProperties = [ "Temperature", "Tint", "Vibrance", "Saturation", "Sharpness", "SharpenRadius", "SharpenDetail", "SharpenEdgeMasking", "ColorNoiseReduction", "ColorNoiseReductionDetail", "ColorNoiseReductionSmoothness", "LuminanceSmoothing", "VignetteAmount", "ShadowTint", "RedHue", "RedSaturation", "GreenHue", "GreenSaturation", "BlueHue", "BlueSaturation", "HueAdjustmentRed", "HueAdjustmentOrange", "HueAdjustmentYellow", "HueAdjustmentGreen", "HueAdjustmentAqua", "HueAdjustmentBlue", "HueAdjustmentPurple", "HueAdjustmentMagenta", "SaturationAdjustmentRed", "SaturationAdjustmentOrange", "SaturationAdjustmentYellow", "SaturationAdjustmentGreen", "SaturationAdjustmentAqua", "SaturationAdjustmentBlue", "SaturationAdjustmentPurple", "SaturationAdjustmentMagenta", "LuminanceAdjustmentRed", "LuminanceAdjustmentOrange", "LuminanceAdjustmentYellow", "LuminanceAdjustmentGreen", "LuminanceAdjustmentAqua", "LuminanceAdjustmentBlue", "LuminanceAdjustmentPurple", "LuminanceAdjustmentMagenta", "SplitToningShadowHue", "SplitToningShadowSaturation", "SplitToningHighlightHue", "SplitToningHighlightSaturation", "SplitToningBalance", "ParametricShadows", "ParametricDarks", "ParametricLights", "ParametricHighlights", "ParametricShadowSplit", "ParametricMidtoneSplit", "ParametricHighlightSplit"]; var nonRawProperties = ["IncrementalTemperature", "IncrementalTint"]; var Properties2012 = ["Exposure2012", "Contrast2012", "Highlights2012", "Shadows2012", "Whites2012", "Blacks2012", "Clarity2012"]; var Properties2010 = ["Exposure", "FillLight", "HighlightRecovery", "Brightness", "Contrast", "Clarity", "Shadows"]; var gradientCorrectionsTag = "GradientBasedCorrections"; var correctionsProperties = ["crs:CorrectionAmount", "crs:LocalExposure", "crs:LocalSaturation", "crs:LocalContrast", "crs:LocalClarity", "crs:LocalSharpness","crs:LocalBrightness","crs:LocalToningHue","crs:LocalToningSaturation","crs:LocalExposure2012","crs:LocalContrast2012","crs:LocalHighlights2012","crs:LocalShadows2012","crs:LocalClarity2012","crs:LocalLuminanceNoise","crs:LocalMoire","crs:LocalDefringe","crs:LocalTemperature","crs:LocalTint"]; var correctionMasksTag = "crs:CorrectionMasks"; var gradientMasksProperties = ["crs:MaskValue","crs:ZeroX","crs:ZeroY","crs:FullX","crs:FullY"]; var radialCorrectionsTag = "CircularGradientBasedCorrections"; var radialMasksProperties = ["crs:MaskValue","crs:Top","crs:Left","crs:Bottom","crs:Right","crs:Angle","crs:Midpoint","crs:Roundness","crs:Feather"]; /******************************************************************************/ function runRamp() { var rampDialog = new Window("dialog { orientation: 'row', text: 'Ramp ACR Settings', alignChildren:'top', \ leftGroup: Group { orientation: 'column', alignChildren:'fill', \ rampPanel: Panel { text: 'Ramp', \ propertyBox: DropDownList { }, \ startGroup: Group { orientation: 'row', \ startLabel: StaticText { text: 'Start: ' }, \ startText: EditText { characters: 8, text: '0' }, \ }, \ endGroup: Group{ \ endLabel: StaticText { text: 'End: ' }, \ endText: EditText { characters: 8, text: '0' }, \ }, \ checkboxesGroup: Group{ orientation: 'row', \ additiveCheckBox: Checkbox { text: 'Additive' }, \ pv2010CheckBox: Checkbox { text: 'Use PV2010' } \ }, \ selectionLabel: StaticText { text: 'Selected Items: ' }, \ } \ }, \ rightGroup: Group { orientation: 'column', alignChildren:'fill', \ okButton: Button { text: 'OK' }, \ cancelButton: Button { text: 'Cancel' } \ } \ } "); var okButton = rampDialog.rightGroup.okButton; var cancelButton = rampDialog.rightGroup.cancelButton; var propertyBox = rampDialog.leftGroup.rampPanel.propertyBox; var pv2010CheckBox=rampDialog.leftGroup.rampPanel.checkboxesGroup.pv2010CheckBox; var allProperties = Properties2012.concat(commonProperties); fillProperties(propertyBox, allProperties); propertyBox.selection = 0; rampDialog.leftGroup.rampPanel.selectionLabel.text += app.document.selections.length + " "; var startText = rampDialog.leftGroup.rampPanel.startGroup.startText; var endText = rampDialog.leftGroup.rampPanel.endGroup.endText pv2010CheckBox.onClick=function () { propertyBox.removeAll(); if (pv2010CheckBox.value == true) { allProperties = Properties2010.concat(commonProperties); } else { allProperties = Properties2012.concat(commonProperties); } //debugPrint("Meeeept: " + allProperties.length); fillProperties(propertyBox, allProperties); propertyBox.selection = 2; } okButton.onClick = function() { rampDialog.close(true); }; cancelButton.onClick = function() { rampDialog.close(false);}; propertyBox.onChange = function() { startText.text = getProperty(propertyBox.selection.text, 0); endText.text = getProperty(propertyBox.selection.text, app.document.selections.length - 1); } propertyBox.onChange(); if(rampDialog.show()) { //debugPrint("MOOOOOO: " + propertyBox.selection.text); applyRamp( propertyBox.selection.text, Number(startText.text), Number(endText.text), rampDialog.leftGroup.rampPanel.checkboxesGroup.additiveCheckBox.value); } } function fillProperties(dropDownToBeFilled, list) { for(var i = 0; i < list.length; i++) { dropDownToBeFilled.add("Item", list[i]); } //return true; } function getProperty(property, index) { var thumb = app.document.selections[index]; var xmp = new XMPMeta(); if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); return Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, property)); } return 0; } function applyRamp(property, startValue, endValue, additive) { saveUndoData(Array(property), "Ramp"); var count = app.document.selections.length; for(var i = 0; i < count; i++) { var thumb = app.document.selections[i]; var xmp = new XMPMeta(); var offset = 0; if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); if(additive) { offset = Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, property)); debugPrint(thumb.name + " offset: " + offset); } } var value = (i / (count - 1)) * (endValue - startValue) + startValue + offset; xmp.setProperty(XMPConst.NS_CAMERA_RAW, property, value); // Write the packet back to the selected file var updatedPacket = xmp.serialize(XMPConst.SERIALIZE_USE_COMPACT_FORMAT); // debugPrint(updatedPacket); thumb.metadata = new Metadata(updatedPacket); } } /******************************************************************************/ var enabledSettings = null; function runRampMultiple() { var rampDialog = new Window("dialog { orientation: 'row', text: 'Ramp ACR Settings', alignChildren:'top', \ leftGroup: Group { orientation: 'column', alignChildren:'left', \ settingsPanel: Panel { orientation: 'row', text: 'Settings', \ group1: Group { orientation: 'column', alignChildren:'left', \ gbcCheckbox: Checkbox { text: 'GradientBasedCorrections' }, \ rbcCheckbox: Checkbox { text: 'CircularGradientBasedCorrections' } \ }, \ group2: Group { orientation: 'column', alignChildren:'left' }, \ group3: Group { orientation: 'column', alignChildren:'left' } \ } \ }, \ rightGroup: Group { orientation: 'column', alignChildren:'fill', \ okButton: Button { text: 'OK' }, \ cancelButton: Button { text: 'Cancel' }, \ line1: Panel { height: 3 }, \ allButton: Button { text: 'Check All' }, \ noneButton: Button { text: 'Check None' }, \ } \ } "); var rawProperties = Properties2012.concat(commonProperties); var allProperties = rawProperties.concat(nonRawProperties); var gbcCheckbox = rampDialog.leftGroup.settingsPanel.group1.gbcCheckbox; var rbcCheckbox = rampDialog.leftGroup.settingsPanel.group1.rbcCheckbox; var settingsPanel = rampDialog.leftGroup.settingsPanel; var okButton = rampDialog.rightGroup.okButton; var cancelButton = rampDialog.rightGroup.cancelButton; var allButton = rampDialog.rightGroup.allButton; var noneButton = rampDialog.rightGroup.noneButton; var checkboxes = new Array(allProperties.length); if(enabledSettings == null) { enabledSettings = new Array(); for(var i = 0; i < rawProperties.length; i++) { enabledSettings.push(rawProperties[i]); } } gbcCheckbox.value = rampGradientCorrections; gbcCheckbox.onClick = function() { rampGradientCorrections = this.value; }; rbcCheckbox.value = rampRadialCorrections; rbcCheckbox.onClick = function() { rampRadialCorrections = this.value; }; for(var i = 0; i < allProperties.length; i++) { var checkbox = null; if((i + 2) < (allProperties.length + 2) / 3) { checkbox = settingsPanel.group1.add("checkbox", undefined, allProperties[i]); } else if((i + 2) < (allProperties.length + 2) * 2 / 3) { checkbox = settingsPanel.group2.add("checkbox", undefined, allProperties[i]); } else { checkbox = settingsPanel.group3.add("checkbox", undefined, allProperties[i]); } checkboxes[i] = checkbox; checkbox.value = indexOf(enabledSettings, allProperties[i]) >= 0; checkbox.onClick = function() { if(this.value) { if(indexOf(enabledSettings, this.text) == -1) enabledSettings.push(this.text); } else { remove(enabledSettings, this.text); } }; } allButton.onClick = function() { for(var i = 0; i < allProperties.length; i++) { checkboxes[i].value = true; checkboxes[i].onClick(); } gbcCheckbox.value = true; rbcCheckbox.value = true; } noneButton.onClick = function() { for(var i = 0; i < allProperties.length; i++) { checkboxes[i].value = false; checkboxes[i].onClick(); } gbcCheckbox.value = false; rbcCheckbox.value = false; } okButton.onClick = function() { rampDialog.close(true); }; cancelButton.onClick = function() { rampDialog.close(false);}; if(rampDialog.show()) { rampMultiple(enabledSettings); } } function indexOf(array, item) { for(var i = 0; i < array.length; i++) { if(array[i] == item) return i; } return -1; } function remove(array, item) { var index = indexOf(array, item); if(index >= 0) { array.splice(index, 1); } } function rampMultiple(enabledSettings) { saveUndoData(enabledSettings, "Ramp Multiple"); var count = app.document.selections.length; var currentKeyframe = 0; var nextKeyframe = 1; var settings = new Array(); var correctionsCount = 0; var masksCount = new Array(); for(var i = 0; i < enabledSettings.length; i++) settings.push(enabledSettings[i]); if(rampGradientCorrections || rampRadialCorrections) { //generate all the property paths we need for gradient corrections var thumb = app.document.selections[0]; var xmp = new XMPMeta(); if(thumb.hasMetadata) { var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); var getCorrectionSettings = function(tag, maskProperties) { correctionsCount = xmp.countArrayItems(XMPConst.NS_CAMERA_RAW, tag); for(var j = 1; j<= correctionsCount; j++) { for(var i = 0; i < correctionsProperties.length; i++) { settings.push(tag + "[" + j + "]/" + correctionsProperties[i]); } masksCount.push(xmp.countArrayItems(XMPConst.NS_CAMERA_RAW, tag + "[" + j + "]/" + correctionMasksTag)); for(var k = 1; k<= masksCount[j-1]; k++) { for(var i = 0; i < maskProperties.length; i++) { settings.push(tag + "[" + j + "]/" + correctionMasksTag + "[" + k + "]/" + maskProperties[i]); } } } }; if(rampGradientCorrections) getCorrectionSettings(gradientCorrectionsTag, gradientMasksProperties); if(rampRadialCorrections) getCorrectionSettings(radialCorrectionsTag, radialMasksProperties); } } var targetStart = readSettings(currentKeyframe, settings); for(nextKeyframe = 1; nextKeyframe < count - 1; nextKeyframe++) { if(app.document.selections[nextKeyframe].rating == keyframeRating) break; } var targetEnd = readSettings(nextKeyframe, settings); for(var i = 1; i < count - 1; i++) { var thumb = app.document.selections[i]; if(thumb.rating == keyframeRating) { currentKeyframe = nextKeyframe; targetStart = targetEnd; nextKeyframe = i + 1; for(nextKeyframe = i + 1; nextKeyframe < count - 1; nextKeyframe++) { if(app.document.selections[nextKeyframe].rating == keyframeRating) break; } targetEnd = readSettings(nextKeyframe, settings); } else { if(targetStart == null || targetEnd == null) break; var xmp = new XMPMeta(); if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); } for(var j = 0; j < settings.length; j ++) { var value = (i - currentKeyframe) / (nextKeyframe - currentKeyframe) * (targetEnd[j] - targetStart[j]) + targetStart[j]; xmp.setProperty(XMPConst.NS_CAMERA_RAW, settings[j], value); } // Write the packet back to the selected file var updatedPacket = xmp.serialize(XMPConst.SERIALIZE_USE_COMPACT_FORMAT); // debugPrint(updatedPacket); thumb.metadata = new Metadata(updatedPacket); } } } function readSettings(keyframe, settings) { var result = new Array(settings.length); var thumb = app.document.selections[keyframe]; var xmp = new XMPMeta(); if(thumb.hasMetadata) { try { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); for(var j = 0; j < settings.length; j ++) result[j] = Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, settings[j])); } catch(err) { alert("Error Loading Metadata for keyframe: "+ thumb.name); return null; } } else { alert("Error Loading Metadata for keyframe: "+ thumb.name); return null; } return result; } /******************************************************************************/ function runDeflickerMain() { var deflickerDialog = new Window("dialog { orientation: 'row', text: 'Deflicker', alignChildren:'top', \ leftGroup: Group { orientation: 'column', alignChildren:'fill', \ deflickerPanel: Panel { text: 'Deflicker', alignChildren:'left', \ percentileGroup: Group{ \ percentileLabel: StaticText { text: 'Percentile: ' }, \ percentileSlider: Slider { minvalue: 0, maxvalue: 100, value: 50 }, \ percentileText: EditText { characters: 5, text: '0.5', helpTip: 'Choose a value between 0.0 and 1.0. Use preview to ensure chosen percentile crosses the sky.'}, \ }, \ previewSizeGroup: Group{ \ previewSizeLabel: StaticText { text: 'Analysis Size: ' }, \ previewSizeText: EditText { characters: 5, text: '200', helpTip: 'Resizes the image to this many pixels on the longest side before analysis (smaller = faster/less acurate)' }, \ }, \ cropLabel: StaticText { text: 'Analysis Crop (%): ' }, \ cropGroup: Group{ \ cropXLabel: StaticText { text: 'X: ' }, \ cropXText: EditText { characters: 3, text: '0' }, \ cropYLabel: StaticText { text: 'Y: ' }, \ cropYText: EditText { characters: 3, text: '0' }, \ cropWidthLabel: StaticText { text: 'Width: ' }, \ cropWidthText: EditText { characters: 3, text: '100' }, \ cropHeightLabel: StaticText { text: 'Height: ' }, \ cropHeightText: EditText { characters: 3, text: '100' }, \ }, \ iterationsGroup: Group{ \ iterationsLabel: StaticText { text: 'Max Iterations: ' }, \ iterationsText: EditText { characters: 3, text: '1', helpTip: 'Maximum number of deflicker passes to run' }, \ }, \ checkboxesGroup: Group { orientation: 'column', alignChildren: 'left', \ pv2010Checkbox: Checkbox { text: 'Use PV2010' }, \ saveDataCheckbox: Checkbox { text: 'Save data in external file' } \ }, \ selectionLabel: StaticText { text: 'Selected Items: ???????' }, \ percentileLabel: StaticText { text: 'Percentile Level: ???????' }, \ } \ }, \ rightGroup: Group { orientation: 'column', alignChildren:'fill', \ okButton: Button { text: 'OK' }, \ cancelButton: Button { text: 'Cancel' }, \ line1: Panel { height: 3 }, \ previewButton: Button { text: 'Preview .', helpTip: 'Shows the image with the percentile level highlighted'} \ } \ } "); var previewSizeText = deflickerDialog.leftGroup.deflickerPanel.previewSizeGroup.previewSizeText; var cropXText = deflickerDialog.leftGroup.deflickerPanel.cropGroup.cropXText; var cropYText = deflickerDialog.leftGroup.deflickerPanel.cropGroup.cropYText; var cropWidthText = deflickerDialog.leftGroup.deflickerPanel.cropGroup.cropWidthText; var cropHeightText = deflickerDialog.leftGroup.deflickerPanel.cropGroup.cropHeightText; var percentileText = deflickerDialog.leftGroup.deflickerPanel.percentileGroup.percentileText; var okButton = deflickerDialog.rightGroup.okButton; var cancelButton = deflickerDialog.rightGroup.cancelButton; var previewButton = deflickerDialog.rightGroup.previewButton; var percentileSlider = deflickerDialog.leftGroup.deflickerPanel.percentileGroup.percentileSlider; var percentileLabel = deflickerDialog.leftGroup.deflickerPanel.percentileLabel; var iterationsText = deflickerDialog.leftGroup.deflickerPanel.iterationsGroup.iterationsText; var pv2010Checkbox=deflickerDialog.leftGroup.deflickerPanel.checkboxesGroup.pv2010Checkbox; var saveDataCheckbox = deflickerDialog.leftGroup.deflickerPanel.checkboxesGroup.saveDataCheckbox; //debugPrint("Moooo: " + saveDataCheckbox); //debugPrint("Moooo: " + okButton); previewHistogram = null; deflickerDialog.leftGroup.deflickerPanel.selectionLabel.text = "Selected Items: " + app.document.selections.length; percentileText.text = percentile; percentileSlider.value = percentile * 100; iterationsText.text = iterations; previewSizeText.text = previewSize; cropXText.text = cropX; cropYText.text = cropY; cropWidthText.text = cropWidth; cropHeightText.text = cropHeight; pv2010Checkbox.onClick = function() { usePV2010 = this.value; } saveDataCheckbox.onClick = function() { saveData = this.value; } iterationsText.onChange = function() { iterations = Number(this.text); }; previewSizeText.onChange = function() { previewSize = Number(this.text); }; cropXText.onChange = function() { cropX = Math.max(this.text, 0); }; cropYText.onChange = function() { cropY = Math.max(this.text, 0); }; cropWidthText.onChange = function() { cropWidth = Math.min(this.text, 100); }; cropHeightText.onChange = function() { cropHeight = Math.min(this.text, 100); }; percentileText.onChange = function() { percentile = Math.min(0.99, Math.max(0.01, Number(percentileText.text))); percentileSlider.value = percentile * 100; }; percentileSlider.onChange = function() { percentile = Math.min(0.99, Math.max(0.01, Number(percentileSlider.value / 100))); percentileText.text = percentile; }; var updateAll = function() { iterationsText.onChange(); percentileText.onChange(); previewSizeText.onChange(); cropXText.onChange(); cropYText.onChange(); cropWidthText.onChange(); cropHeightText.onChange(); pv2010Checkbox.onClick(); saveDataCheckbox.onClick(); } okButton.onClick = function() { updateAll(); deflickerDialog.close(true); }; cancelButton.onClick = function() { deflickerDialog.close(false);}; previewButton.onClick = function() { updateAll(); percentileLabel.text = "Percentile Level: " + showPercentilePreview(); }; if(deflickerDialog.show()) { deflicker(); } } function showPercentilePreview() { //get target values from the first image var thumb = app.document.selections[0]; var bitmap = getPreview(thumb, previewSize);; var level = computePercentile(bitmap, percentile); var output = bitmap.clone(); var xmin = Math.round(Math.max(cropX * output.width / 100, 0)); var ymin = Math.round(Math.max(cropY * output.height / 100, 0)); var xmax = Math.min(cropX + cropWidth, 100) * output.width / 100; var ymax = Math.min(cropY + cropHeight, 100) * output.height / 100; for(var x = xmin; x < xmax; x+=2) { for(var y = ymin; y < ymax; y+=2) { if(x == xmin || Math.abs(x - xmax) <= 2) { output.setPixel(x, y, y % 8 < 4 ? "#000000" : "#ffffff"); output.setPixel(x, y + 1, y % 8 < 4 ? "#000000" : "#ffffff"); } else if(y == ymin || Math.abs(y - ymax) <= 2) { output.setPixel(x, y, x % 8 < 4 ? "#000000" : "#ffffff"); output.setPixel(x + 1, y, x % 8 < 4 ?"#000000" : "#ffffff"); } else { var pixel = new Color(output.getPixel(x,y)); var lum = Math.round((pixel.red + pixel.green + pixel.blue)/3); if(lum >= level - 4 && lum <= level + 4) output.setPixel(x, y, "#ff0000"); } } } var tempFilename = Folder.temp + "/PercentilePreview.jpg"; var tempFile = File(tempFilename); debugPrint("temp file path: " + tempFile.fsName); if(tempFile.exists) { tempFile.remove(); } output.exportTo(tempFile, 100); File(tempFilename).execute(); return level; } function convertToEV(value) { return evCurveCoefficent * Math.log(value); } function computePercentile(bitmap, percentile) { var histogram = new Array(256); var total = 0; for(var h = 0; h < 256; h++) histogram[h] = 0; var xmin = Math.round(Math.max(cropX * bitmap.width / 100, 0)); var ymin = Math.round(Math.max(cropY * bitmap.height / 100, 0)); var xmax = Math.min(cropX + cropWidth, 100) * bitmap.width / 100; var ymax = Math.min(cropY + cropHeight, 100) * bitmap.height / 100; for(var x = xmin; x < xmax; x++) { for(var y = ymin; y < ymax; y++) { var pixel = new Color(bitmap.getPixel(x,y)); histogram[Math.round((pixel.red + pixel.green + pixel.blue)/3)]++; total++; } } var runningTotal = 0; var level = 0; for(level = 0; level < 256; level++) { runningTotal += histogram[level]; if(runningTotal / total >= percentile) return level; } return 255; } function getPreview(thumb, size) { if(thumb.core.preview.preview == null) { debugPrint("\npreview not ready, waiting..."); var timeout = 30;//30 seconds while(thumb.core.preview.preview == null) { $.sleep(1000); timeout--; if(timeout < 0) break; } } if(thumb.core.preview.preview != null) return thumb.core.preview.preview.resize(size); else throw "Error: preview data not available for thumbnail"; } function deflicker() { initializeProgress(); if (usePV2010 == true) { var exposureEnXMP = 'Exposure'; } else { var exposureEnXMP = 'Exposure2012'; } saveUndoData(Array(exposureEnXMP), "Deflicker"); var count = app.document.selections.length; var moreIterationsNeeded = false; var currentKeyframe = 0; var nextKeyframe = 0; app.synchronousMode = true; var items = new Array(count); var originalData=new Array(count); var dataToBeSaved=new Array(count); for(var i = 0; i < count; i++) items[i] = app.document.selections[i]; for(var iteration = 0; iteration < iterations; iteration++) { initializeProgress("Deflicker Progress" + (iterations > 1 ? " (Iteration " + (iteration + 1) + ")" : "")); debugPrint("\n*** Iteration " + (iteration + 1) + " ***"); //get target values from the first image if(iteration > 0) { statusText.text = "Purge Cache"; for(var i = 0; i < count; i++) { app.purgeFolderCache(items[i]); app.document.select(items[i]); } } else { for (var i=0; i < count; i++) { var xmp = new XMPMeta(); var thumb = items[i]; if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; var xmp = new XMPMeta(md.serialize()); originalData[i] = Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, exposureEnXMP)); //debugPrint("Mooooo: " + originalData[i]); } } } currentKeyframe = 0; nextKeyframe = 0; var thumb = items[0]; progress.value = 100 * 1 / (count + 1); statusText.text = "Processing " + thumb.name + " (keyframe)"; var bitmap = getPreview(thumb, previewSize); var targetStart = computePercentile(bitmap, percentile); var targetEnd = targetStart; debugPrint("keyframe " + thumb.name + ": " + targetStart); var findNextKeyframe = function(index) { nextKeyframe = index + 1; for(nextKeyframe = index + 1; nextKeyframe < count - 1; nextKeyframe++) { if(items[nextKeyframe].rating == keyframeRating) break; } //get target values from the last image var thumb = items[nextKeyframe]; progress.value = 100 * (index + 2) / (count + 1); statusText.text = "Processing " + thumb.name + " (keyframe)"; var bitmap = getPreview(thumb, previewSize); var result = computePercentile(bitmap, percentile); debugPrint("keyframe " + thumb.name + ": " + result); return result; } targetEnd = findNextKeyframe(0); moreIterationsNeeded = false; for(var i = 1; i < count - 1; i++) { thumb = items[i]; if(thumb.rating == keyframeRating) { targetStart = targetEnd; currentKeyframe = nextKeyframe; targetEnd = findNextKeyframe(i); } else { progress.value = 100 * (i + 2) / (count + 1); statusText.text = "Processing " + thumb.name; bitmap = getPreview(thumb, previewSize); computed = computePercentile(bitmap, percentile); var xmp = new XMPMeta(); var offset = 0; if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; var xmp = new XMPMeta(md.serialize()); offset = Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, exposureEnXMP)); } var target = ((i - currentKeyframe) / (nextKeyframe - currentKeyframe)) * (targetEnd - targetStart) + targetStart; var ev = convertToEV(target) - convertToEV(computed) + offset; if(Math.abs(target - computed) > deflickerThreshold) moreIterationsNeeded = true; debugPrint(thumb.name + ": " + ev + "ev (" + target + " - " + computed + ")"); xmp.setProperty(XMPConst.NS_CAMERA_RAW, exposureEnXMP, ev) dataToBeSaved[i]=ev; // Write the packet back to the selected file var updatedPacket = xmp.serialize(XMPConst.SERIALIZE_USE_COMPACT_FORMAT); // debugPrint(updatedPacket); thumb.metadata = new Metadata(updatedPacket); } } if(!moreIterationsNeeded) break; } for(var i = 0; i < count; i++) { app.purgeFolderCache(items[i]); app.document.select(items[i]); } if (saveData == true) { var allThumbs=selectAllThumbsInFolder(); var seqOffset=findSeqOffset(allThumbs, items[0]); for(var i = 0; i < count; i++) { app.document.select(items[i]); dataToBeSaved[i] = dataToBeSaved[i] - originalData[i]; } dataToBeSaved[0]=0; dataToBeSaved[count - 1]=0; var json = { "offset": seqOffset, "evValues" : dataToBeSaved }; //debugPrint("Mooooo: " + (app.document.presentationPath + "/deFlickerdata.json")); //debugPrint("Datos: " + json.toSource()); var jsonFile=new File(app.document.presentationPath + "/deFlickerdata.json"); jsonFile.open("w"); jsonFile.write(json.toSource()); jsonFile.close(); } if(moreIterationsNeeded) alert("More Deflicker Iterations may be needed"); progressWindow.hide(); } function findSeqOffset(allThumbs, selectionStart) { var selectionStartName=selectionStart.name; for (i=0; i < allThumbs.length; i++) { if (allThumbs[i].name == selectionStartName) { return i; } } //Something weird happened here... The selected thumb isn't in the current folder!!! This should not happen return false; } function selectAllThumbsInFolder() { var count = app.document.selections.length; var tempThumbs=Array(); for (i=0; i < count; i++) { tempThumbs[i]=app.document.selections[i]; } app.document.selectAll(); var allThumbs=app.document.getSelection("cr2"); app.document.deselectAll(); for (i=0; i < count; i++) { app.document.select(tempThumbs[i]); } return allThumbs; } function initializeProgress(title) { progressWindow = new Window("palette { text:'Deflicker Progress', \ statusText: StaticText { text: 'Processing Images...', preferredSize: [350,20] }, \ progressGroup: Group { \ progress: Progressbar { minvalue: 0, maxvalue: 100, value: 0, preferredSize: [300,20] }, \ cancelButton: Button { text: 'Cancel' } \ } \ }"); progressWindow.text = title; statusText = progressWindow.statusText; progress = progressWindow.progressGroup.progress; progressWindow.progressGroup.cancelButton.onClick = function() { userCanceled = true; } progressWindow.show(); } function runBackupXMP() { var backupLocation = Folder.selectDialog ("Select a destination folder for the backup"); if(backupLocation != null) { var count = app.document.selections.length; for(var i = 0; i < count; i++) { var thumb = app.document.selections[i]; var sourceXMP = File(thumb.spec.fullName.substr(0, thumb.spec.fullName.lastIndexOf(".")) + ".XMP"); if(!sourceXMP.exists) sourceXMP = File(thumb.spec.fullName.substr(0, thumb.spec.fullName.lastIndexOf(".")) + ".xmp"); if(sourceXMP.exists) { var destination = File(backupLocation.fullName + "/" + sourceXMP.name); if(destination.exists) { alert("Error: file(s) already exist in this destination"); break; } sourceXMP.copy(destination); } } } } function runUndo() { var jsonFile=new File(app.document.presentationPath + "/undoData.json"); if (jsonFile.exists) { jsonFile.open("r"); var allData=eval(jsonFile.read()); jsonFile.close(); } else { return false; } if (allData.length == 0) { return false; } var undoDialog = new Window("dialog { orientation: 'row', text: 'Undo Ramp/Deflicker', alignChildren:'top', \ leftGroup: Group { orientation: 'column', alignChildren:'fill', \ undoPanel: Panel { text: 'Previous actions', \ propertyBox: DropDownList { }, \ } \ }, \ rightGroup: Group { orientation: 'column', alignChildren:'fill', \ okButton: Button { text: 'OK' }, \ cancelButton: Button { text: 'Cancel' } \ } \ } "); var okButton = undoDialog.rightGroup.okButton; var cancelButton = undoDialog.rightGroup.cancelButton; var propertyBox = undoDialog.leftGroup.undoPanel.propertyBox; var count=allData.length; for (i=0; i < count; i++) { var item=allData[i]; propertyBox.add("Item", "Undo " + item["descripcion"]); } propertyBox.selection = count - 1; okButton.onClick = function() { undoDialog.close(true); }; cancelButton.onClick = function() { undoDialog.close(false);}; if(undoDialog.show()) { //debugPrint("MOOOOOO: " + propertyBox.selection.text); Undo(Number(propertyBox.selection)); } } function saveUndoData(propertiesToBeChanged, action) { //Actually we don't used "propertiesToBeChanged" anymore, since we save all the data in the XMPs. //Right now, we only use it to generate the descriptive text for each undo set. var allThumbs=selectAllThumbsInFolder(); var seqOffset=findSeqOffset(allThumbs, app.document.selections[0]); var allProperties = commonProperties.concat(Properties2010, Properties2012); var numAllProperties=allProperties.length; var numPropertiesToBeChanged=propertiesToBeChanged.length; if (numPropertiesToBeChanged > 3) { numPropertiesToBeChanged = 4; propertiesToBeChanged[3] = "etc."; } var count = app.document.selections.length; var undoObject={ "offset": seqOffset, "descripcion": action + " (" } for (i=0; i < numPropertiesToBeChanged; i++) { undoObject["descripcion"] += (propertiesToBeChanged[i] + " "); } undoObject["descripcion"] += ")"; for (i=0; i < numAllProperties; i++) { undoObject[allProperties[i]] = Array(); } for (i=0; i < count; i++) { var thumb=app.document.selections[i]; var xmp = new XMPMeta(); if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); for (j=0; j < numAllProperties; j++) { var p = allProperties[j]; //debugPrint("MOOOOOO: " + p); undoObject[p][i] = Number(xmp.getProperty(XMPConst.NS_CAMERA_RAW, allProperties[j])); } //debugPrint("Meeeept: " + datos[j]); } } saveToDiskUndo(undoObject); } function saveToDiskUndo(objeto) { var jsonFile=new File(app.document.presentationPath + "/undoData.json"); if (jsonFile.exists) { jsonFile.open("r"); var allData=eval(jsonFile.read()); jsonFile.close(); } else { var allData=Array(); } allData.push(objeto); if (allData.length > undoLevels) { allData.shift(); } jsonFile.open("w"); jsonFile.write(allData.toSource()); jsonFile.close(); } function Undo(num) { var jsonFile=new File(app.document.presentationPath + "/undoData.json"); if (jsonFile.exists) { jsonFile.open("r"); var allData=eval(jsonFile.read()); jsonFile.close(); } else { return false; } var dataToBeRestored=allData[num]; var offset=dataToBeRestored["offset"]; var allThumbs=selectAllThumbsInFolder(); var count=allThumbs.length; //We leave in this object only the data to be restored, which will make it easier to loop through it later delete dataToBeRestored["offset"]; delete dataToBeRestored["descripcion"]; for (i = offset; i < count; i++) { var thumb=allThumbs[i]; var xmp = new XMPMeta(); if(thumb.hasMetadata) { //load the xmp metadata var md = thumb.synchronousMetadata; xmp = new XMPMeta(md.serialize()); } for (var parameter in dataToBeRestored) { var value=dataToBeRestored[parameter][i - offset]; xmp.setProperty(XMPConst.NS_CAMERA_RAW, parameter, value); } // Write the packet back to the selected file var updatedPacket = xmp.serialize(XMPConst.SERIALIZE_USE_COMPACT_FORMAT); // debugPrint(updatedPacket); thumb.metadata = new Metadata(updatedPacket); } for (var i = offset; i < count; i++) { app.purgeFolderCache(allThumbs[i]); app.document.select(allThumbs[i]); } //Deletes already used undo data var newData=Array(); var numCurrentData=allData.length; //Not a bug!!! We only loop until num, which is the current undo level. for (i=0; i < num; i++) { newData[i]=allData[i]; } jsonFile.open("w"); jsonFile.write(newData.toSource()); jsonFile.close(); } new BrRamp().run();