﻿
// This script exports photoshop layers as individual images.
// It also write a JSON file that can be imported into Spine
// where the images will be displayed in the same positions.

// Settings.
var ignoreHiddenLayers = true;
var savePNGs = true;
var saveJSON = true;
var scaleFactor = 1;
showDialog();

function main () {
	// Output dir.
	var dir = app.activeDocument.path + "/images/";

	new Folder(dir).create();

	var name = decodeURI(app.activeDocument.name);
	name = name.substring(0, name.indexOf("."));

	app.activeDocument.duplicate();

	// Collect original layer visibility and hide all layers.
	var layers = [];
	getLayers(app.activeDocument, layers);

	var layerCount = layers.length;
	var layerVisibility = {};

	for (var i = layerCount - 1; i >= 0; i--) {
		var layer = layers[i];
		layerVisibility[layer] = layer.visible;
		layer.visible = false;
	}

	// Save JSON.
	if (saveJSON || savePNGs) {
		var json = "{\"bones\":[{\"name\":\"root\"}],\"slots\":[\n";
		for (var i = layerCount - 1; i >= 0; i--) {
			var layer = layers[i];
			
			if (ignoreHiddenLayers && !layerVisibility[layer]) continue;

			json += "{\"name\":\"" + trim(layer.name) + "\",\"bone\":\"root\",\"attachment\":\"" + trim(layer.name) + "\"},\n";
		}
		json += "],\"skins\":{\"default\":{\n";
		for (var i = layerCount - 1; i >= 0; i--) {
			var layer = layers[i];
			
			if (ignoreHiddenLayers && !layerVisibility[layer]) continue;
				
			var x = app.activeDocument.width.as("px") * scaleFactor;
			var y = app.activeDocument.height.as("px") * scaleFactor;
			
			layer.visible = true;
			if (!layer.isBackgroundLayer)
				app.activeDocument.trim(TrimType.TRANSPARENT, false, true, true, false);
			x -= app.activeDocument.width.as("px") * scaleFactor;
			y -= app.activeDocument.height.as("px") * scaleFactor;
			if (!layer.isBackgroundLayer)
				app.activeDocument.trim(TrimType.TRANSPARENT, true, false, false, true);
			var width = app.activeDocument.width.as("px") * scaleFactor;
			var height = app.activeDocument.height.as("px") * scaleFactor;

			// Save image.
			if (savePNGs) {
				if (scaleFactor != 1) scaleImage();

				var file = File(dir + "/" + trim(layer.name));
				if (file.exists) file.remove();
				activeDocument.saveAs(file, new PNGSaveOptions(), true, Extension.LOWERCASE);

				if (scaleFactor != 1) stepHistoryBack();
			}
			
			if (!layer.isBackgroundLayer) {
				stepHistoryBack();
				stepHistoryBack();
			}
			layer.visible = false;
			
			x += Math.round(width) / 2;
			y += Math.round(height) / 2;
			json += "\"" + trim(layer.name) + "\":{\"" + trim(layer.name) + "\":{\"x\":" + x + ",\"y\":" + y+ ",\"width\":" + Math.round(width) + ",\"height\":" + Math.round(height) + "}},\n";
		}
		json += "}}, \"animations\": { \"animation\": {} }}";

		if (saveJSON) {
			// Write file.
			var file = new File(dir + name + ".json");
			file.remove();
			file.open("a");
			file.lineFeed = "\n";
			file.write(json);
			file.close();
		}
	}

	activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}

// Unfinished!
function hasLayerSets (layerset) {
	layerset = layerset.layerSets;
	for (var i = 0; i < layerset.length; i++)
		if (layerset[i].layerSets.length > 0) hasLayerSets(layerset[i]);
}

function scaleImage() {
	var imageSize = app.activeDocument.width.as("px");
	app.activeDocument.resizeImage(UnitValue(imageSize * scaleFactor, "px"), undefined, 72, ResampleMethod.BICUBICSHARPER);
}

function stepHistoryBack () {
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( charIDToTypeID( "HstS" ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Prvs" ));
	desc.putReference(charIDToTypeID( "null" ), ref);
	executeAction( charIDToTypeID( "slct" ), desc, DialogModes.NO );
}

function getLayers (layer, collect) {
	if (!layer.layers || layer.layers.length == 0) return layer;
	for (var i = 0, n = layer.layers.length; i < n; i++) {
		// For checking if its an adjustment layer, but it also excludes
		// LayerSets so we need to find the different types needed.
		//if (layer.layers[i].kind == LayerKind.NORMAL) {
			var child = getLayers(layer.layers[i], collect)
			if (child) collect.push(child);
		//}
	}
}

function trim (value) {
	return value.replace(/^\s+|\s+$/g, "");
}

function hasFilePath() {
	var ref = new ActionReference();
	ref.putEnumerated( charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
	return executeActionGet(ref).hasKey(stringIDToTypeID('fileReference'));
}

function showDialog () {
	if (!hasFilePath()) {
		alert("File path not found.\nYou need to save the document before continuing.");
		return;
	}

	var dialog = new Window("dialog", "Export Layers");

	dialog.savePNGs = dialog.add("checkbox", undefined, "Save PNGs"); 
	dialog.savePNGs.value = savePNGs;
	dialog.savePNGs.alignment = "left";

	dialog.saveJSON = dialog.add("checkbox", undefined, "Save JSON");
	dialog.saveJSON.alignment = "left";
	dialog.saveJSON.value = saveJSON;

	dialog.ignoreHiddenLayers = dialog.add("checkbox", undefined, "Ignore hidden layers");
	dialog.ignoreHiddenLayers.alignment = "left";
	dialog.ignoreHiddenLayers.value = ignoreHiddenLayers;

	var scaleGroup = dialog.add("panel", [0, 0, 180, 50], "Image Scale");
	var scaleText = scaleGroup.add("edittext", [10,10,40,30], scaleFactor * 100); 
	scaleGroup.add("statictext", [45, 12, 100, 20], "%");
	var scaleSlider = scaleGroup.add("slider", [60, 10,165,20], scaleFactor * 100, 1, 100);
	scaleText.onChanging = function() {
		scaleSlider.value = scaleText.text;
		if (scaleText.text < 1 || scaleText.text > 100) {
			alert("Valid numbers are 1-100.");
			scaleText.text = scaleFactor * 100;
			scaleSlider.value = scaleFactor * 100;
		}
	};
	scaleSlider.onChanging = function() { scaleText.text = Math.round(scaleSlider.value); };

	var confirmGroup = dialog.add("group", [0, 0, 180, 50]);
	var runButton = confirmGroup.add("button", [10, 10, 80, 35], "Ok");
	var cancelButton = confirmGroup.add("button", [90, 10, 170, 35], "Cancel");
	cancelButton.onClick = function() { this.parent.close(0); return; };
	runButton.onClick = function() {
		savePNGs = dialog.savePNGs.value;
		saveJSON = dialog.saveJSON.value;
		ignoreHiddenLayers = dialog.ignoreHiddenLayers.value;
		scaleFactor = scaleSlider.value / 100;
		main();
		this.parent.close(0);
	};

	dialog.orientation = "column";
	dialog.center();
	dialog.show();
}
