整理ps插件,加入重命名插件
This commit is contained in:
1 parent
3b74c36083
commit
0905159c6e
15 files changed
+7115
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,476 @@
|
||||
// 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, "");
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
|
||||
// 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();
|
||||
}
|
||||
@@ -0,0 +1,950 @@
|
||||
#target photoshop
|
||||
app.bringToFront();
|
||||
|
||||
// This script exports Adobe Photoshop layers as individual PNGs. It also
|
||||
// writes a JSON file which can be imported into Spine where the images
|
||||
// will be displayed in the same positions and draw order.
|
||||
|
||||
// Copyright (c) 2012-2017, Esoteric Software
|
||||
// All rights reserved.
|
||||
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
var scriptVersion = 2.9; // This is incremented every time the script is modified, so you know if you have the latest.
|
||||
|
||||
var cs2 = parseInt(app.version) < 10;
|
||||
|
||||
var originalDoc;
|
||||
try {
|
||||
originalDoc = app.activeDocument;
|
||||
} catch (ignored) {}
|
||||
|
||||
var defaultSettings = {
|
||||
ignoreHiddenLayers: false,
|
||||
ignoreBackground: true,
|
||||
writeTemplate: false,
|
||||
writeJson: true,
|
||||
trimWhitespace: true,
|
||||
scale: 1,
|
||||
padding: 1,
|
||||
imagesDir: "./images/",
|
||||
jsonPath: "./",
|
||||
};
|
||||
var settings = loadSettings();
|
||||
showSettingsDialog();
|
||||
|
||||
var progress, cancel;
|
||||
function run () {
|
||||
showProgressDialog();
|
||||
|
||||
// Output dirs.
|
||||
var jsonFile = new File(jsonPath(settings.jsonPath));
|
||||
jsonFile.parent.create();
|
||||
var imagesDir = absolutePath(settings.imagesDir);
|
||||
var imagesFolder = new Folder(imagesDir);
|
||||
imagesFolder.create();
|
||||
|
||||
// Get ruler origin.
|
||||
var action = new ActionReference();
|
||||
action.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt"));
|
||||
var result = executeActionGet(action);
|
||||
var xOffSet = result.getInteger(sID("rulerOriginH")) >> 16;
|
||||
var yOffSet = result.getInteger(sID("rulerOriginV")) >> 16;
|
||||
|
||||
activeDocument.duplicate();
|
||||
deselectLayers();
|
||||
|
||||
try {
|
||||
convertToRGB();
|
||||
} catch (ignored) {}
|
||||
if (app.activeDocument.mode != DocumentMode.RGB) {
|
||||
alert("Please change the image mode to RGB color.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Output template image.
|
||||
if (settings.writeTemplate) {
|
||||
if (settings.scale != 1) {
|
||||
storeHistory();
|
||||
scaleImage();
|
||||
}
|
||||
|
||||
var file = new File(imagesDir + "template.png");
|
||||
if (file.exists) file.remove();
|
||||
|
||||
savePNG(file);
|
||||
|
||||
if (settings.scale != 1) restoreHistory();
|
||||
}
|
||||
|
||||
if (!settings.jsonPath && !settings.imagesDir) {
|
||||
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rasterize all layers.
|
||||
try {
|
||||
executeAction(sID("rasterizeAll"), undefined, DialogModes.NO);
|
||||
} catch (ignored) {}
|
||||
|
||||
// Add a history item to prevent layer visibility from changing by the active layer being reset to the top.
|
||||
activeDocument.artLayers.add();
|
||||
|
||||
// Collect and hide layers.
|
||||
var layers = [];
|
||||
collectLayers(activeDocument, layers);
|
||||
var layersCount = layers.length;
|
||||
|
||||
// Add a history item to prevent layer visibility from changing by restoreHistory.
|
||||
activeDocument.artLayers.add();
|
||||
|
||||
// Store the bones, slot names, and layers for each skin.
|
||||
var bones = { root: { name: "root", x: 0, y: 0, children: [] } };
|
||||
var slots = {}, slotsCount = 0;
|
||||
var skins = { "default": [] }, skinsCount = 0;
|
||||
var totalLayerCount = 0;
|
||||
outer:
|
||||
for (var i = 0; i < layersCount; i++) {
|
||||
var layer = layers[i];
|
||||
if (layer.kind != LayerKind.NORMAL && !isGroup(layer)) continue;
|
||||
layer.attachmentName = folders(layer, "") + stripTags(layer.name);
|
||||
|
||||
var bone = null;
|
||||
var boneLayer = findTagLayer(layer, "bone", null);
|
||||
if (boneLayer) {
|
||||
function getParentBone (boneLayer, bones) {
|
||||
var parentName = findTag(boneLayer.parent, "bone", "root");
|
||||
var parent = bones[parentName];
|
||||
if (!parent) { // Parent bone group with no attachment layers.
|
||||
var parentParent = getParentBone(boneLayer.parent, bones);
|
||||
bones[parentName] = parent = { name: parentName, parent: parentParent, children: [], x: 0, y: 0 };
|
||||
parentParent.children.push(parent);
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
var parent = getParentBone(boneLayer, bones);
|
||||
|
||||
var boneName = stripTags(boneLayer.name);
|
||||
bone = bones[boneName];
|
||||
if (bone) {
|
||||
if (parent != bone.parent) {
|
||||
alert("Multiple layers for the \"" + boneName + "\" bone have different parent bones:\n\n"
|
||||
+ bone.parent.name + "\n"
|
||||
+ parent.name);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
bones[boneName] = bone = { name: boneName, parent: parent, children: [] };
|
||||
parent.children.push(bone);
|
||||
}
|
||||
if (layer.wasVisible) {
|
||||
bone.x = (layer.bounds[0].as("px")) * settings.scale - settings.padding;
|
||||
bone.x += (layer.bounds[2].as("px") - layer.bounds[0].as("px")) * settings.scale / 2 + settings.padding;
|
||||
bone.y = (activeDocument.height.as("px") - layer.bounds[1].as("px")) * settings.scale + settings.padding;
|
||||
bone.y -= (layer.bounds[3].as("px") - layer.bounds[1].as("px")) * settings.scale / 2 + settings.padding;
|
||||
|
||||
// Make relative to the Photoshop document ruler origin.
|
||||
bone.x -= xOffSet * settings.scale;
|
||||
bone.y -= (activeDocument.height.as("px") - yOffSet) * settings.scale;
|
||||
}
|
||||
}
|
||||
|
||||
layer.slotName = findTag(layer, "slot", layer.attachmentName);
|
||||
if (!slots[layer.slotName]) slotsCount++;
|
||||
slots[layer.slotName] = { bone: bone, attachment: layer.wasVisible ? layer.attachmentName : null };
|
||||
|
||||
if (layer.blendMode == BlendMode.LINEARDODGE)
|
||||
slots[layer.slotName].blend = "additive";
|
||||
else if (layer.blendMode == BlendMode.MULTIPLY)
|
||||
slots[layer.slotName].blend = "multiply";
|
||||
else if (layer.blendMode == BlendMode.SCREEN)
|
||||
slots[layer.slotName].blend = "screen";
|
||||
|
||||
var skinName = findTag(layer, "skin", "default");
|
||||
var skinSlots = skins[skinName];
|
||||
if (!skinSlots) {
|
||||
skins[skinName] = skinSlots = {};
|
||||
skinsCount++;
|
||||
}
|
||||
|
||||
var skinLayers = skinSlots[layer.slotName];
|
||||
if (!skinLayers) skinSlots[layer.slotName] = skinLayers = [];
|
||||
for (var ii = 0, nn = skinLayers.length; ii < nn; ii++) {
|
||||
if (skinLayers[ii].attachmentName == layer.attachmentName) {
|
||||
alert("Multiple layers for the \"" + skinName + "\" skin have the same name:\n\n"
|
||||
+ layer.attachmentName
|
||||
+ "\n\nRename or use the [ignore] tag for the other layers.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
skinLayers[skinLayers.length] = layer;
|
||||
totalLayerCount++;
|
||||
}
|
||||
|
||||
// Output skeleton.
|
||||
var json = '{ "skeleton": { "images": "' + imagesDir + '" },\n"bones": [\n';
|
||||
|
||||
// Output bones.
|
||||
function outputBone (bone) {
|
||||
var json = bone.parent ? ",\n" : "";
|
||||
json += '\t{ "name": ' + quote(bone.name);
|
||||
var x = bone.x, y = bone.y;
|
||||
if (bone.parent) {
|
||||
x -= bone.parent.x;
|
||||
y -= bone.parent.y;
|
||||
json += ', "parent": ' + quote(bone.parent.name);
|
||||
}
|
||||
if (x) json += ', "x": ' + x;
|
||||
if (y) json += ', "y": ' + y;
|
||||
json += ' }';
|
||||
for (var i = 0, n = bone.children.length; i < n; i++)
|
||||
json += outputBone(bone.children[i]);
|
||||
return json;
|
||||
}
|
||||
for (var boneName in bones) {
|
||||
if (!bones.hasOwnProperty(boneName)) continue;
|
||||
var bone = bones[boneName];
|
||||
if (!bone.parent) json += outputBone(bone);
|
||||
}
|
||||
json += '\n],\n"slots": [\n';
|
||||
|
||||
// Output slots.
|
||||
var slotIndex = 0;
|
||||
for (var slotName in slots) {
|
||||
if (!slots.hasOwnProperty(slotName)) continue;
|
||||
var slot = slots[slotName];
|
||||
json += '\t{ "name": ' + quote(slotName) + ', "bone": ' + quote(slot.bone ? slot.bone.name : "root");
|
||||
if (slot.attachment) json += ', "attachment": ' + quote(slot.attachment);
|
||||
if (slot.blend) json += ', "blend": ' + quote(slot.blend);
|
||||
json += ' }';
|
||||
slotIndex++;
|
||||
json += slotIndex < slotsCount ? ",\n" : "\n";
|
||||
}
|
||||
json += '],\n"skins": {\n';
|
||||
|
||||
// Output skins.
|
||||
var skinIndex = 0, layerCount = 0;
|
||||
for (var skinName in skins) {
|
||||
if (!skins.hasOwnProperty(skinName)) continue;
|
||||
json += '\t"' + skinName + '": {\n';
|
||||
|
||||
var skinSlots = skins[skinName];
|
||||
var skinSlotIndex = 0, skinSlotsCount = countAssocArray(skinSlots);
|
||||
for (var slotName in skinSlots) {
|
||||
if (!skinSlots.hasOwnProperty(slotName)) continue;
|
||||
var bone = slots[slotName].bone;
|
||||
|
||||
json += '\t\t' + quote(slotName) + ': {\n';
|
||||
|
||||
var skinLayers = skinSlots[slotName];
|
||||
var skinLayerIndex = 0, skinLayersCount = skinLayers.length;
|
||||
for (var i = skinLayersCount - 1; i >= 0; i--) {
|
||||
var layer = skinLayers[i];
|
||||
layer.visible = true;
|
||||
|
||||
if (cancel) {
|
||||
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
|
||||
return;
|
||||
}
|
||||
setProgress(++layerCount / totalLayerCount, trim(layer.name));
|
||||
|
||||
var placeholderName = layer.attachmentName;
|
||||
var attachmentName = (skinName == "default" ? "" : skinName + "/") + placeholderName;
|
||||
|
||||
if (isGroup(layer)) {
|
||||
activeDocument.activeLayer = layer;
|
||||
layer = layer.merge();
|
||||
}
|
||||
|
||||
storeHistory();
|
||||
|
||||
var x = activeDocument.width.as("px") * settings.scale;
|
||||
var y = activeDocument.height.as("px") * settings.scale;
|
||||
if (settings.trimWhitespace) {
|
||||
if (!layer.isBackgroundLayer) activeDocument.trim(TrimType.TRANSPARENT, false, true, true, false);
|
||||
x -= activeDocument.width.as("px") * settings.scale;
|
||||
y -= activeDocument.height.as("px") * settings.scale;
|
||||
if (!layer.isBackgroundLayer) activeDocument.trim(TrimType.TRANSPARENT, true, false, false, true);
|
||||
}
|
||||
var width = activeDocument.width.as("px") * settings.scale + settings.padding * 2;
|
||||
var height = activeDocument.height.as("px") * settings.scale + settings.padding * 2;
|
||||
|
||||
// Save image.
|
||||
if (settings.imagesDir) {
|
||||
if (settings.scale != 1) scaleImage();
|
||||
if (settings.padding > 0) activeDocument.resizeCanvas(width, height, AnchorPosition.MIDDLECENTER);
|
||||
|
||||
var file = new File(imagesDir + attachmentName);
|
||||
file.parent.create();
|
||||
savePNG(file);
|
||||
}
|
||||
|
||||
restoreHistory();
|
||||
if (layerCount < totalLayerCount) deleteLayer(layer);
|
||||
|
||||
x += Math.round(width) / 2 - settings.padding;
|
||||
y += Math.round(height) / 2 - settings.padding;
|
||||
|
||||
// Make relative to the Photoshop document ruler origin.
|
||||
x -= xOffSet * settings.scale;
|
||||
y -= (activeDocument.height.as("px") - yOffSet) * settings.scale;
|
||||
|
||||
if (bone) { // Make relative to parent bone.
|
||||
x -= bone.x;
|
||||
y -= bone.y;
|
||||
}
|
||||
|
||||
json += "\t\t\t" + quote(placeholderName) + ': { ';
|
||||
if (attachmentName != placeholderName) json += '"name": ' + quote(attachmentName) + ', ';
|
||||
json += '"x": ' + x + ', "y": ' + y + ', "width": ' + Math.round(width) + ', "height": ' + Math.round(height);
|
||||
|
||||
json += " }" + (++skinLayerIndex < skinLayersCount ? ",\n" : "\n");
|
||||
}
|
||||
|
||||
json += "\t\t}" + (++skinSlotIndex < skinSlotsCount ? ",\n" : "\n");
|
||||
}
|
||||
|
||||
json += "\t\}" + (++skinIndex < skinsCount ? ",\n" : "\n");
|
||||
}
|
||||
json += '},\n"animations": { "animation": {} }\n}';
|
||||
|
||||
activeDocument.close(SaveOptions.DONOTSAVECHANGES);
|
||||
|
||||
// Output JSON file.
|
||||
if (settings.writeJson && settings.jsonPath) {
|
||||
jsonFile.encoding = "UTF-8";
|
||||
jsonFile.remove();
|
||||
jsonFile.open("w", "TEXT");
|
||||
jsonFile.lineFeed = "\n";
|
||||
jsonFile.write(json);
|
||||
jsonFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Settings dialog:
|
||||
|
||||
function showSettingsDialog () {
|
||||
if (parseInt(app.version) < 9) {
|
||||
alert("Photoshop CS2 or later is required.");
|
||||
return;
|
||||
}
|
||||
if (!originalDoc) {
|
||||
alert("Please open a document before running the PhotoshopToSpine script.");
|
||||
return;
|
||||
}
|
||||
if (!hasFilePath()) {
|
||||
alert("Please save the document before running the PhotoshopToSpine script.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Layout.
|
||||
var dialog = new Window("dialog", "PhotoshopToSpine v" + scriptVersion), group;
|
||||
dialog.alignChildren = "fill";
|
||||
|
||||
try {
|
||||
dialog.add("image", undefined, new File(scriptDir() + "logo.png"));
|
||||
} catch (ignored) {}
|
||||
|
||||
var settingsGroup = dialog.add("panel", undefined, "Settings");
|
||||
settingsGroup.margins = [10,15,10,10];
|
||||
settingsGroup.alignChildren = "fill";
|
||||
var checkboxGroup = settingsGroup.add("group");
|
||||
checkboxGroup.alignChildren = ["left", ""];
|
||||
checkboxGroup.orientation = "row";
|
||||
group = checkboxGroup.add("group");
|
||||
group.orientation = "column";
|
||||
group.alignChildren = ["left", ""];
|
||||
var ignoreHiddenLayersCheckbox = group.add("checkbox", undefined, " Ignore hidden layers");
|
||||
ignoreHiddenLayersCheckbox.value = settings.ignoreHiddenLayers;
|
||||
var ignoreBackgroundCheckbox = group.add("checkbox", undefined, " Ignore background layer");
|
||||
ignoreBackgroundCheckbox.value = settings.ignoreBackground;
|
||||
var trimWhitespaceCheckbox = group.add("checkbox", undefined, " Trim whitespace");
|
||||
trimWhitespaceCheckbox.value = settings.trimWhitespace;
|
||||
group = checkboxGroup.add("group");
|
||||
group.orientation = "column";
|
||||
group.alignChildren = ["left", ""];
|
||||
group.alignment = ["", "top"];
|
||||
var writeJsonCheckbox = group.add("checkbox", undefined, " Write Spine JSON");
|
||||
writeJsonCheckbox.value = settings.writeJson;
|
||||
var writeTemplateCheckbox = group.add("checkbox", undefined, " Write template image");
|
||||
writeTemplateCheckbox.value = settings.writeTemplate;
|
||||
var scaleText, paddingText, scaleSlider, paddingSlider;
|
||||
if (!cs2) {
|
||||
var slidersGroup = settingsGroup.add("group");
|
||||
group = slidersGroup.add("group");
|
||||
group.orientation = "column";
|
||||
group.alignChildren = ["right", ""];
|
||||
group.add("statictext", undefined, "Scale:");
|
||||
group.add("statictext", undefined, "Padding:");
|
||||
group = slidersGroup.add("group");
|
||||
group.orientation = "column";
|
||||
scaleText = group.add("edittext", undefined, settings.scale * 100);
|
||||
scaleText.characters = 4;
|
||||
paddingText = group.add("edittext", undefined, settings.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.orientation = "column";
|
||||
group.alignChildren = ["fill", ""];
|
||||
group.alignment = ["fill", ""];
|
||||
scaleSlider = group.add("slider", undefined, settings.scale * 100, 1, 100);
|
||||
paddingSlider = group.add("slider", undefined, settings.padding, 0, 4);
|
||||
} else {
|
||||
group = settingsGroup.add("group");
|
||||
group.add("statictext", undefined, "Scale:");
|
||||
scaleText = group.add("edittext", undefined, settings.scale * 100);
|
||||
scaleText.preferredSize.width = 50;
|
||||
scaleSlider = settingsGroup.add("slider", undefined, settings.scale * 100, 1, 100);
|
||||
group = settingsGroup.add("group");
|
||||
group.add("statictext", undefined, "Padding:");
|
||||
paddingText = group.add("edittext", undefined, settings.padding);
|
||||
paddingText.preferredSize.width = 50;
|
||||
paddingSlider = settingsGroup.add("slider", undefined, settings.padding, 0, 4);
|
||||
}
|
||||
|
||||
var outputPathGroup = dialog.add("panel", undefined, "Output Paths");
|
||||
outputPathGroup.alignChildren = ["fill", ""];
|
||||
outputPathGroup.margins = [10,15,10,10];
|
||||
var imagesDirText, imagesDirPreview, jsonPathText, jsonPathPreview;
|
||||
if (!cs2) {
|
||||
var textGroup = outputPathGroup.add("group");
|
||||
textGroup.orientation = "column";
|
||||
textGroup.alignChildren = ["fill", ""];
|
||||
group = textGroup.add("group");
|
||||
group.add("statictext", undefined, "Images:");
|
||||
imagesDirText = group.add("edittext", undefined, settings.imagesDir);
|
||||
imagesDirText.alignment = ["fill", ""];
|
||||
imagesDirPreview = textGroup.add("statictext", undefined, "");
|
||||
imagesDirPreview.maximumSize.width = 260;
|
||||
group = textGroup.add("group");
|
||||
var jsonLabel = group.add("statictext", undefined, "JSON:");
|
||||
jsonLabel.justify = "right";
|
||||
jsonLabel.minimumSize.width = 41;
|
||||
jsonPathText = group.add("edittext", undefined, settings.jsonPath);
|
||||
jsonPathText.alignment = ["fill", ""];
|
||||
jsonPathPreview = textGroup.add("statictext", undefined, "");
|
||||
jsonPathPreview.maximumSize.width = 260;
|
||||
} else {
|
||||
outputPathGroup.add("statictext", undefined, "Images:");
|
||||
imagesDirText = outputPathGroup.add("edittext", undefined, settings.imagesDir);
|
||||
imagesDirText.alignment = "fill";
|
||||
outputPathGroup.add("statictext", undefined, "JSON:");
|
||||
jsonPathText = outputPathGroup.add("edittext", undefined, settings.jsonPath);
|
||||
jsonPathText.alignment = "fill";
|
||||
}
|
||||
var buttonGroup = dialog.add("group");
|
||||
var helpButton;
|
||||
if (!cs2) helpButton = buttonGroup.add("button", undefined, "Help");
|
||||
group = buttonGroup.add("group");
|
||||
group.alignment = ["fill", ""];
|
||||
group.alignChildren = ["right", ""];
|
||||
var runButton = group.add("button", undefined, "OK");
|
||||
var cancelButton = group.add("button", undefined, "Cancel");
|
||||
|
||||
// Tooltips.
|
||||
writeTemplateCheckbox.helpTip = "When checked, a PNG is written for the currently visible layers.";
|
||||
writeJsonCheckbox.helpTip = "When checked, a Spine JSON file is written.";
|
||||
trimWhitespaceCheckbox.helpTip = "When checked, blank pixels aroind the edges of each image are removed.";
|
||||
scaleSlider.helpTip = "Scales the PNG files. Useful when using higher resolution art in Photoshop than in Spine.";
|
||||
paddingSlider.helpTip = "Blank pixels around the edge of each image. Can avoid aliasing artifacts for opaque pixels along the image edge.";
|
||||
imagesDirText.helpTip = "The folder to write PNGs. Begin with \"./\" to be relative to the PSD file. Blank to disable writing PNGs.";
|
||||
jsonPathText.helpTip = "Output JSON file if ending with \".json\", else the folder to write the JSON file. Begin with \"./\" to be relative to the PSD file. Blank to disable writing a JSON file.";
|
||||
|
||||
// Events.
|
||||
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); };
|
||||
cancelButton.onClick = function () {
|
||||
cancel = true;
|
||||
dialog.close();
|
||||
return;
|
||||
};
|
||||
if (!cs2) helpButton.onClick = showHelpDialog;
|
||||
jsonPathText.onChanging = function () {
|
||||
var text = jsonPathText.text ? jsonPath(jsonPathText.text) : "<no JSON output>";
|
||||
if (!cs2) {
|
||||
jsonPathPreview.text = text;
|
||||
jsonPathPreview.helpTip = text;
|
||||
} else
|
||||
jsonPathText.helpTip = text;
|
||||
};
|
||||
imagesDirText.onChanging = function () {
|
||||
var text = imagesDirText.text ? absolutePath(imagesDirText.text) : "<no image output>";
|
||||
if (!cs2) {
|
||||
imagesDirPreview.text = text;
|
||||
imagesDirPreview.helpTip = text;
|
||||
} else
|
||||
imagesDirText.helpTip = text;
|
||||
};
|
||||
|
||||
// Run now.
|
||||
jsonPathText.onChanging();
|
||||
imagesDirText.onChanging();
|
||||
|
||||
function updateSettings () {
|
||||
settings.ignoreHiddenLayers = ignoreHiddenLayersCheckbox.value;
|
||||
settings.ignoreBackground = ignoreBackgroundCheckbox.value;
|
||||
settings.writeTemplate = writeTemplateCheckbox.value;
|
||||
settings.writeJson = writeJsonCheckbox.value;
|
||||
settings.trimWhitespace = trimWhitespaceCheckbox.value;
|
||||
|
||||
var scaleValue = parseFloat(scaleText.text);
|
||||
if (scaleValue > 0 && scaleValue <= 100) settings.scale = scaleValue / 100;
|
||||
|
||||
settings.imagesDir = imagesDirText.text;
|
||||
settings.jsonPath = jsonPathText.text;
|
||||
|
||||
var paddingValue = parseInt(paddingText.text);
|
||||
if (paddingValue >= 0) settings.padding = paddingValue;
|
||||
}
|
||||
|
||||
runButton.onClick = function () {
|
||||
if (scaleText.text <= 0 || scaleText.text > 100) {
|
||||
alert("Scale must be between > 0 and <= 100.");
|
||||
return;
|
||||
}
|
||||
if (paddingText.text < 0) {
|
||||
alert("Padding must be >= 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
updateSettings();
|
||||
saveSettings();
|
||||
|
||||
ignoreHiddenLayersCheckbox.enabled = false;
|
||||
ignoreBackgroundCheckbox.enabled = false;
|
||||
writeTemplateCheckbox.enabled = false;
|
||||
writeJsonCheckbox.enabled = false;
|
||||
trimWhitespaceCheckbox.enabled = false;
|
||||
scaleText.enabled = false;
|
||||
scaleSlider.enabled = false;
|
||||
paddingText.enabled = false;
|
||||
paddingSlider.enabled = false;
|
||||
imagesDirText.enabled = false;
|
||||
jsonPathText.enabled = false;
|
||||
if (!cs2) helpButton.enabled = false;
|
||||
runButton.enabled = false;
|
||||
cancelButton.enabled = false;
|
||||
|
||||
var rulerUnits = app.preferences.rulerUnits;
|
||||
app.preferences.rulerUnits = Units.PIXELS;
|
||||
try {
|
||||
// var start = new Date().getTime();
|
||||
run();
|
||||
// alert(new Date().getTime() - start);
|
||||
} catch (e) {
|
||||
alert("An unexpected error has occurred.\n\nTo debug, run the PhotoshopToSpine 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.close();
|
||||
}
|
||||
};
|
||||
|
||||
dialog.center();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
function loadSettings () {
|
||||
var options = null;
|
||||
try {
|
||||
options = app.getCustomOptions(sID("settings"));
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
var settings = {};
|
||||
for (var key in defaultSettings) {
|
||||
if (!defaultSettings.hasOwnProperty(key)) continue;
|
||||
var typeID = sID(key);
|
||||
if (options && options.hasKey(typeID))
|
||||
settings[key] = options["get" + getOptionType(defaultSettings[key])](typeID);
|
||||
else
|
||||
settings[key] = defaultSettings[key];
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
function saveSettings () {
|
||||
if (cs2) return; // No putCustomOptions.
|
||||
var action = new ActionDescriptor();
|
||||
for (var key in defaultSettings) {
|
||||
if (!defaultSettings.hasOwnProperty(key)) continue;
|
||||
action["put" + getOptionType(defaultSettings[key])](sID(key), settings[key]);
|
||||
}
|
||||
app.putCustomOptions(sID("settings"), action, true);
|
||||
}
|
||||
|
||||
function getOptionType (value) {
|
||||
switch (typeof(value)) {
|
||||
case "boolean": return "Boolean";
|
||||
case "string": return "String";
|
||||
case "number": return "Double";
|
||||
};
|
||||
throw new Error("Invalid default setting: " + value);
|
||||
}
|
||||
|
||||
// Help dialog.
|
||||
|
||||
function showHelpDialog () {
|
||||
var dialog = new Window("dialog", "PhotoshopToSpine - Help");
|
||||
dialog.alignChildren = ["fill", ""];
|
||||
dialog.orientation = "column";
|
||||
dialog.alignment = ["", "top"];
|
||||
|
||||
var helpText = dialog.add("statictext", undefined, ""
|
||||
+ "This script writes layers as images and creates a JSON file to bring the images into Spine in the same positions and draw order as they had in Photoshop.\n"
|
||||
+ "\n"
|
||||
+ "The ruler origin corresponds to 0,0 in Spine.\n"
|
||||
+ "\n"
|
||||
+ "Tags in square brackets can be used in layer and group names to customize the output.\n"
|
||||
+ "\n"
|
||||
+ "Group names:\n"
|
||||
+ "• [bone] Slot and bone layers in the group are placed under a bone, named after the group. The bone is created at the center of a visible attachment.\n"
|
||||
+ "• [slot] Layers in the group are placed in a slot, named after the group.\n"
|
||||
+ "• [skin] Layers in the group are placed in a skin, named after the group. Skin images are output in a subfolder for the skin.\n"
|
||||
+ "• [merge] Layers in the group are merged and a single image is output, named after the group.\n"
|
||||
+ "• [folder] Layers in the group will be output in a subfolder. Folder groups can be nested.\n"
|
||||
+ "• [ignore] Layers in the group and any child groups will not be output.\n"
|
||||
+ "\n"
|
||||
+ "Layer names:\n"
|
||||
+ "• [ignore] The layer will not be output."
|
||||
, {multiline: true});
|
||||
helpText.preferredSize.width = 325;
|
||||
|
||||
var closeButton = dialog.add("button", undefined, "Close");
|
||||
closeButton.alignment = ["center", ""];
|
||||
|
||||
closeButton.onClick = function () {
|
||||
dialog.close();
|
||||
};
|
||||
|
||||
dialog.center();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
// Progress dialog:
|
||||
|
||||
function showProgressDialog () {
|
||||
var dialog = new Window("palette", "PhotoshopToSpine - Processing...");
|
||||
dialog.alignChildren = "fill";
|
||||
dialog.orientation = "column";
|
||||
|
||||
var message = dialog.add("statictext", undefined, "Initializing...");
|
||||
|
||||
var group = dialog.add("group");
|
||||
var bar = group.add("progressbar");
|
||||
bar.preferredSize = [300, 16];
|
||||
bar.maxvalue = 10000;
|
||||
var cancelButton = group.add("button", undefined, "Cancel");
|
||||
|
||||
cancelButton.onClick = function () {
|
||||
cancel = true;
|
||||
cancelButton.enabled = false;
|
||||
return;
|
||||
};
|
||||
|
||||
dialog.center();
|
||||
dialog.show();
|
||||
dialog.active = true;
|
||||
|
||||
progress = {
|
||||
dialog: dialog,
|
||||
bar: bar,
|
||||
message: message
|
||||
};
|
||||
}
|
||||
|
||||
function setProgress (percent, layerName) {
|
||||
progress.bar.value = 10000 * percent;
|
||||
progress.message.text = "Layer: " + layerName;
|
||||
if (!progress.dialog.active) progress.dialog.active = true;
|
||||
}
|
||||
|
||||
// PhotoshopToSpine utility:
|
||||
|
||||
function unlock (layer) {
|
||||
if (layer.allLocked) layer.allLocked = false;
|
||||
if (!layer.layers) return;
|
||||
for (var i = layer.layers.length - 1; i >= 0; i--)
|
||||
unlock(layer.layers[i]);
|
||||
}
|
||||
|
||||
function deleteLayer (layer) {
|
||||
unlock(layer);
|
||||
activeDocument.activeLayer = activeDocument.artLayers[0];
|
||||
layer.remove();
|
||||
}
|
||||
|
||||
function collectLayers (parent, collect) {
|
||||
for (var i = parent.layers.length - 1; i >= 0; i--) {
|
||||
if (cancel) return;
|
||||
var layer = parent.layers[i];
|
||||
if (settings.ignoreHiddenLayers && !layer.visible) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
if (settings.ignoreBackground && layer.isBackgroundLayer) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
if (findTag(layer, "ignore")) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
var group = isGroup(layer);
|
||||
if (!group && layer.bounds[2] == 0 && layer.bounds[3] == 0) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure tags are valid.
|
||||
var re = /\[([^\]]+)\]/g;
|
||||
while (true) {
|
||||
var matches = re.exec(layer.name);
|
||||
if (!matches) break;
|
||||
var tag = matches[1].toLowerCase();
|
||||
if (group) {
|
||||
if (!isValidGroupTag(tag)) {
|
||||
var message = "Invalid group name:\n\n" + layer.name;
|
||||
if (isValidLayerTag(tag))
|
||||
message += "\n\nThe [" + tag + "] tag is only valid for layers, not for groups.";
|
||||
else
|
||||
message += "\n\nThe [" + tag + "] tag is not a valid tag.";
|
||||
alert(message);
|
||||
cancel = true;
|
||||
return;
|
||||
}
|
||||
} else if (!isValidLayerTag(tag)) {
|
||||
var message = "Invalid layer name:\n\n" + layer.name;
|
||||
if (isValidGroupTag(tag))
|
||||
message += "\n\nThe [" + tag + "] tag is only valid for groups, not for layers.";
|
||||
else
|
||||
message += "\n\nThe [" + tag + "] tag is not a valid tag.";
|
||||
alert(message);
|
||||
cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure only one tag.
|
||||
if (layer.name.replace(/\[[^\]]+\]/, "").search(/\[[^\]]+\]/) != -1) {
|
||||
alert("A " + (group ? "group" : "layer") + " name must not have more than one tag:\n" + layer.name);
|
||||
cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var changeVisibility = layer.kind == LayerKind.NORMAL || group;
|
||||
if (changeVisibility) {
|
||||
layer.wasVisible = layer.visible;
|
||||
layer.visible = true;
|
||||
if (layer.allLocked) layer.allLocked = false;
|
||||
}
|
||||
|
||||
if (group && findTag(layer, "merge")) {
|
||||
collectGroupMerge(layer);
|
||||
if (!layer.layers || layer.layers.length == 0) continue;
|
||||
} else if (layer.layers && layer.layers.length > 0) {
|
||||
collectLayers(layer, collect);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (changeVisibility) layer.visible = false;
|
||||
collect.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
function collectGroupMerge (parent) {
|
||||
if (!parent.layers) return;
|
||||
for (var i = parent.layers.length - 1; i >= 0; i--) {
|
||||
var layer = parent.layers[i];
|
||||
if (settings.ignoreHiddenLayers && !layer.visible) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
if (findTag(layer, "ignore")) {
|
||||
deleteLayer(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
collectGroupMerge(layer);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidGroupTag (tag) {
|
||||
switch (tag) {
|
||||
case "bone":
|
||||
case "slot":
|
||||
case "skin":
|
||||
case "merge":
|
||||
case "folder":
|
||||
case "ignore":
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isValidLayerTag (tag) {
|
||||
switch (tag) {
|
||||
case "ignore":
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isGroup (layer) {
|
||||
return layer.typename == "LayerSet";
|
||||
}
|
||||
|
||||
function stripTags (name) {
|
||||
return trim(name.replace(/\[[^\]]+\]/g, ""));
|
||||
}
|
||||
|
||||
function findTagLayer (layer, tag) {
|
||||
while (layer) {
|
||||
if (tag == "ignore" || isGroup(layer)) { // Non-group layers can only have ignore tag.
|
||||
if (layer.name.toLowerCase().indexOf("[" + tag + "]") != -1) return layer;
|
||||
}
|
||||
layer = layer.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTag (layer, tag, otherwise) {
|
||||
var found = findTagLayer(layer, tag);
|
||||
return found ? stripTags(found.name) : otherwise;
|
||||
}
|
||||
|
||||
function jsonPath (jsonPath) {
|
||||
if (endsWith(jsonPath, ".json")) {
|
||||
var index = jsonPath.replace("\\", "/").lastIndexOf("/");
|
||||
if (index != -1) return absolutePath(jsonPath.slice(0, index + 1)) + jsonPath.slice(index + 1);
|
||||
return absolutePath("./") + jsonPath;
|
||||
}
|
||||
var name = decodeURI(originalDoc.name);
|
||||
return absolutePath(jsonPath) + name.substring(0, name.indexOf(".")) + ".json";
|
||||
}
|
||||
|
||||
function folders (layer, path) {
|
||||
var folderLayer = findTagLayer(layer, "folder");
|
||||
return folderLayer ? folders(folderLayer.parent, stripTags(folderLayer.name) + "/" + path) : path;
|
||||
}
|
||||
|
||||
// Photoshop utility:
|
||||
|
||||
function scaleImage () {
|
||||
var imageSize = activeDocument.width.as("px") * settings.scale;
|
||||
activeDocument.resizeImage(UnitValue(imageSize, "px"), null, null, ResampleMethod.BICUBICSHARPER);
|
||||
}
|
||||
|
||||
var history;
|
||||
function storeHistory () {
|
||||
history = activeDocument.activeHistoryState;
|
||||
}
|
||||
function restoreHistory () {
|
||||
activeDocument.activeHistoryState = history;
|
||||
}
|
||||
|
||||
function scriptDir () {
|
||||
var file;
|
||||
if (!cs2)
|
||||
file = $.fileName;
|
||||
else {
|
||||
try {
|
||||
var error = THROW_ERROR; // Force error which provides the script file name.
|
||||
} catch (ex) {
|
||||
file = ex.fileName;
|
||||
}
|
||||
}
|
||||
return new File(file).parent + "/";
|
||||
}
|
||||
|
||||
function hasFilePath () {
|
||||
var action = new ActionReference();
|
||||
action.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt"));
|
||||
return executeActionGet(action).hasKey(sID("fileReference"));
|
||||
}
|
||||
|
||||
function absolutePath (path) {
|
||||
path = trim(path);
|
||||
if (!startsWith(path, "./")) {
|
||||
var absolute = decodeURI(new File(path).absoluteURI);
|
||||
if (!startsWith(absolute, decodeURI(new File("child").parent.absoluteURI))) return absolute + "/";
|
||||
path = "./" + path;
|
||||
}
|
||||
if (path.length == 0)
|
||||
path = decodeURI(activeDocument.path);
|
||||
else if (startsWith(settings.imagesDir, "./"))
|
||||
path = decodeURI(activeDocument.path) + path.substring(1);
|
||||
path = (new File(path).fsName).toString();
|
||||
path = path.replace(/\\/g, "/");
|
||||
if (path.substring(path.length - 1) != "/") path += "/";
|
||||
return path;
|
||||
}
|
||||
|
||||
function cID (id) {
|
||||
return charIDToTypeID(id);
|
||||
}
|
||||
|
||||
function sID (id) {
|
||||
return stringIDToTypeID(id);
|
||||
}
|
||||
|
||||
function bgColor (control, r, g, b) {
|
||||
control.graphics.backgroundColor = control.graphics.newBrush(control.graphics.BrushType.SOLID_COLOR, [r, g, b]);
|
||||
}
|
||||
|
||||
function deselectLayers () {
|
||||
var desc = new ActionDescriptor();
|
||||
var ref = new ActionReference();
|
||||
ref.putEnumerated(cID("Lyr "), cID("Ordn"), cID("Trgt"));
|
||||
desc.putReference(cID("null"), ref);
|
||||
executeAction(sID("selectNoLayers"), desc, DialogModes.NO);
|
||||
}
|
||||
|
||||
function convertToRGB () {
|
||||
var desc = new ActionDescriptor();
|
||||
desc.putClass(cID("T "), cID("RGBM"));
|
||||
desc.putBoolean(cID("Mrge"), false);
|
||||
desc.putBoolean(cID("Rstr"), true);
|
||||
executeAction(cID("CnvM"), desc, DialogModes.NO);
|
||||
}
|
||||
|
||||
function savePNG (file) {
|
||||
var options = new PNGSaveOptions();
|
||||
options.compression = 9;
|
||||
activeDocument.saveAs(file, options, true, Extension.LOWERCASE);
|
||||
}
|
||||
|
||||
// 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 startsWith (str, prefix) {
|
||||
return str.indexOf(prefix) === 0;
|
||||
}
|
||||
|
||||
function endsWith (str, suffix) {
|
||||
return str.indexOf(suffix, str.length - suffix.length) !== -1;
|
||||
}
|
||||
|
||||
function quote (value) {
|
||||
return '"' + value.replace('"', '\\"') + '"';
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
#target photoshop
|
||||
|
||||
/*
|
||||
Export art layers with optional scope, format and trimming settings.
|
||||
- Can export every art layer or only layers currently visible (including parent visibility).
|
||||
- Supports PNG and JPEG with adjustable JPEG quality (0-100).
|
||||
- Can keep the full document canvas or trim transparent edges for each layer.
|
||||
- Does not add prefixes or four-digit sequence numbers.
|
||||
- Works on a duplicate, so the source document and its visibility states stay unchanged.
|
||||
- Nested art layers are supported; layer groups themselves are not exported.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
if (app.documents.length === 0) {
|
||||
alert("请先打开一个 PSD/PSB 文档。");
|
||||
return;
|
||||
}
|
||||
|
||||
function pad2(value) {
|
||||
return value < 10 ? "0" + value : String(value);
|
||||
}
|
||||
|
||||
function timeStamp() {
|
||||
var now = new Date();
|
||||
return now.getFullYear() +
|
||||
pad2(now.getMonth() + 1) +
|
||||
pad2(now.getDate()) + "_" +
|
||||
pad2(now.getHours()) +
|
||||
pad2(now.getMinutes()) +
|
||||
pad2(now.getSeconds());
|
||||
}
|
||||
|
||||
function safeName(name) {
|
||||
var value = String(name);
|
||||
value = value.replace(/\.(png|jpe?g)$/i, "");
|
||||
value = value.replace(/[\\\/:*?"<>|]/g, "_");
|
||||
value = value.replace(/^\s+|\s+$/g, "");
|
||||
value = value.replace(/[\. ]+$/g, "");
|
||||
return value.length > 0 ? value : "unnamed_layer";
|
||||
}
|
||||
|
||||
function showOptionsDialog() {
|
||||
var dialog = new Window("dialog", "图层导出选项");
|
||||
dialog.orientation = "column";
|
||||
dialog.alignChildren = "fill";
|
||||
|
||||
var scopePanel = dialog.add("panel", undefined, "导出范围");
|
||||
scopePanel.orientation = "column";
|
||||
scopePanel.alignChildren = "left";
|
||||
scopePanel.margins = 12;
|
||||
var visibleOnly = scopePanel.add("checkbox", undefined, "仅导出当前可见图层");
|
||||
visibleOnly.value = true;
|
||||
scopePanel.add("statictext", undefined, "未勾选时导出所有普通图层");
|
||||
|
||||
var formatPanel = dialog.add("panel", undefined, "文件格式");
|
||||
formatPanel.orientation = "column";
|
||||
formatPanel.alignChildren = "left";
|
||||
formatPanel.margins = 12;
|
||||
|
||||
var formatChoices = formatPanel.add("group");
|
||||
formatChoices.orientation = "row";
|
||||
var pngRadio = formatChoices.add("radiobutton", undefined, "PNG");
|
||||
var jpegRadio = formatChoices.add("radiobutton", undefined, "JPEG");
|
||||
pngRadio.value = true;
|
||||
|
||||
var qualityGroup = formatPanel.add("group");
|
||||
qualityGroup.orientation = "row";
|
||||
qualityGroup.alignChildren = ["left", "center"];
|
||||
var qualityLabel = qualityGroup.add("statictext", undefined, "JPEG 质量:");
|
||||
var qualitySlider = qualityGroup.add("slider", undefined, 90, 0, 100);
|
||||
qualitySlider.preferredSize.width = 180;
|
||||
var qualityValue = qualityGroup.add("statictext", undefined, "90");
|
||||
qualityValue.characters = 3;
|
||||
|
||||
function setQualityEnabled(enabled) {
|
||||
qualityLabel.enabled = enabled;
|
||||
qualitySlider.enabled = enabled;
|
||||
qualityValue.enabled = enabled;
|
||||
}
|
||||
|
||||
qualitySlider.onChanging = function () {
|
||||
qualityValue.text = String(Math.round(qualitySlider.value));
|
||||
};
|
||||
qualitySlider.onChange = qualitySlider.onChanging;
|
||||
pngRadio.onClick = function () {
|
||||
setQualityEnabled(false);
|
||||
};
|
||||
jpegRadio.onClick = function () {
|
||||
setQualityEnabled(true);
|
||||
};
|
||||
setQualityEnabled(false);
|
||||
|
||||
var trimPanel = dialog.add("panel", undefined, "画布");
|
||||
trimPanel.orientation = "column";
|
||||
trimPanel.alignChildren = "left";
|
||||
trimPanel.margins = 12;
|
||||
var trimOutput = trimPanel.add("checkbox", undefined, "导出时裁切透明边缘");
|
||||
trimOutput.value = true;
|
||||
trimPanel.add("statictext", undefined, "未勾选时保留原文档画布尺寸");
|
||||
|
||||
var buttons = dialog.add("group");
|
||||
buttons.alignment = "right";
|
||||
buttons.add("button", undefined, "取消", {name: "cancel"});
|
||||
buttons.add("button", undefined, "继续", {name: "ok"});
|
||||
|
||||
if (dialog.show() !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
visibleOnly: visibleOnly.value,
|
||||
format: jpegRadio.value ? "jpeg" : "png",
|
||||
jpegQuality: Math.round(qualitySlider.value),
|
||||
trim: trimOutput.value
|
||||
};
|
||||
}
|
||||
|
||||
function collectArtLayers(container, result) {
|
||||
for (var i = 0; i < container.layers.length; i++) {
|
||||
var layer = container.layers[i];
|
||||
if (layer.typename === "ArtLayer") {
|
||||
result.push(layer);
|
||||
} else if (layer.typename === "LayerSet") {
|
||||
collectArtLayers(layer, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEffectivelyVisible(layer) {
|
||||
if (!layer.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var parent = layer.parent;
|
||||
while (parent && parent.typename === "LayerSet") {
|
||||
if (!parent.visible) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideAllArtLayers(layers) {
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
try {
|
||||
layers[i].visible = false;
|
||||
} catch (ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
function showLayerAndParents(layer) {
|
||||
layer.visible = true;
|
||||
var parent = layer.parent;
|
||||
while (parent && parent.typename === "LayerSet") {
|
||||
parent.visible = true;
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
function createUniqueOutputFolder(parent, documentName, formatLabel) {
|
||||
var baseName = safeName(documentName.replace(/\.[^\.]+$/, "")) + "_" + formatLabel;
|
||||
var folder = new Folder(parent.fsName + "/" + baseName);
|
||||
if (folder.exists) {
|
||||
folder = new Folder(parent.fsName + "/" + baseName + "_" + timeStamp());
|
||||
}
|
||||
if (!folder.create()) {
|
||||
throw new Error("无法创建输出文件夹:" + folder.fsName);
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
function createExportOptions(format, jpegQuality) {
|
||||
var exportOptions = new ExportOptionsSaveForWeb();
|
||||
exportOptions.includeProfile = false;
|
||||
|
||||
if (format === "jpeg") {
|
||||
exportOptions.format = SaveDocumentType.JPEG;
|
||||
exportOptions.quality = jpegQuality;
|
||||
exportOptions.optimized = true;
|
||||
} else {
|
||||
exportOptions.format = SaveDocumentType.PNG;
|
||||
exportOptions.PNG8 = false;
|
||||
exportOptions.transparency = true;
|
||||
exportOptions.interlaced = false;
|
||||
}
|
||||
|
||||
return exportOptions;
|
||||
}
|
||||
|
||||
var sourceDoc = app.activeDocument;
|
||||
var settings = showOptionsDialog();
|
||||
if (settings === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var formatLabel = settings.format === "jpeg" ? "JPEG" : "PNG";
|
||||
var extension = settings.format === "jpeg" ? ".jpg" : ".png";
|
||||
var parentFolder = Folder.selectDialog("选择 " + formatLabel + " 导出目录");
|
||||
if (parentFolder === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldDialogs = app.displayDialogs;
|
||||
var workDoc = null;
|
||||
|
||||
try {
|
||||
app.displayDialogs = DialogModes.NO;
|
||||
workDoc = sourceDoc.duplicate();
|
||||
app.activeDocument = workDoc;
|
||||
|
||||
var allLayers = [];
|
||||
var layers = [];
|
||||
collectArtLayers(workDoc, allLayers);
|
||||
|
||||
for (var i = 0; i < allLayers.length; i++) {
|
||||
if (!settings.visibleOnly || isEffectivelyVisible(allLayers[i])) {
|
||||
layers.push(allLayers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (layers.length === 0) {
|
||||
if (settings.visibleOnly) {
|
||||
throw new Error("文档中没有当前可见的普通图层。");
|
||||
}
|
||||
throw new Error("文档中没有可导出的普通图层。");
|
||||
}
|
||||
|
||||
var outputFolder = createUniqueOutputFolder(parentFolder, sourceDoc.name, formatLabel);
|
||||
var exportOptions = createExportOptions(settings.format, settings.jpegQuality);
|
||||
|
||||
var usedNames = {};
|
||||
hideAllArtLayers(allLayers);
|
||||
|
||||
for (i = 0; i < layers.length; i++) {
|
||||
hideAllArtLayers(allLayers);
|
||||
showLayerAndParents(layers[i]);
|
||||
|
||||
var baseName = safeName(layers[i].name);
|
||||
var key = baseName.toLowerCase();
|
||||
if (usedNames[key] === undefined) {
|
||||
usedNames[key] = 1;
|
||||
} else {
|
||||
usedNames[key]++;
|
||||
baseName += "_" + usedNames[key];
|
||||
}
|
||||
|
||||
var preTrimState = null;
|
||||
if (settings.trim) {
|
||||
preTrimState = workDoc.activeHistoryState;
|
||||
try {
|
||||
workDoc.trim(TrimType.TRANSPARENT, true, true, true, true);
|
||||
} catch (ignoreTrim) {
|
||||
// Empty/adjustment-only layers may not contain trim-able pixels.
|
||||
}
|
||||
}
|
||||
|
||||
var outputFile = new File(outputFolder.fsName + "/" + baseName + extension);
|
||||
workDoc.exportDocument(outputFile, ExportType.SAVEFORWEB, exportOptions);
|
||||
|
||||
if (preTrimState !== null) {
|
||||
try {
|
||||
workDoc.activeHistoryState = preTrimState;
|
||||
} catch (ignoreHistory) {}
|
||||
}
|
||||
}
|
||||
|
||||
workDoc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
workDoc = null;
|
||||
app.activeDocument = sourceDoc;
|
||||
app.displayDialogs = oldDialogs;
|
||||
|
||||
alert(
|
||||
"导出完成:" + layers.length + " 个 " + formatLabel + "\n\n" +
|
||||
"范围 = " + (settings.visibleOnly ? "仅当前可见图层" : "所有普通图层") + "\n" +
|
||||
(settings.format === "jpeg" ? "JPEG 质量 = " + settings.jpegQuality + "\n" : "") +
|
||||
"文件名 = 图层名\n" +
|
||||
"尺寸 = " + (settings.trim ? "已裁切透明边缘" : "原文档画布尺寸") + "\n\n" +
|
||||
"输出目录:\n" + outputFolder.fsName
|
||||
);
|
||||
} catch (error) {
|
||||
if (workDoc !== null) {
|
||||
try {
|
||||
workDoc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
} catch (ignoreClose) {}
|
||||
}
|
||||
try {
|
||||
app.activeDocument = sourceDoc;
|
||||
} catch (ignoreActivate) {}
|
||||
app.displayDialogs = oldDialogs;
|
||||
alert("导出失败:\n" + error.message);
|
||||
}
|
||||
}());
|
||||
@@ -0,0 +1,385 @@
|
||||
#target photoshop
|
||||
|
||||
/*
|
||||
* 重命名图层 - 独立版
|
||||
*
|
||||
* 使用方法:
|
||||
* Photoshop -> 文件 -> 脚本 -> 浏览,选择本文件。
|
||||
*
|
||||
* 支持规则:
|
||||
* %n 正序编号:1、2、3...
|
||||
* %%n 倒序编号
|
||||
* %nn 两位编号:01、02、03...
|
||||
* %n### 编号 + 原图层名
|
||||
* %%n### 倒序编号 + 原图层名
|
||||
* %n=5 从 5 开始编号
|
||||
* 旧文字=>新文字 替换名称中的第一个匹配文本
|
||||
*
|
||||
* CEP 调用:
|
||||
* 先把 CMM_RENAME_AUTO_RUN 改为 false,加载本文件后调用:
|
||||
* cmm_batchName("%n###", true, true);
|
||||
* 第三个参数控制是否重命名图层组,省略时默认为 true。
|
||||
*/
|
||||
|
||||
// 独立运行时设为 true;作为 CEP 函数库加载时改为 false。
|
||||
var CMM_RENAME_AUTO_RUN = true;
|
||||
|
||||
// suspendHistory 使用的全局参数。
|
||||
var CMM_RENAME_EXPRESSION = "";
|
||||
var CMM_RENAME_FILTER_SUBLAYERS = true;
|
||||
var CMM_RENAME_INCLUDE_GROUPS = true;
|
||||
|
||||
function cmm_s2t(value) {
|
||||
return stringIDToTypeID(value);
|
||||
}
|
||||
|
||||
function cmm_c2t(value) {
|
||||
return charIDToTypeID(value);
|
||||
}
|
||||
|
||||
function cmm_getActiveLayerId() {
|
||||
var ref = new ActionReference();
|
||||
ref.putProperty(cmm_s2t("property"), cmm_s2t("layerID"));
|
||||
ref.putEnumerated(
|
||||
cmm_s2t("layer"),
|
||||
cmm_s2t("ordinal"),
|
||||
cmm_s2t("targetEnum")
|
||||
);
|
||||
|
||||
return executeActionGet(ref).getInteger(cmm_s2t("layerID"));
|
||||
}
|
||||
|
||||
function cmm_getSelectedLayerIds() {
|
||||
var ids = [];
|
||||
var ref = new ActionReference();
|
||||
|
||||
ref.putProperty(cmm_c2t("Prpr"), cmm_s2t("targetLayers"));
|
||||
ref.putEnumerated(cmm_c2t("Dcmn"), cmm_c2t("Ordn"), cmm_c2t("Trgt"));
|
||||
|
||||
var desc = executeActionGet(ref);
|
||||
|
||||
// Photoshop 单选图层时,部分版本不返回 targetLayers。
|
||||
if (!desc.hasKey(cmm_s2t("targetLayers"))) {
|
||||
ids.push(cmm_getActiveLayerId());
|
||||
return ids;
|
||||
}
|
||||
|
||||
var list = desc.getList(cmm_s2t("targetLayers"));
|
||||
var backgroundOffset = 0;
|
||||
|
||||
try {
|
||||
if (app.activeDocument.backgroundLayer) {
|
||||
backgroundOffset = -1;
|
||||
}
|
||||
} catch (ignoreBackgroundError) {
|
||||
backgroundOffset = 0;
|
||||
}
|
||||
|
||||
for (var i = 0; i < list.count; i += 1) {
|
||||
var layerIndex = list.getReference(i).getIndex(cmm_c2t("Lyr ")) + 1;
|
||||
var layerRef = new ActionReference();
|
||||
|
||||
layerRef.putProperty(cmm_c2t("Prpr"), cmm_c2t("LyrI"));
|
||||
layerRef.putIndex(
|
||||
cmm_c2t("Lyr "),
|
||||
layerIndex + backgroundOffset
|
||||
);
|
||||
|
||||
var layerDesc = executeActionGet(layerRef);
|
||||
ids.push(layerDesc.getInteger(cmm_c2t("LyrI")));
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
function cmm_selectLayerById(layerId, addToSelection) {
|
||||
var ref = new ActionReference();
|
||||
var desc = new ActionDescriptor();
|
||||
|
||||
ref.putIdentifier(cmm_c2t("Lyr "), layerId);
|
||||
desc.putReference(cmm_c2t("null"), ref);
|
||||
desc.putBoolean(cmm_c2t("MkVs"), false);
|
||||
|
||||
if (addToSelection) {
|
||||
desc.putEnumerated(
|
||||
cmm_s2t("selectionModifier"),
|
||||
cmm_s2t("selectionModifierType"),
|
||||
cmm_s2t("addToSelection")
|
||||
);
|
||||
}
|
||||
|
||||
executeAction(cmm_c2t("slct"), desc, DialogModes.NO);
|
||||
}
|
||||
|
||||
function cmm_restoreLayerSelection(ids) {
|
||||
if (!ids || ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
cmm_selectLayerById(ids[0], false);
|
||||
|
||||
for (var i = 1; i < ids.length; i += 1) {
|
||||
cmm_selectLayerById(ids[i], true);
|
||||
}
|
||||
}
|
||||
|
||||
function cmm_indexOf(arr, value) {
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
if (arr[i] === value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 根据选项排除图层组。普通图层和图层组都使用 Photoshop 图层 ID,
|
||||
* 因此必须选中目标后通过 typename 判断其类型。
|
||||
*/
|
||||
function cmm_filterLayerGroups(ids, includeLayerGroups) {
|
||||
if (includeLayerGroups) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
var result = [];
|
||||
|
||||
for (var i = 0; i < ids.length; i += 1) {
|
||||
cmm_selectLayerById(ids[i], false);
|
||||
|
||||
if (app.activeDocument.activeLayer.typename !== "LayerSet") {
|
||||
result.push(ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* 如果父图层组和它的子图层同时被选中,只保留父图层组。
|
||||
*/
|
||||
function cmm_filterSubLayer(ids) {
|
||||
var result = [];
|
||||
|
||||
for (var i = 0; i < ids.length; i += 1) {
|
||||
cmm_selectLayerById(ids[i], false);
|
||||
|
||||
var parent = app.activeDocument.activeLayer.parent;
|
||||
var isChildOfSelectedGroup = false;
|
||||
|
||||
while (parent && parent.typename !== "Document") {
|
||||
if (cmm_indexOf(ids, parent.id) !== -1) {
|
||||
isChildOfSelectedGroup = true;
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (!isChildOfSelectedGroup) {
|
||||
result.push(ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function cmm_isBlank(value) {
|
||||
return value === undefined ||
|
||||
value === null ||
|
||||
String(value).replace(/\s/g, "") === "";
|
||||
}
|
||||
|
||||
/*
|
||||
* 核心函数。函数名和原插件保持一致,方便 CEP 直接调用。
|
||||
*/
|
||||
function cmm_batchName(expression, filterSubLayers, includeLayerGroups) {
|
||||
if (app.documents.length === 0) {
|
||||
throw new Error("请先打开一个 Photoshop 文档");
|
||||
}
|
||||
|
||||
if (cmm_isBlank(expression)) {
|
||||
throw new Error("重命名规则不能为空");
|
||||
}
|
||||
|
||||
var rule = String(expression);
|
||||
var ids = cmm_getSelectedLayerIds();
|
||||
|
||||
// 为兼容原来的两个参数调用,第三个参数省略时默认包含图层组。
|
||||
if (includeLayerGroups === undefined) {
|
||||
includeLayerGroups = true;
|
||||
} else {
|
||||
includeLayerGroups = Boolean(includeLayerGroups);
|
||||
}
|
||||
|
||||
// 先排除图层组。这样关闭“重命名图层组”时,选中的普通子图层
|
||||
// 不会因为其父图层组也被选中而被过滤掉。
|
||||
ids = cmm_filterLayerGroups(ids, includeLayerGroups);
|
||||
|
||||
if (filterSubLayers && includeLayerGroups) {
|
||||
ids = cmm_filterSubLayer(ids);
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
throw new Error("当前选择中没有符合选项的可重命名图层");
|
||||
}
|
||||
|
||||
var reverse = rule.indexOf("%%") !== -1;
|
||||
if (reverse) {
|
||||
rule = rule.replace("%%", "%");
|
||||
}
|
||||
|
||||
// %n=5、%nn=5、%%n=5 均支持;默认从 1 开始。
|
||||
var startNumber = 1;
|
||||
var startMatch = rule.match(/n=(-?\d+)/);
|
||||
|
||||
if (startMatch) {
|
||||
startNumber = parseInt(startMatch[1], 10);
|
||||
|
||||
if (isNaN(startNumber)) {
|
||||
startNumber = 1;
|
||||
}
|
||||
|
||||
rule = rule.replace("=" + startMatch[1], "");
|
||||
}
|
||||
|
||||
try {
|
||||
for (var i = 0; i < ids.length; i += 1) {
|
||||
cmm_selectLayerById(ids[i], false);
|
||||
|
||||
var activeLayer = app.activeDocument.activeLayer;
|
||||
var oldName = activeLayer.name;
|
||||
var number;
|
||||
|
||||
if (reverse) {
|
||||
number = startNumber + ids.length - 1 - i;
|
||||
} else {
|
||||
number = startNumber + i;
|
||||
}
|
||||
|
||||
var newName = rule.replace("###", oldName);
|
||||
|
||||
if (rule.indexOf("%nn") !== -1) {
|
||||
var paddedNumber = number;
|
||||
|
||||
if (number >= 0 && number < 10) {
|
||||
paddedNumber = "0" + number;
|
||||
}
|
||||
|
||||
newName = newName.replace("%nn", paddedNumber);
|
||||
} else if (rule.indexOf("%n") !== -1) {
|
||||
newName = newName.replace("%n", number);
|
||||
} else if (rule.indexOf("=>") !== -1) {
|
||||
var separatorIndex = rule.indexOf("=>");
|
||||
var searchText = rule.substring(0, separatorIndex);
|
||||
var replacementText = rule.substring(separatorIndex + 2);
|
||||
|
||||
if (searchText === "") {
|
||||
throw new Error("替换的原文字不能为空");
|
||||
}
|
||||
|
||||
// 与原插件一致:每个图层只替换第一个匹配项。
|
||||
newName = oldName.replace(searchText, replacementText);
|
||||
}
|
||||
|
||||
activeLayer.name = newName;
|
||||
}
|
||||
} finally {
|
||||
cmm_restoreLayerSelection(ids);
|
||||
}
|
||||
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
function cmm_historyRenameRunner() {
|
||||
cmm_batchName(
|
||||
CMM_RENAME_EXPRESSION,
|
||||
CMM_RENAME_FILTER_SUBLAYERS,
|
||||
CMM_RENAME_INCLUDE_GROUPS
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* 把整次批量重命名合并成一条 Photoshop 历史记录。
|
||||
*/
|
||||
function cmm_runBatchName(expression, filterSubLayers, includeLayerGroups) {
|
||||
CMM_RENAME_EXPRESSION = String(expression);
|
||||
CMM_RENAME_FILTER_SUBLAYERS = Boolean(filterSubLayers);
|
||||
CMM_RENAME_INCLUDE_GROUPS = includeLayerGroups === undefined ?
|
||||
true : Boolean(includeLayerGroups);
|
||||
|
||||
app.activeDocument.suspendHistory(
|
||||
"重命名图层",
|
||||
"cmm_historyRenameRunner()"
|
||||
);
|
||||
}
|
||||
|
||||
function cmm_showRenameDialog() {
|
||||
if (app.documents.length === 0) {
|
||||
alert("请先打开一个 Photoshop 文档");
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new Window("dialog", "重命名图层");
|
||||
dialog.orientation = "column";
|
||||
dialog.alignChildren = "fill";
|
||||
|
||||
var ruleGroup = dialog.add("group");
|
||||
ruleGroup.add("statictext", undefined, "规则:");
|
||||
|
||||
var ruleInput = ruleGroup.add("edittext", undefined, "###%%n");
|
||||
ruleInput.characters = 28;
|
||||
|
||||
var groupCheckbox = dialog.add(
|
||||
"checkbox",
|
||||
undefined,
|
||||
"重命名图层组"
|
||||
);
|
||||
groupCheckbox.value = true;
|
||||
|
||||
var filterCheckbox = dialog.add(
|
||||
"checkbox",
|
||||
undefined,
|
||||
"过滤已选父图层组中的子图层"
|
||||
);
|
||||
filterCheckbox.value = false;
|
||||
|
||||
groupCheckbox.onClick = function () {
|
||||
// 不重命名图层组时,所有已选普通图层都会参与重命名,
|
||||
// “过滤子图层”选项不再适用。
|
||||
filterCheckbox.enabled = groupCheckbox.value;
|
||||
};
|
||||
|
||||
var helpText = dialog.add(
|
||||
"statictext",
|
||||
undefined,
|
||||
"%n 正序 %%n 倒序 %nn 两位编号\n%n=5 从5开始编号 ### 原名称\n旧文字=>新文字 可执行替换",
|
||||
{ multiline: true }
|
||||
);
|
||||
helpText.alignment = "left";
|
||||
|
||||
var buttonGroup = dialog.add("group");
|
||||
buttonGroup.alignment = "right";
|
||||
buttonGroup.add("button", undefined, "取消", { name: "cancel" });
|
||||
buttonGroup.add("button", undefined, "确定", { name: "ok" });
|
||||
|
||||
ruleInput.active = true;
|
||||
|
||||
if (dialog.show() !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cmm_runBatchName(
|
||||
ruleInput.text,
|
||||
filterCheckbox.value,
|
||||
groupCheckbox.value
|
||||
);
|
||||
} catch (error) {
|
||||
alert("重命名失败:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (CMM_RENAME_AUTO_RUN) {
|
||||
cmm_showRenameDialog();
|
||||
}
|
||||
Reference in new issue
Block a user