// Add Exif Caption.jsx // Version 1.0 // Shaun Ivory (shaun@ivory.org) // // Feel free to modify this script. If you do anything interesting with it, // please let me know. //************************************************************************ // // Caption constants. Change these if you want to. // //************************************************************************ // Copyright string c_CaptionCopyright = "Copyright © Shaun Ivory"; c_CaptionFont = "TrebuchetMS"; // Caption text c_CaptionGpsPrefix = "Location:" c_CaptionDateTimeTaken = "Date taken:" c_CaptionModel = "Camera:" c_CaptionExposureTime = "Exposure time:" c_CaptionAperture = "Aperture:" c_CaptionFocalLength = "Focal length:" c_CaptionIsoRating = "ISO:" //************************************************************************ // // Date and time constants. Change these if you want to. // //************************************************************************ c_MonthsArray = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; c_AM = "am" c_PM = "pm" //************************************************************************ // // Colors // //************************************************************************ function CreateSolidRgbColor(r, g, b) { var result = new SolidColor; result.rgb.red = r; result.rgb.green = g; result.rgb.blue = b; return result; } var colorCaptionBackground = CreateSolidRgbColor(255, 255, 255); var colorFrame = CreateSolidRgbColor(0, 0, 0); var colorText = CreateSolidRgbColor(0, 0, 0); //************************************************************************ // // Include EXIF constants and methods. // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Note the this is not a comment: it includes the contents // of Shaun's Exif Helpers.inc in this script! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // //************************************************************************ //@include "Shaun's Exif Helpers.inc" //************************************************************************ // // Functions // //************************************************************************ // // Photoshop CS formats the latitude and longitude strings // oddly: // // Example: 47.00 38.00' 33.60" // // There is no degrees symbol after the degrees, and // they leave useless zeroes on the right side of the decimal // point for both degrees and minutes. This function parses // the numbers from the latitude and longitude strings, and // inserts the correct characters where they belong: // // Example: 47° 38' 33.6" // // It returns an empty string if the string is in an unexpected // form or doesn't exist. // function CorrectlyFormatLatitudeOrLongitude(strLatLong) { var strResult = new String(""); // Copy the input string var strSource = new String(strLatLong); // Find the first space nIndex = strSource.indexOf(" "); if (nIndex >= 0) { // Copy up to the first space strDegrees = strSource.substr(0, nIndex); // Skip this part, plus the space strSource = strSource.substr(nIndex + 1); // Find the tick mark nIndex = strSource.indexOf("'"); if (nIndex >= 0) { // Copy up to the tick mark strMinutes = strSource.substr(0, nIndex); // Skip this chunk, plus the tick and space strSource = strSource.substr(nIndex + 2); // Find the seconds mark: " nIndex = strSource.indexOf("\""); if (nIndex >= 0) { // Copy up to the seconds strSeconds = strSource.substr(0, nIndex); // Get rid of extraneous trailing zeroes var nDegrees = parseFloat(strDegrees); var nMinutes = parseFloat(strMinutes); var nSeconds = parseFloat(strSeconds); // Use the correct symbols strResult = nDegrees.toString() + "° " + nMinutes.toString() + "' " + nSeconds.toString() + "\""; } } } return strResult; } // // This function gets the GPS location fields and formats them correctly // // It returns an empty string if they don't exist // function GetFormattedGpsData() { var strResult = new String(""); // Get the fields strLatitude = GetRawExifValueIfPresent(c_ExifGpsLatitude); strLatitudeRef = GetRawExifValueIfPresent(c_ExifGpsLatitudeRef); strLongitude = GetRawExifValueIfPresent(c_ExifGpsLongitude); strLongitudeRef = GetRawExifValueIfPresent(c_ExifGpsLongitudeRef); // Do all of them exist? if (strLatitude.length && strLatitudeRef.length && strLongitude.length && strLongitudeRef.length) { // Parse and reformat the latitude and longitude strFinalLatitude = CorrectlyFormatLatitudeOrLongitude(strLatitude); strFinalLongitude = CorrectlyFormatLatitudeOrLongitude(strLongitude); // Are they still valid? if (strFinalLatitude.length && strFinalLongitude.length) { // Create the result (with the constant prefix) strResult = c_CaptionGpsPrefix + " " + strFinalLatitude + " " + strLatitudeRef + ", " + strFinalLongitude + " " + strLongitudeRef; } } return strResult; } // // EXIF dates are formatted kind of funny, with colons // between the date tokens: // // Example: 2005:04:13 16:22:47 // // This function parses the numbers out of the string // and formats it like this: // // Example: Apr 13, 2005 4:22:47 pm // function CorrectlyFormatDateAndTime(strDateAndTime) { var strResult = new String(""); // Copy the input string var strSource = new String(strDateAndTime); // Find the first colon nIndex = strSource.indexOf(":"); if (nIndex >= 0) { // Copy up to the first space strYear = strSource.substr(0, nIndex); // Skip past the colon strSource = strSource.substr(nIndex + 1); // Find the next colon nIndex = strSource.indexOf(":"); if (nIndex >= 0) { // Copy up to colon strMonth = strSource.substr(0, nIndex); // Skip the colon strSource = strSource.substr(nIndex + 1); // Find the next space nIndex = strSource.indexOf(" "); if (nIndex >= 0) { // Copy up to the space strDay = strSource.substr(0, nIndex); // Skip the space strSource = strSource.substr(nIndex + 1); // Find the next colon nIndex = strSource.indexOf(":"); if (nIndex >= 0) { // Save the hours strHours = strSource.substr(0, nIndex); // Skip the colon strSource = strSource.substr(nIndex + 1); // Find the next colon nIndex = strSource.indexOf(":"); if (nIndex >= 0) { // Save the minutes strMinutes = strSource.substr(0, nIndex); // Skip the colon strSource = strSource.substr(nIndex + 1); // Save the seconds strSeconds = strSource; // Assume it is AM strAmPm = c_AM; // Is it after noon? if (strHours >= 12) { // Use PM strAmPm = c_PM; // If it is after 13:00, subtract 12 if (strHours >= 13) { strHours -= 12; } } // If it is 12:xx AM, make it 12:xx instead of 00:xx else if (strHours == 0) { strHours = 12; } // Format the string strResult = c_MonthsArray[parseInt(strMonth) - 1] + " " + parseInt(strDay).toString() + ", " + strYear + " " + parseInt(strHours).toString() + ":" + strMinutes + ":" + strSeconds + " " + strAmPm; } } } } } return strResult; } // // If the value is not an empty string, prepend // a caption and return the result // function GetPrefixedValue(strCaption, strValue) { var strResult = new String(""); if (strValue.length) { strResult = strCaption + " " + strValue; } return strResult; } // // Get a simple EXIF property that doesn't need reformatting // and prepend its title if the value is present // function GetPrefixedSimpleProperty(strCaption, strExifField) { return GetPrefixedValue(strCaption, GetRawExifValueIfPresent(strExifField)); } // // Format the date and time // function GetFormattedDateTimeTaken() { return GetPrefixedValue(c_CaptionDateTimeTaken, CorrectlyFormatDateAndTime(GetRawExifValueIfPresent(c_ExifDateTimeOriginal))); } // // Get the model name of the camera. // // Unfortunately, there is little consistency for EXIF makes and models. // For example, the Nikon Coolpix 990 looks like this: // // E990 // // While the D2x looks like this: // // NIKON D2X // // If your camera has a user-unfriendly name, you might want to modify // this function to return a user-friendly name. // function GetFormattedModel() { return GetPrefixedSimpleProperty(c_CaptionModel, c_ExifModel) } // // EXIF exposure time // function GetFormattedExposureTime() { return GetPrefixedSimpleProperty(c_CaptionExposureTime, c_ExifExposureTime); } // // EXIF f-Stop // function GetFormattedAperture() { return GetPrefixedSimpleProperty(c_CaptionAperture, c_ExifAperture); } // // EXIF focal length // function GetFormattedFocalLength() { return GetPrefixedSimpleProperty(c_CaptionFocalLength, c_ExifFocalLength); } // // EXIF ISO rating // function GetFormattedIsoRating() { return GetPrefixedSimpleProperty(c_CaptionIsoRating, c_ExifIsoSpeedRating); } // // If strValue is not empty, add it to the array // function AddAvailableExifFieldToArray(availableFieldsArray, strValue) { // If we have a string, add it to the end of the array if (strValue.length > 0) { availableFieldsArray[availableFieldsArray.length] = strValue; } } // // Extract the year from the EXIF date-taken // function GetYearFromDateTaken() { var strResult = new String(""); // Get the EXIF date taken var strSource = GetRawExifValueIfPresent(c_ExifDateTimeOriginal); if (strSource.length) { // Find the first colon nIndex = strSource.indexOf(":"); if (nIndex >= 0) { // Copy up to the first colon strResult = strSource.substr(0, nIndex); } } // If we don't have a string, use today's date if (strResult.length == 0) { strResult = ((new Date()).getYear() + 1900).toString(); } return strResult; } // // Format the copyright string // function GetFormattedCopyright() { // Start with the copyright string var strResult = new String(c_CaptionCopyright); // Get the year var strYear = GetYearFromDateTaken(); if (strYear.length != 0) { // Append it (after a comma and a space) strResult += ", " + strYear; } return strResult; } // // Get all of the interesting EXIF fields, format them, and put them in a comma // separated string for display. // function GetAllAvailableFields() { // Add all of the fields to the array var AvailableFields = new Array; AddAvailableExifFieldToArray(AvailableFields, GetFormattedModel()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedExposureTime()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedAperture()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedFocalLength()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedIsoRating()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedGpsData()); AddAvailableExifFieldToArray(AvailableFields, GetFormattedDateTimeTaken()); // Turn it into one big string var strResult = new String; for (nCurrentEntry = 0; nCurrentEntry < AvailableFields.length; ++nCurrentEntry) { if (nCurrentEntry != 0) { strResult += ", "; } strResult += AvailableFields[nCurrentEntry]; } return strResult; } //************************************************************************ // // Begin script // //************************************************************************ function Main() { if (app.documents.length > 0) { // Save the old background color and ruler units var oldRulerUnits = preferences.rulerUnits; var oldBackgroundColor = backgroundColor; try { // Turn off dialog boxes displayDialogs = DialogModes.NO; // Use inches for our scale preferences.rulerUnits = Units.INCHES; // Decide which axis is longer var nLongAxis = (activeDocument.height.value > activeDocument.width.value) ? activeDocument.height.value : activeDocument.width.value; // Calculate the border thickness var nBorderThickness = nLongAxis / 180; // How big do we want the font? var nFontHeight = activeDocument.width.value / 85; // Calculate the text area height var nTextAreaHeight = nFontHeight * 3; // Convert the font size to points var nFontHeightInPoints = nFontHeight * 72; // Add the frame backgroundColor = colorFrame; activeDocument.resizeCanvas(activeDocument.width.value + nBorderThickness*2, activeDocument.height.value + nBorderThickness*2, AnchorPosition.MIDDLECENTER); // Save the bottom of the image frame before we add the caption area var nBottomOfFrame = activeDocument.height.value; // Add the caption area backgroundColor = colorCaptionBackground; activeDocument.resizeCanvas(activeDocument.width.value, activeDocument.height.value + nTextAreaHeight, AnchorPosition.TOPCENTER); // Create the caption ("\u000D" is a new-line) var strCaption = GetFormattedCopyright() + "\u000D" + GetAllAvailableFields(); // Create the text layer var newTextLayer = activeDocument.artLayers.add(); newTextLayer.kind = LayerKind.TEXT; newTextLayer.textItem.font = c_CaptionFont; newTextLayer.textItem.position = [nBorderThickness, nBottomOfFrame + nFontHeight*0.8 + nBorderThickness]; newTextLayer.textItem.size = nFontHeightInPoints; newTextLayer.textItem.color = colorText; newTextLayer.textItem.contents = strCaption; } catch (e) { alert(e); } // Restore the background color and ruler units backgroundColor = oldBackgroundColor; preferences.rulerUnits = oldRulerUnits; } else { alert("You don't have an image opened. Please open an image before running this script."); } } Main();