Create a gist now

Instantly share code, notes, and snippets.

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, "");
}
@apiiipz
apiiipz commented Nov 6, 2014

open program (adobe extendscript toolkit) ---> copy paste this script --> save as

@tayl0r
tayl0r commented Nov 9, 2014

I'm using PS CC 2014.2.1 on Windows 8.1 and when I run this script the exported PNGs are all messed up. I get this:

And here are my PSD layers + export screen (note, I've tried all kinds of different options, all with basically the same result):

It's working for another artist on my team who using PS 14.0 x64 on OSX. Any ideas on what the problem could be, how to fix, or what I could do to debug?

@fserb
fserb commented Nov 9, 2014

I'm getting the same thing as @tayl0r with PS CC on Mac.

@pinkart
pinkart commented Sep 21, 2015

how to download

@kamil-badalov

Png export is fine. But json is empty. What should I do?

@MintyMerry

This script is a great time-saver! Especially with the set ruler 0,0 as origin feature :")

Would it be possible to add a feature that would define which (Spine) slot each layer would be exported into? For example, if we have a couple frames for a blinking animation with the following layers:

  • Eyes_closed
  • Eyes_open
    It would be nice to skip the extra drag, drop, and rename steps if they can be exported into the same slot, named Eyes.
@adankoala

Updated the script but i think is not the problem with the script, Png export is ok but i can't open it on Spine, the images are missing, i can't work for now !!! any advice on the new update from Spine? before it was all working fine !!!
I have a MacBook Pro (17-inch, Late 2011) all up to date. :(
untitled-1

@iamdash
iamdash commented May 24, 2016

+1

@ryanreptoid

I'm also having the broken image issue. Any ideas?
broken

@ryanreptoid

Figured it out: you need to browse to the folder you exported images to. This fixes it! @adankoala
browse

@Jordimlaka
Jordimlaka commented Aug 23, 2016 edited

Hi,

This script saves the PNG with the color profile incrusted?, when the graphics is showed into Spine, these graphics cannot conserve the color profile aplied in photoshop.

Any ideas?

For other hand, i want to put one bone for each slot automatically when i export from psd, how can i have edit the script to do this?

Thanks.

@BethHicks

This is a total noob question, but i rarely use scripts... now I have saved it into my extendscript toolkit... not even sure if i did it right. lol. how do i actually run the script to get the files i need to get them to show in Spine? Here's an image of my layer setup...

layer setup

@AlexMaan

Hello guys,

Error importing skeleton.
I had message when you import files from a photoshop - An error occurred while importing yhe skeleton.
import_error
Sptires also did not appear.
import_sprites_error
I decided to deal with the .JSON file and found the cause of the error:
Setting - sprite size - using fractional values and added an extra comma (like (x,5,y,z,)), so JSON did not load correctly.
To fix this bug I use nopad-find&replace command, but it`s not very convenient, maybe you can take into account this case in your Script.
json_comma_error

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment