Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save thewei/0771446d77205a197e638893bd7f8752 to your computer and use it in GitHub Desktop.
Save thewei/0771446d77205a197e638893bd7f8752 to your computer and use it in GitHub Desktop.
Adobe Photoshop script to export to Esoteric Software's Spine: http://esotericsoftware.com/
// This script exports photoshop layers as individual PNGs. It also
// writes a JSON file that can be imported into Spine where the images
// will be displayed in the same positions and draw order.
// Setting defaults.
var writePngs = true;
var writeTemplate = false;
var writeJson = true;
var ignoreHiddenLayers = true;
var pngScale = 1;
var groupsAsSkins = false;
var useRulerOrigin = false;
var imagesDir = "./images/";
var projectDir = "";
var padding = 1;
// IDs for saving settings.
const settingsID = stringIDToTypeID("settings");
const writePngsID = stringIDToTypeID("writePngs");
const writeTemplateID = stringIDToTypeID("writeTemplate");
const writeJsonID = stringIDToTypeID("writeJson");
const ignoreHiddenLayersID = stringIDToTypeID("ignoreHiddenLayers");
const groupsAsSkinsID = stringIDToTypeID("groupsAsSkins");
const useRulerOriginID = stringIDToTypeID("useRulerOrigin");
const pngScaleID = stringIDToTypeID("pngScale");
const imagesDirID = stringIDToTypeID("imagesDir");
const projectDirID = stringIDToTypeID("projectDir");
const paddingID = stringIDToTypeID("padding");
var originalDoc;
try {
originalDoc = app.activeDocument;
} catch (ignored) {}
var settings, progress;
loadSettings();
showDialog();
function run () {
// Output dirs.
var absProjectDir = absolutePath(projectDir);
new Folder(absProjectDir).create();
var absImagesDir = absolutePath(imagesDir);
var imagesFolder = new Folder(absImagesDir);
imagesFolder.create();
var relImagesDir = imagesFolder.getRelativeURI(absProjectDir);
relImagesDir = relImagesDir == "." ? "" : (relImagesDir + "/");
// Get ruler origin.
var xOffSet = 0, yOffSet = 0;
if (useRulerOrigin) {
var ref = new ActionReference();
ref.putEnumerated(charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
var desc = executeActionGet(ref);
xOffSet = desc.getInteger(stringIDToTypeID("rulerOriginH")) >> 16;
yOffSet = desc.getInteger(stringIDToTypeID("rulerOriginV")) >> 16;
}
activeDocument.duplicate();
// Output template image.
if (writeTemplate) {
if (pngScale != 1) {
scaleImage();
storeHistory();
}
var file = new File(absImagesDir + "template");
if (file.exists) file.remove();
activeDocument.saveAs(file, new PNGSaveOptions(), true, Extension.LOWERCASE);
if (pngScale != 1) restoreHistory();
}
if (!writeJson && !writePngs) {
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
return;
}
// Rasterize all layers.
try {
executeAction(stringIDToTypeID( "rasterizeAll" ), undefined, DialogModes.NO);
} catch (ignored) {}
// Collect and hide layers.
var layers = [];
collectLayers(activeDocument, layers);
var layersCount = layers.length;
storeHistory();
// Store the slot names and layers for each skin.
var slots = {}, skins = { "default": [] };
for (var i = layersCount - 1; i >= 0; i--) {
var layer = layers[i];
// Use groups as skin names.
var potentialSkinName = trim(layer.parent.name);
var layerGroupSkin = potentialSkinName.indexOf("-NOSKIN") == -1;
var skinName = (groupsAsSkins && layer.parent.typename == "LayerSet" && layerGroupSkin) ? potentialSkinName : "default";
var skinLayers = skins[skinName];
if (!skinLayers) skins[skinName] = skinLayers = [];
skinLayers[skinLayers.length] = layer;
slots[layerName(layer)] = true;
}
// Output skeleton and bones.
var json = '{"skeleton":{"images":"' + relImagesDir + '"},\n"bones":[{"name":"root"}],\n"slots":[\n';
// Output slots.
var slotsCount = countAssocArray(slots);
var slotIndex = 0;
for (var slotName in slots) {
if (!slots.hasOwnProperty(slotName)) continue;
// Use image prefix if slot's attachment is in the default skin.
var attachmentName = slotName;
var defaultSkinLayers = skins["default"];
for (var i = defaultSkinLayers.length - 1; i >= 0; i--) {
if (layerName(defaultSkinLayers[i]) == slotName) {
attachmentName = slotName;
break;
}
}
json += '\t{"name":"' + slotName + '","bone":"root","attachment":"' + attachmentName + '"}';
slotIndex++;
json += slotIndex < slotsCount ? ",\n" : "\n";
}
json += '],\n"skins":{\n';
// Output skins.
var skinsCount = countAssocArray(skins);
var skinIndex = 0;
for (var skinName in skins) {
if (!skins.hasOwnProperty(skinName)) continue;
json += '\t"' + skinName + '":{\n';
var skinLayers = skins[skinName];
var skinLayersCount = skinLayers.length;
var skinLayerIndex = 0;
for (var i = skinLayersCount - 1; i >= 0; i--) {
var layer = skinLayers[i];
var slotName = layerName(layer);
var placeholderName, attachmentName;
if (skinName == "default") {
placeholderName = slotName;
attachmentName = placeholderName;
} else {
placeholderName = slotName;
attachmentName = skinName + "/" + slotName;
}
var x = activeDocument.width.as("px") * pngScale;
var y = activeDocument.height.as("px") * pngScale;
layer.visible = true;
if (!layer.isBackgroundLayer) activeDocument.trim(TrimType.TRANSPARENT, false, true, true, false);
x -= activeDocument.width.as("px") * pngScale;
y -= activeDocument.height.as("px") * pngScale;
if (!layer.isBackgroundLayer) activeDocument.trim(TrimType.TRANSPARENT, true, false, false, true);
var width = activeDocument.width.as("px") * pngScale + padding * 2;
var height = activeDocument.height.as("px") * pngScale + padding * 2;
// Save image.
if (writePngs) {
if (pngScale != 1) scaleImage();
if (padding > 0) activeDocument.resizeCanvas(width, height, AnchorPosition.MIDDLECENTER);
if (skinName != "default") new Folder(absImagesDir + skinName).create();
activeDocument.saveAs(new File(absImagesDir + attachmentName), new PNGSaveOptions(), true, Extension.LOWERCASE);
}
restoreHistory();
layer.visible = false;
x += Math.round(width) / 2;
y += Math.round(height) / 2;
// Make relative to the Photoshop document ruler origin.
if (useRulerOrigin) {
x -= xOffSet * pngScale;
y -= activeDocument.height.as("px") * pngScale - yOffSet * pngScale; // Invert y.
}
if (attachmentName == placeholderName) {
json += '\t\t"' + slotName + '":{"' + placeholderName + '":{'
+ '"x":' + x + ',"y":' + y + ',"width":' + Math.round(width) + ',"height":' + Math.round(height) + '}}';
} else {
json += '\t\t"' + slotName + '":{"' + placeholderName + '":{"name":"' + attachmentName + '", '
+ '"x":' + x + ',"y":' + y + ',"width":' + Math.round(width) + ',"height":' + Math.round(height) + '}}';
}
skinLayerIndex++;
json += skinLayerIndex < skinLayersCount ? ",\n" : "\n";
}
json += "\t\}";
skinIndex++;
json += skinIndex < skinsCount ? ",\n" : "\n";
}
json += '},\n"animations":{"animation":{}}\n}';
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
// Output JSON file.
if (writeJson) {
var name = decodeURI(originalDoc.name);
name = name.substring(0, name.indexOf("."));
var file = new File(absProjectDir + name + ".json");
file.remove();
file.open("w", "TEXT");
file.lineFeed = "\n";
file.write(json);
file.close();
}
}
// Dialog and settings:
function showDialog () {
if (!originalDoc) {
alert("Please open a document before running the LayersToPNG script.");
return;
}
if (!hasFilePath()) {
alert("Please save the document before running the LayersToPNG script.");
return;
}
var dialog = new Window("dialog", "Spine LayersToPNG");
dialog.alignChildren = "fill";
var checkboxGroup = dialog.add("group");
var group = checkboxGroup.add("group");
group.orientation = "column";
group.alignChildren = "left";
var writePngsCheckbox = group.add("checkbox", undefined, " Write layers as PNGs");
writePngsCheckbox.value = writePngs;
var writeTemplateCheckbox = group.add("checkbox", undefined, " Write a template PNG");
writeTemplateCheckbox.value = writeTemplate;
var writeJsonCheckbox = group.add("checkbox", undefined, " Write Spine JSON");
writeJsonCheckbox.value = writeJson;
group = checkboxGroup.add("group");
group.orientation = "column";
group.alignChildren = "left";
var ignoreHiddenLayersCheckbox = group.add("checkbox", undefined, " Ignore hidden layers");
ignoreHiddenLayersCheckbox.value = ignoreHiddenLayers;
var groupsAsSkinsCheckbox = group.add("checkbox", undefined, " Use groups as skins");
groupsAsSkinsCheckbox.value = groupsAsSkins;
var useRulerOriginCheckbox = group.add("checkbox", undefined, " Use ruler origin as 0,0");
useRulerOriginCheckbox.value = useRulerOrigin;
var slidersGroup = dialog.add("group");
group = slidersGroup.add("group");
group.orientation = "column";
group.alignChildren = "right";
group.add("statictext", undefined, "PNG scale:");
group.add("statictext", undefined, "Padding:");
group = slidersGroup.add("group");
group.orientation = "column";
var scaleText = group.add("edittext", undefined, pngScale * 100);
scaleText.characters = 4;
var paddingText = group.add("edittext", undefined, padding);
paddingText.characters = 4;
group = slidersGroup.add("group");
group.orientation = "column";
group.add("statictext", undefined, "%");
group.add("statictext", undefined, "px");
group = slidersGroup.add("group");
group.alignment = ["fill", ""];
group.orientation = "column";
group.alignChildren = ["fill", ""];
var scaleSlider = group.add("slider", undefined, pngScale * 100, 1, 100);
var paddingSlider = group.add("slider", undefined, padding, 0, 4);
scaleText.onChanging = function () { scaleSlider.value = scaleText.text; };
scaleSlider.onChanging = function () { scaleText.text = Math.round(scaleSlider.value); };
paddingText.onChanging = function () { paddingSlider.value = paddingText.text; };
paddingSlider.onChanging = function () { paddingText.text = Math.round(paddingSlider.value); };
var outputGroup = dialog.add("panel", undefined, "Output directories");
outputGroup.alignChildren = "fill";
outputGroup.margins = [10,15,10,10];
var textGroup = outputGroup.add("group");
group = textGroup.add("group");
group.orientation = "column";
group.alignChildren = "right";
group.add("statictext", undefined, "Images:");
group.add("statictext", undefined, "JSON:");
group = textGroup.add("group");
group.orientation = "column";
group.alignChildren = "fill";
group.alignment = ["fill", ""];
var imagesDirText = group.add("edittext", undefined, imagesDir);
var projectDirText = group.add("edittext", undefined, projectDir);
outputGroup.add("statictext", undefined, "Begin paths with \"./\" to be relative to the PSD file.").alignment = "center";
var group = dialog.add("group");
group.alignment = "center";
var runButton = group.add("button", undefined, "OK");
var cancelButton = group.add("button", undefined, "Cancel");
cancelButton.onClick = function () {
dialog.close(0);
return;
};
function updateSettings () {
writePngs = writePngsCheckbox.value;
writeTemplate = writeTemplateCheckbox.value;
writeJson = writeJsonCheckbox.value;
ignoreHiddenLayers = ignoreHiddenLayersCheckbox.value;
var scaleValue = parseFloat(scaleText.text);
if (scaleValue > 0 && scaleValue <= 100) pngScale = scaleValue / 100;
groupsAsSkins = groupsAsSkinsCheckbox.value;
useRulerOrigin = useRulerOriginCheckbox.value;
imagesDir = imagesDirText.text;
projectDir = projectDirText.text;
var paddingValue = parseInt(paddingText.text);
if (paddingValue >= 0) padding = paddingValue;
}
dialog.onClose = function() {
updateSettings();
saveSettings();
};
runButton.onClick = function () {
if (scaleText.text <= 0 || scaleText.text > 100) {
alert("PNG scale must be between > 0 and <= 100.");
return;
}
if (paddingText.text < 0) {
alert("Padding must be >= 0.");
return;
}
dialog.close(0);
var rulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
try {
run();
} catch (e) {
alert("An unexpected error has occurred.\n\nTo debug, run the LayersToPNG script using Adobe ExtendScript "
+ "with \"Debug > Do not break on guarded exceptions\" unchecked.");
debugger;
} finally {
if (activeDocument != originalDoc) activeDocument.close(SaveOptions.DONOTSAVECHANGES);
app.preferences.rulerUnits = rulerUnits;
}
};
dialog.center();
dialog.show();
}
function loadSettings () {
try {
settings = app.getCustomOptions(settingsID);
} catch (e) {
saveSettings();
}
if (typeof settings == "undefined") saveSettings();
settings = app.getCustomOptions(settingsID);
if (settings.hasKey(writePngsID)) writePngs = settings.getBoolean(writePngsID);
if (settings.hasKey(writeTemplateID)) writeTemplate = settings.getBoolean(writeTemplateID);
if (settings.hasKey(writeJsonID)) writeJson = settings.getBoolean(writeJsonID);
if (settings.hasKey(ignoreHiddenLayersID)) ignoreHiddenLayers = settings.getBoolean(ignoreHiddenLayersID);
if (settings.hasKey(pngScaleID)) pngScale = settings.getDouble(pngScaleID);
if (settings.hasKey(groupsAsSkinsID)) groupsAsSkins = settings.getBoolean(groupsAsSkinsID);
if (settings.hasKey(useRulerOriginID)) useRulerOrigin = settings.getBoolean(useRulerOriginID);
if (settings.hasKey(imagesDirID)) imagesDir = settings.getString(imagesDirID);
if (settings.hasKey(projectDirID)) projectDir = settings.getString(projectDirID);
if (settings.hasKey(paddingID)) padding = settings.getDouble(paddingID);
}
function saveSettings () {
var settings = new ActionDescriptor();
settings.putBoolean(writePngsID, writePngs);
settings.putBoolean(writeTemplateID, writeTemplate);
settings.putBoolean(writeJsonID, writeJson);
settings.putBoolean(ignoreHiddenLayersID, ignoreHiddenLayers);
settings.putDouble(pngScaleID, pngScale);
settings.putBoolean(groupsAsSkinsID, groupsAsSkins);
settings.putBoolean(useRulerOriginID, useRulerOrigin);
settings.putString(imagesDirID, imagesDir);
settings.putString(projectDirID, projectDir);
settings.putDouble(paddingID, padding);
app.putCustomOptions(settingsID, settings, true);
}
// Photoshop utility:
function scaleImage () {
var imageSize = activeDocument.width.as("px");
activeDocument.resizeImage(UnitValue(imageSize * pngScale, "px"), null, null, ResampleMethod.BICUBICSHARPER);
}
var historyIndex;
function storeHistory () {
historyIndex = activeDocument.historyStates.length - 1;
}
function restoreHistory () {
activeDocument.activeHistoryState = activeDocument.historyStates[historyIndex];
}
function collectLayers (layer, collect) {
for (var i = 0, n = layer.layers.length; i < n; i++) {
var child = layer.layers[i];
if (ignoreHiddenLayers && !child.visible) continue;
if (child.bounds[2] == 0 && child.bounds[3] == 0) continue;
if (child.layers && child.layers.length > 0)
collectLayers(child, collect);
else if (child.kind == LayerKind.NORMAL) {
collect.push(child);
child.visible = false;
}
}
}
function hasFilePath () {
var ref = new ActionReference();
ref.putEnumerated(charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
return executeActionGet(ref).hasKey(stringIDToTypeID("fileReference"));
}
function absolutePath (path) {
path = trim(path);
if (path.length == 0)
path = activeDocument.path.toString();
else if (imagesDir.indexOf("./") == 0)
path = activeDocument.path + path.substring(1);
path = path.replace(/\\/g, "/");
if (path.substring(path.length - 1) != "/") path += "/";
return path;
}
// JavaScript utility:
function countAssocArray (obj) {
var count = 0;
for (var key in obj)
if (obj.hasOwnProperty(key)) count++;
return count;
}
function trim (value) {
return value.replace(/^\s+|\s+$/g, "");
}
function endsWith (str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
function stripSuffix (str, suffix) {
if (endsWith(str.toLowerCase(), suffix.toLowerCase())) str = str.substring(0, str.length - suffix.length);
return str;
}
function layerName (layer) {
return stripSuffix(trim(layer.name), ".png").replace(/[:\/\\*\?\"\<\>\|]/g, "");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment