477 lines
16 KiB
React
477 lines
16 KiB
React
// 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" + ".png");
|
|
if (file.exists) file.remove();
|
|
|
|
var opts;
|
|
opts = new ExportOptionsSaveForWeb();
|
|
opts.format = SaveDocumentType.PNG;
|
|
opts.PNG8 = false;
|
|
opts.quality = 100;
|
|
|
|
activeDocument.exportDocument(file, ExportType.SAVEFORWEB, opts);
|
|
|
|
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();
|
|
|
|
var opts;
|
|
opts = new ExportOptionsSaveForWeb();
|
|
opts.format = SaveDocumentType.PNG;
|
|
opts.PNG8 = false;
|
|
opts.quality = 100;
|
|
|
|
activeDocument.exportDocument(new File(absImagesDir + attachmentName + ".png"), ExportType.SAVEFORWEB, opts);
|
|
}
|
|
|
|
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, "");
|
|
}
|