Skip to content

Instantly share code, notes, and snippets.

@GetUpKidAK
Last active February 8, 2022 08:46
Show Gist options
  • Save GetUpKidAK/f3b4c36d7761264255d2 to your computer and use it in GitHub Desktop.
Save GetUpKidAK/f3b4c36d7761264255d2 to your computer and use it in GitHub Desktop.
Photoshop script to export textures from layered PSD
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Enables exporting of several PBR texture maps from a single PSD with a few clicks:
//
// 1. Select an export folder
// 2. Choose which PSD layer group corresponds to which map (split into separate RGB/Alpha channels)
// 3. Change the file export options (if required)
// 4. Hit export.
//
// Created by Ash Kendall. ash.kendall(at)gmail.com
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#target photoshop
app.bringToFront();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// DEFAULT OPTIONS
//
// autoSave (true/false) : Auto-save exported files when created (this will overwrite any existing file without a prompt)
// lowercaseFilename (true/false) : Make export filenames all lowercase (Base filename will match the document you are exporting from)
// removeFilenameSpaces (true/false) : Replace spaces in exported filename with underscores
// closeDocsOnSave (true/false) : Close auto-saved documents after being exported
// useCompression (true/false) : Use Targa's RLE compression when saving files
// exportPathDefault (string) : Use this as the default export path. If blank the current document path will be used (Save before you export!)
// Usage example: "C:/Users/~YourUserName~/Documents/" - Windows (NOTE THE FORWARD SLASHES)
// "/Users/~YourUserName~/Documents/" - OSX
// exportedFileExtension : Not supported, and will likely break something. DO NOT CHANGE. SERIOUSLY
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var options = {
autoSave: true,
lowercaseFilename: true,
removeFilenameSpaces: true,
useCompression: true,
closeDocsOnSave: true,
exportedFileExtension: ".tga" // Not supported, don't change!
};
// Map constructor. These are set up below...
var Map = function(mapName, defaultLayerName, defaultAlphaLayerName, filePostfix, alphaState)
{
this.mapName = mapName;
this.defaultLayerName = defaultLayerName;
this.defaultAlphaLayerName = defaultAlphaLayerName;
this.filePostfix = filePostfix;
this.exportMap = false;
this.exportRGB = false;
this.exportAlpha = false;
this.rgbLayerIndex = 0;
this.alphaLayerIndex = 0;
this.alphaState = alphaState;
this.UpdateMapInfo = function(selected, rgb, a)
{
this.exportMap = selected;
this.exportRGB = rgb > 0 ? true : false;
this.exportAlpha = a > 0 ? true : false;
this.rgbLayerIndex = rgb;
this.alphaLayerIndex = a;
};
this.ReadyToExport = function()
{
if (!this.exportRGB && !this.exportAlpha) // Selected for export but no layer groups selected
{
errorsLog += "- The " + this.mapName + " map is marked for export but no channels have been selected.\n\n";
return false;
}
if (!this.exportRGB && this.exportAlpha)
{
if (this.alphaState == alpha.EXCLUSIVE) return true;
errorsLog += "- You can't export an Alpha channel without an RGB channel on the " + this.mapName + " map.\n\n";
return false;
}
else if (this.exportRGB && (!this.exportAlpha && this.alphaState == alpha.REQUIRED))
{
errorsLog += "- The " + this.mapName + " map can't be exported without the required Alpha channel.\n\n";
return false;
}
return true;
}
}
var alpha = { AVAILABLE: 0, REQUIRED: 1, DISABLED: 2, EXCLUSIVE: 3 }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AVAILABLE MAPS CAN BE ADJUSTED BELOW
// Parameters are as follows:
//
// Map name: Display name for the map
// Default RGB Layer Name: Name to be checked against the Layer Groups when pre-filling the dropdown (for RGB channel)
// Default Alpha Layer Name: Name to be checked against the Layer Groups when pre-filling the dropdown (for Alpha channel)
// File postfix: Postfix to be added after the base filename when exported
// Alpha Channel status: Status of Alpha channel for map - AVAILABLE for use, REQUIRED for the map, DISABLED (Unused) for map,
// and EXCLUSIVE (Alpha-only)
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// As detailed above: |Map Name |RGB Layer Name |Alpha Layer Name |File post-fix |Alpha channel status
var maps = {};
maps.albedo = new Map ("Albedo", "albedo", "transparency", "_albedo", alpha.AVAILABLE);
maps.metallic = new Map ("Metallic", "metallic", "smoothness", "_metallic", alpha.REQUIRED);
maps.normal = new Map ("Normal", "normal", "", "_normal", alpha.DISABLED);
maps.height = new Map ("Height", "height", "", "_height", alpha.DISABLED);
maps.ao = new Map ("Occlusion", "ao", "", "_ao", alpha.DISABLED);
maps.emission = new Map ("Emission", "emission", "", "_emission", alpha.DISABLED);
maps.detailMask = new Map ("Detail Mask", "", "detail mask", "_detailMask", alpha.EXCLUSIVE);
// Document caches
var doc; // Active document
var docFilename; // Document filename
var docLayerSets; // Layer groups cache
// Global variables
var exportedFilename; // Filename for exported file
var exportedFilePath; // Path for exported file
// Settings file info
var settingsPath = new Folder(Folder.myDocuments + "/TextureExporter/"); // Settings path
var settingsFilename = "Settings.cfg"; // Settings filename
var settingsFile = new File(settingsPath.fsName + "/" + settingsFilename); // Full settings filepath
var errorsLog; // Error log
var mapsToExport = new Array(0); // Maps to export
function Main ()
{
// Check there is an open document, otherwise quit
if (app.documents.length == 0) { alert ("No active document."); return 1; }
// Document cache
doc = app.activeDocument;
// Quit if there are no layer groups in the document
if (doc.layerSets.length == 0) { alert ("There are no Layer Groups in the active document."); return 1; }
// Early setup (set up export path and ready error log)
exportedFilePath = GetExportPath();
errorsLog = "The following errors occurred: \n\n";
// Show dialog
var result = ShowDialog();
// Form is all ok
if (result == 0)
{
// Get the updated filename
exportedFilename = GetBaseFilename();
// Export all eligible maps
for (var i = 0; i < mapsToExport.length; i++)
{
ExportMaps(mapsToExport[i]);
}
alert("All textures exported.");
// Save settings
SaveSettingsFile();
}
else
{
// Quit
return 1;
}
// Quit
return 0;
}
// GET EXPORT PATH
function GetExportPath ()
{
// If settings file doesn't exist
if (!settingsFile.exists)
{
// Return current document path
return doc.path.toString();
}
else
{
// Open settings file and get saved path
settingsFile.open("r");
var path = settingsFile.readln();
settingsFile.close();
return path; // Return path
}
}
// CREATE BASE FILENAME FOR EXPORTED FILE(S)
function GetBaseFilename ()
{
var baseName = doc.name.substr(0, doc.name.lastIndexOf('.'));
if (options.lowercaseFilename) baseName = baseName.toLowerCase();
if (options.removeFilenameSpaces) baseName = baseName.replace (" ", "_");
return baseName;
}
function SaveSettingsFile ()
{
if (!settingsPath.exists)
{
if (!settingsPath.create())
{
alert("Couldn't create the settings file.");
return;
}
}
settingsFile.open ("w");
settingsFile.writeln(exportedFilePath);
settingsFile.close();
}
// MAIN DIALOG WINDOW
function ShowDialog ()
{
// Create the window
var win = new Window ("dialog", "Texture Map Exporter");
var mainPanel = win.add("panel", undefined, "Export Textures");
// Add a group for the exported file details
var exportInfoGroup = mainPanel.add("panel", undefined, "Export path");
var folderGroup = exportInfoGroup.add("group", undefined);
var exportPathField = folderGroup.add("edittext", undefined, exportedFilePath);
exportPathField.size = [420, ""];
var selectFolderBtn = folderGroup.add ("button", undefined, "...");
// Caches for checkboxes and exported channels
var exportCheckboxes = new Array(0);
var rgbChannels = new Array(0);
var alphaChannels = new Array(0);
// Generate layer groups list for drop-downs
var docLayerSets = [ " -- " ];
for (var i = 0; i < doc.layerSets.length; i++)
{
var layerName = doc.layerSets[i].name;
docLayerSets.push(layerName);
}
// Add panels
var i = 0;
// Create panels and dropdowns for each exportable map
for (var map in maps)
{
var mapPanel = mainPanel.add("panel", undefined, maps[map].mapName + " map:");
mapPanel.orientation = "row";
mapPanel.add("statictext", undefined, "Export map:");
exportCheckboxes[i] = mapPanel.add("checkbox", undefined);
mapPanel.add("statictext", undefined, "RGB channel: ");
rgbChannels[i] = mapPanel.add("dropdownList", undefined, docLayerSets);
FindDefaultLayer(rgbChannels[i], maps[map].defaultLayerName);
mapPanel.add("statictext", undefined, "Alpha channel: ");
alphaChannels[i] = mapPanel.add("dropdownList", undefined, docLayerSets);
FindDefaultLayer(alphaChannels[i], maps[map].defaultAlphaLayerName);
if (maps[map].alphaState == alpha.EXCLUSIVE) rgbChannels[i].enabled = false;
if (maps[map].alphaState == alpha.DISABLED) alphaChannels[i].enabled = false;
if (rgbChannels[i].selection != 0 || alphaChannels[i].selection != 0)
{
exportCheckboxes[i].value = true;
}
// CALLBACKS
rgbChannels[i].onChange = function()
{
if (this.selection.index != 0)
{
this.parent.children[1].value = true;
}
else
{
if (this.parent.children[5].selection == 0)
{
this.parent.children[1].value = false;
}
}
}
alphaChannels[i].onChange = function()
{
if (this.selection.index != 0)
{
this.parent.children[1].value = true;
}
else
{
if (this.parent.children[3].selection == 0)
{
this.parent.children[1].value = false;
}
}
}
i++;
}
// Add export options group
var optionsGroup = mainPanel.add("panel", undefined, "Export options");
optionsGroup.size = [510, 160]
var autoSaveCheckbox = optionsGroup.add("checkbox", undefined, "Auto-save exported files (Overwrites any existing files)");
var lowercaseCheckbox = optionsGroup.add("checkbox", undefined, "Convert filename to lowercase ('ExportMap' to 'exportmap')");
var removeSpacesCheckbox = optionsGroup.add("checkbox", undefined, "Convert filename spaces to underscores ('export map' to 'export_map')");
var saveOptionsGroup = optionsGroup.add("panel", undefined, "Save options");
saveOptionsGroup.orientation = "row";
var compressionCheckbox = saveOptionsGroup.add("checkbox", undefined, "Use RLE compression");
var closeOnSaveCheckbox = saveOptionsGroup.add("checkbox", undefined, "Auto-close exported documents");
// Enable/disable options according to defaults
autoSaveCheckbox.value = options.autoSave;
lowercaseCheckbox.value = options.lowercaseFilename;
removeSpacesCheckbox.value = options.removeFilenameSpaces;
compressionCheckbox.value = options.useCompression;
closeOnSaveCheckbox.value = options.closeDocsOnSave;
// Enabled/disable save options based on auto-save checkbox
compressionCheckbox.enabled = autoSaveCheckbox.value;
closeOnSaveCheckbox.enabled = autoSaveCheckbox.value;
// Add buttons group
var buttonGroup = mainPanel.add("group");
var exportBtn = buttonGroup.add("button", undefined, "Export maps", {name: "ok"});
var cancelBtn = buttonGroup.add("button", undefined, "Cancel");
// Main callbacks
autoSaveCheckbox.onClick = function ()
{
// Update save options based on auto-save checkbox
compressionCheckbox.enabled = this.value;
closeOnSaveCheckbox.enabled = this.value;
}
selectFolderBtn.onClick = function ()
{
// Show folder select dialog and update based on selection
var newPath = new Folder(exportedFilePath);
newPath = newPath.selectDlg("Select a folder to export to: ");
if (newPath != null)
{
exportPathField.text = newPath.fsName;
}
}
exportBtn.onClick = function ()
{
// Update options based on selections
options.autoSave = autoSaveCheckbox.value;
options.lowercaseFilename = lowercaseCheckbox.value;
options.removeFilenameSpaces = removeSpacesCheckbox.value;
options.useCompression = compressionCheckbox.value;
options.closeDocsOnSave = closeOnSaveCheckbox.value;
exportedFilePath = exportPathField.text;
// Update map info based on form info
var i = 0;
for (var map in maps)
{
maps[map].UpdateMapInfo(exportCheckboxes[i].value, rgbChannels[i].selection, alphaChannels[i].selection);
i++;
}
// Check if entries are all valid
if (FormEntryValid())
win.close(1); // If validation passes
else
alert(errorsLog); // Show errors if not
errorsLog = "The following errors occurred: \n\n";
}
if (win.show() == 1)
{
errorsLog = "The following errors occurred: \n\n";
return 0;
}
else
{
return 1;
}
}
// CHECK IF DEFAULT LAYER NAME EXISTS FOR THIS MAP
function FindDefaultLayer (list, defaultLayerName)
{
// Set active selection to first entry
list.selection = 0;
// Loop through dropdown
for (var i = 0; i < list.items.length; i++)
{
// Get item name
var itemName = list.items[i].text.toLowerCase();
// Check if current entry matches the default layer name for this map
if (itemName == defaultLayerName.toLowerCase())
{
list.selection = i; // Set as active selection if matching
return; // Stop looping
}
}
}
// CHECK FORM IS VALID BEFORE SUBMITTING
function FormEntryValid ()
{
if (ExportMapSelectionsValid() && FolderExists ())
return true;
}
// CHECK IF EXPORT MAPS ARE VALID
function ExportMapSelectionsValid ()
{
mapsToExport.length = 0;
var errorsCount = 0;
for (var map in maps)
{
if (maps[map].exportMap)
{
if (!maps[map].ReadyToExport())
{
errorsCount++;
}
else
{
mapsToExport.push(maps[map]);
}
}
}
if (mapsToExport.length == 0) { errorsLog += "- There are no maps selected for export.\n\n"; errorsCount++; }
if (errorsCount > 0) return false;
return true;
}
// CHECK IF EXPORT FOLDER EXISTS
function FolderExists ()
{
if (exportedFilePath.exists)
return true; // All good
else
{
// Setup folder variable
var newPath = new Folder (exportedFilePath);
// Try to create path
if (newPath.create())
return true; // Created successfully
else
{
errorsLog += "- The export folder doesn't exist and couldn't be created. Please check you've entered a valid path.\n\n";
return false; // Failed, and error added to log
}
}
}
// DO THE EXPORT
function ExportMaps (mapToExport)
{
// New document variable
var newDoc;
var docCreated = false; // Check if new doc has been made yet.
// Set original doc as active document
app.activeDocument = doc;
// Set active channels to component channels (RGB)
doc.activeChannels = doc.componentChannels;
HideAllLayers();
// If RGB channel is being exported for this map...
if (mapToExport.exportRGB)
{
// Get the layer group that is being exported, make it visible and the active layer
var layerToExport = doc.layerSets[mapToExport.rgbLayerIndex];
layerToExport.visible = true;
doc.activeLayer = layerToExport;
// Select all and copy merged
var selection = doc.selection.selectAll();
doc.selection.copy (true);
// Create new document
newDoc = app.documents.add(doc.width, doc.height, doc.resolution, exportedFilename + mapToExport.filePostfix);
docCreated = true;
// Make the new document active, select RGB channels and paste
app.activeDocument = newDoc;
newDoc.activeChannels = newDoc.componentChannels;
newDoc.paste();
}
// If Alpha channel is being exported for this map...
if (mapToExport.exportAlpha)
{
// Set original doc as active document
app.activeDocument = doc;
HideAllLayers ();
// Get the layer group that is being exported, make it visible and the active layer
var layerToExport = doc.layerSets[mapToExport.alphaLayerIndex];
layerToExport.visible = true;
doc.activeLayer = layerToExport;
// Select all and copy merged
var selection = doc.selection.selectAll();
doc.selection.copy (true);
// If document has been created, select it...
if (docCreated)
app.activeDocument = newDoc;
else
{
// Otherwise, create the document
newDoc = app.documents.add(doc.width, doc.height, doc.resolution, exportedFilename + mapToExport.filePostfix);
}
// Create the alpha channel, select the alpha channel, and paste
var alphaChannel = newDoc.channels.add();
newDoc.activeChannels = [alphaChannel];
newDoc.paste();
}
// If autosave is enabled, save away
if (options.autoSave)
{
SaveFile(newDoc);
}
// Set original doc as active document, select RGB channels
app.activeDocument = doc;
doc.activeChannels = doc.componentChannels;
// Deselect all
doc.selection.deselect();
}
// HIDE ALL LAYERS
function HideAllLayers ()
{
// Loop through layers
for (var i = 0; i < doc.layerSets.length; i++)
{
// Turn off visibility
doc.layerSets[i].visible = false;
}
}
// SAVE FILE
function SaveFile ()
{
// Cache active document
var currentDoc = app.activeDocument;
// Create SaveOptions variable
tgaSaveOptions = new TargaSaveOptions();
// Check if document has an alpha channel
var alphaExists = currentDoc.channels.length > 3;
// Alpha Channel is saved if alpha channel exists in document
tgaSaveOptions.alphaChannels = alphaExists ? true : false;
// 24-bit resolution (if no alpha) or 32-bit resolution (if alpha exists)
tgaSaveOptions.resolution = alphaExists ? TargaBitsPerPixels.THIRTYTWO : TargaBitsPerPixels.TWENTYFOUR;
// Compression set based on selection options
tgaSaveOptions.rleCompression = options.useCompression;
// Generate final file path
fullSavePath = new File(exportedFilePath + "/" + currentDoc.name + options.exportedFileExtension);
// Save the file
currentDoc.saveAs(fullSavePath, tgaSaveOptions, true, Extension.LOWERCASE);
// Close doc if option is set
if (options.closeDocsOnSave) currentDoc.close(SaveOptions.DONOTSAVECHANGES);
}
// GO!
Main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment