294 lines
10 KiB
React
294 lines
10 KiB
React
#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);
|
|
}
|
|
}());
|