diff --git a/Mac工具/Yeomai-Toolbox-CEP/CSXS/manifest.xml b/Mac工具/Yeomai-Toolbox-CEP/CSXS/manifest.xml new file mode 100644 index 0000000..4601d4a --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/CSXS/manifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + ./index.html + + + true + + + Panel + PS 脚本工具箱 + + + 520 + 320 + + + 260 + 240 + + + 2000 + 1200 + + + + + + + diff --git a/Mac工具/Yeomai-Toolbox-CEP/README_CN.md b/Mac工具/Yeomai-Toolbox-CEP/README_CN.md new file mode 100644 index 0000000..0199abb --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/README_CN.md @@ -0,0 +1,58 @@ +# PS 脚本工具箱(CEP 旧版扩展) + +这是一个可停靠在 Photoshop 侧边栏的 JSX 脚本启动器。 + +当前版本:1.2.0(支持递归子目录和可折叠目录分组) + +已内置: + +- 按图层名导出 PNG +- 自动发现 `jsx/scripts` 文件夹内的所有 `.jsx` +- 递归扫描任意层级子文件夹,并在面板中显示为可折叠目录 +- 搜索脚本 +- 选择并运行任意外部 JSX +- 一键打开脚本文件夹 + +## 安装 + +### Windows + +1. 双击 `enable_debug_windows.reg`,允许写入注册表,然后完全退出 Photoshop。 +2. 将整个 `ps-script-toolbox-cep` 文件夹复制到: + + `%APPDATA%\Adobe\CEP\extensions\` + +3. 重新打开 Photoshop。 +4. 打开 `窗口 → 扩展(旧版)→ PS 脚本工具箱`。 + +### macOS + +1. 在终端运行: + + `chmod +x enable_debug_macos.command && ./enable_debug_macos.command` + +2. 完全退出 Photoshop。 +3. 将整个 `ps-script-toolbox-cep` 文件夹复制到: + + `~/Library/Application Support/Adobe/CEP/extensions/` + +4. 重新打开 Photoshop。 +5. 打开 `窗口 → 扩展(旧版)→ PS 脚本工具箱`。 + +## 添加脚本 + +1. 在面板里点击“打开脚本文件夹”。 +2. 把 `.jsx` 文件复制进去。 +3. 回到面板点击右上角刷新按钮。 + +无需修改面板代码,脚本会自动成为新按钮。可以任意建立子文件夹进行分类;文件名中的下划线会在按钮标题中显示为空格。 + +## 卸载 + +从 CEP `extensions` 目录删除 `ps-script-toolbox-cep` 文件夹,然后重启 Photoshop。 + +## 兼容性 + +- 目标 Photoshop 版本:20.0 及以上。 +- 这是未签名的本地 CEP 扩展,因此首次使用需要启用 PlayerDebugMode。 +- 若菜单中没有“扩展(旧版)”,请先确认你的 Photoshop 版本仍启用了 CEP 支持。 diff --git a/Mac工具/Yeomai-Toolbox-CEP/css/panel.css b/Mac工具/Yeomai-Toolbox-CEP/css/panel.css new file mode 100644 index 0000000..17defb6 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/css/panel.css @@ -0,0 +1,224 @@ +:root { + color-scheme: dark; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; + font-size: 13px; + --bg: #232323; + --surface: #2c2c2c; + --surface-hover: #363636; + --border: #444; + --text: #f1f1f1; + --muted: #aaa; + --accent: #4b9cf5; + --accent-hover: #69adf7; +} + +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: var(--bg); + color: var(--text); +} + +button, +input { + font: inherit; +} + +button { + color: var(--text); + cursor: pointer; +} + +button:disabled { + cursor: wait; + opacity: 0.55; +} + +.app { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 14px; + gap: 12px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.header h1 { + margin: 0; + font-size: 17px; + font-weight: 650; +} + +.header p { + margin: 3px 0 0; + color: var(--muted); + font-size: 11px; +} + +.icon-button, +.secondary-button, +.script-button { + border: 1px solid var(--border); + background: var(--surface); +} + +.icon-button { + width: 32px; + height: 32px; + border-radius: 6px; + font-size: 20px; + line-height: 1; +} + +.icon-button:hover, +.secondary-button:hover, +.script-button:hover { + background: var(--surface-hover); +} + +.toolbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; +} + +#searchInput { + min-width: 0; + height: 32px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 6px; + outline: none; + background: #1e1e1e; + color: var(--text); +} + +#searchInput:focus { + border-color: var(--accent); +} + +.secondary-button { + height: 32px; + padding: 0 10px; + border-radius: 6px; + white-space: nowrap; +} + +.content { + min-height: 0; + flex: 1; + overflow-y: auto; + padding-right: 2px; +} + +.script-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); + gap: 8px; +} + +.script-button { + min-height: 52px; + padding: 9px 10px; + border-radius: 7px; + text-align: left; + word-break: break-word; +} + +.script-button::before { + content: "▶"; + margin-right: 7px; + color: var(--accent); + font-size: 10px; +} + +.folder-group { + grid-column: 1 / -1; + min-width: 0; + border: 1px solid var(--border); + border-radius: 7px; + background: rgba(255, 255, 255, 0.018); +} + +.folder-summary { + padding: 9px 11px; + cursor: pointer; + color: #d8d8d8; + font-weight: 600; + user-select: none; +} + +.folder-summary:hover { + background: var(--surface-hover); +} + +.folder-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); + gap: 8px; + margin: 0 8px 8px 12px; + padding: 8px 0 0 10px; + border-left: 2px solid #3d3d3d; +} + +.folder-content .folder-group { + background: transparent; +} + +.empty-state { + padding: 28px 14px; + border: 1px dashed var(--border); + border-radius: 8px; + color: var(--muted); + text-align: center; + line-height: 1.6; +} + +.footer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.primary-button { + min-height: 36px; + padding: 8px 12px; + border: 0; + border-radius: 6px; + background: var(--accent); + color: #fff; + font-weight: 600; +} + +.primary-button:hover { + background: var(--accent-hover); +} + +.status { + min-height: 16px; + margin: 0; + overflow: hidden; + color: var(--muted); + font-size: 11px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status.error { + color: #ff8a8a; +} diff --git a/Mac工具/Yeomai-Toolbox-CEP/enable_debug_macos.command b/Mac工具/Yeomai-Toolbox-CEP/enable_debug_macos.command new file mode 100755 index 0000000..d8e39d4 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/enable_debug_macos.command @@ -0,0 +1,10 @@ +#!/bin/bash + +defaults write com.adobe.CSXS.9 PlayerDebugMode 1 +defaults write com.adobe.CSXS.10 PlayerDebugMode 1 +defaults write com.adobe.CSXS.11 PlayerDebugMode 1 +defaults write com.adobe.CSXS.12 PlayerDebugMode 1 +defaults write com.adobe.CSXS.13 PlayerDebugMode 1 + +echo "CEP 调试模式已启用。请重启 Photoshop。" + diff --git a/Mac工具/Yeomai-Toolbox-CEP/enable_debug_windows.reg b/Mac工具/Yeomai-Toolbox-CEP/enable_debug_windows.reg new file mode 100644 index 0000000..5e4689a --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/enable_debug_windows.reg @@ -0,0 +1,17 @@ +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Adobe\CSXS.9] +"PlayerDebugMode"="1" + +[HKEY_CURRENT_USER\Software\Adobe\CSXS.10] +"PlayerDebugMode"="1" + +[HKEY_CURRENT_USER\Software\Adobe\CSXS.11] +"PlayerDebugMode"="1" + +[HKEY_CURRENT_USER\Software\Adobe\CSXS.12] +"PlayerDebugMode"="1" + +[HKEY_CURRENT_USER\Software\Adobe\CSXS.13] +"PlayerDebugMode"="1" + diff --git a/Mac工具/Yeomai-Toolbox-CEP/index.html b/Mac工具/Yeomai-Toolbox-CEP/index.html new file mode 100644 index 0000000..1579d48 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/index.html @@ -0,0 +1,39 @@ + + + + + + PS 脚本工具箱 + + + +
+
+
+

脚本工具箱

+

集中运行 Photoshop JSX · v1.2

+
+ +
+ +
+ + +
+ +
+
+ +
+ + +
+ + + + diff --git a/Mac工具/Yeomai-Toolbox-CEP/js/panel.js b/Mac工具/Yeomai-Toolbox-CEP/js/panel.js new file mode 100644 index 0000000..6af2fa9 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/js/panel.js @@ -0,0 +1,312 @@ +(function () { + "use strict"; + + var scriptList = document.getElementById("scriptList"); + var emptyState = document.getElementById("emptyState"); + var statusText = document.getElementById("statusText"); + var searchInput = document.getElementById("searchInput"); + var refreshButton = document.getElementById("refreshButton"); + var openFolderButton = document.getElementById("openFolderButton"); + var runExternalButton = document.getElementById("runExternalButton"); + var scripts = []; + var extensionPathToken = ""; + + function getExtensionPath() { + if (!window.__adobe_cep__ || !window.__adobe_cep__.getSystemPath) { + return ""; + } + + var path = window.__adobe_cep__.getSystemPath("extension") || ""; + path = decodeURI(path); + + if (/^file:\/\/\/[A-Za-z]:/i.test(path)) { + path = path.substring(8); + } else if (/^file:\/\//i.test(path)) { + path = path.substring(7); + } + + return path; + } + + function setStatus(message, isError) { + statusText.textContent = message || ""; + statusText.className = isError ? "status error" : "status"; + statusText.title = message || ""; + } + + function evalHost(code, callback) { + if (!window.__adobe_cep__ || !window.__adobe_cep__.evalScript) { + callback("ERROR|当前页面不在 Photoshop CEP 环境中运行"); + return; + } + window.__adobe_cep__.evalScript(code, callback); + } + + function buildListCode() { + return '(function(){try{' + + 'var folder=new Folder(decodeURIComponent("' + extensionPathToken + '")+"/jsx/scripts");' + + 'if(!folder.exists){return "ERROR|脚本文件夹不存在:"+folder.fsName;}' + + 'var result=[];' + + 'function walk(current,prefix,depth){' + + 'if(depth>20){return;}' + + 'var entries=current.getFiles();' + + 'entries.sort(function(a,b){' + + 'var af=a instanceof Folder;var bf=b instanceof Folder;' + + 'if(af!==bf){return af?-1:1;}' + + 'var left=File.decode(a.displayName).toLowerCase();' + + 'var right=File.decode(b.displayName).toLowerCase();' + + 'return leftright?1:0);' + + '});' + + 'for(var i=0;i= 2) { + result.push({ + label: decodeURIComponent(parts[0]), + relativePath: decodeURIComponent(parts[1]) + }); + } + } + return result; + } + + function runBundledScript(script) { + var buttons = scriptList.querySelectorAll("button"); + for (var i = 0; i < buttons.length; i++) { + buttons[i].disabled = true; + } + setStatus("正在运行:" + script.label, false); + + evalHost(buildRunCode(encodeURIComponent(script.relativePath)), function (result) { + for (var j = 0; j < buttons.length; j++) { + buttons[j].disabled = false; + } + if (result && result.indexOf("ERROR|") === 0) { + setStatus(result.substring(6), true); + } else if (result === "EvalScript error.") { + setStatus("脚本运行失败,请检查 JSX 语法", true); + } else { + setStatus("已完成:" + script.label, false); + } + }); + } + + function makeTreeNode() { + return {folders: {}, scripts: []}; + } + + function addScriptToTree(root, script) { + var parts = script.relativePath.split("/"); + parts.pop(); + var node = root; + for (var i = 0; i < parts.length; i++) { + if (!node.folders[parts[i]]) { + node.folders[parts[i]] = makeTreeNode(); + } + node = node.folders[parts[i]]; + } + node.scripts.push(script); + } + + function countScripts(node) { + var count = node.scripts.length; + var names = Object.keys(node.folders); + for (var i = 0; i < names.length; i++) { + count += countScripts(node.folders[names[i]]); + } + return count; + } + + function createScriptButton(script) { + var button = document.createElement("button"); + button.type = "button"; + button.className = "script-button"; + button.textContent = script.label; + button.title = script.relativePath; + button.addEventListener("click", function () { + runBundledScript(script); + }); + return button; + } + + function appendTree(node, container, depth, expandAll) { + var folderNames = Object.keys(node.folders).sort(function (a, b) { + var left = a.toLowerCase(); + var right = b.toLowerCase(); + return left < right ? -1 : (left > right ? 1 : 0); + }); + + for (var i = 0; i < folderNames.length; i++) { + var folderName = folderNames[i]; + var childNode = node.folders[folderName]; + var details = document.createElement("details"); + details.className = "folder-group"; + details.open = expandAll; + + var summary = document.createElement("summary"); + summary.className = "folder-summary"; + summary.textContent = folderName + "(" + countScripts(childNode) + ")"; + + var content = document.createElement("div"); + content.className = "folder-content"; + appendTree(childNode, content, depth + 1, expandAll); + + details.appendChild(summary); + details.appendChild(content); + container.appendChild(details); + } + + for (var j = 0; j < node.scripts.length; j++) { + container.appendChild(createScriptButton(node.scripts[j])); + } + } + + function renderScripts() { + var query = searchInput.value.toLowerCase().replace(/^\s+|\s+$/g, ""); + scriptList.innerHTML = ""; + var visibleCount = 0; + var root = makeTreeNode(); + + for (var i = 0; i < scripts.length; i++) { + var searchable = (scripts[i].label + " " + scripts[i].relativePath).toLowerCase(); + if (query && searchable.indexOf(query) === -1) { + continue; + } + visibleCount++; + addScriptToTree(root, scripts[i]); + } + + appendTree(root, scriptList, 0, query.length > 0); + + emptyState.hidden = visibleCount !== 0; + } + + function loadScripts() { + refreshButton.disabled = true; + setStatus("正在读取脚本…", false); + evalHost(buildListCode(), function (raw) { + refreshButton.disabled = false; + try { + scripts = parseScriptList(raw); + renderScripts(); + setStatus("已载入 " + scripts.length + " 个脚本", false); + } catch (error) { + scripts = []; + renderScripts(); + setStatus(error.message, true); + } + }); + } + + refreshButton.addEventListener("click", loadScripts); + searchInput.addEventListener("input", renderScripts); + + openFolderButton.addEventListener("click", function () { + evalHost(buildOpenFolderCode(), function (result) { + if (result && result.indexOf("ERROR|") === 0) { + setStatus(result.substring(6), true); + } else if (result === "EvalScript error.") { + setStatus("无法执行打开文件夹命令", true); + } else { + setStatus("已打开脚本文件夹", false); + } + }); + }); + + runExternalButton.addEventListener("click", function () { + setStatus("请选择一个 JSX 文件…", false); + evalHost(buildExternalScriptCode(), function (result) { + if (result && result.indexOf("ERROR|") === 0) { + setStatus(result.substring(6), true); + } else if (result === "CANCEL") { + setStatus("已取消", false); + } else if (result === "EvalScript error.") { + setStatus("外部脚本运行失败", true); + } else { + setStatus("外部脚本已运行", false); + } + }); + }); + + var extensionPath = getExtensionPath(); + extensionPathToken = encodeURIComponent(extensionPath); + if (!extensionPath) { + setStatus("无法获取扩展目录,请重启 Photoshop", true); + } else { + loadScripts(); + } +}()); diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_HP_v1.6.2.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_HP_v1.6.2.jsx new file mode 100644 index 0000000..8469094 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_HP_v1.6.2.jsx @@ -0,0 +1,1981 @@ +#target photoshop +app.bringToFront(); + +// Photoshop Layers to PNG (for Spine) +// 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. +// +// This script is based on the older version created by NathanSweet from Esoteric Software. +// Offical Version : +// https://github.com/EsotericSoftware/spine-scripts +// https://github.com/EsotericSoftware/spine-scripts/blob/master/photoshop/PhotoshopToSpine.jsx +// +// 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. + +// +// Modified by Hope +// +// ---------- New Features -------------------------------------------------------------------------------- +// * Export layer/group to PNG with spicific scale and rotation by adding suffix to layer/group name. +// * Optimize image rotation while exporting automatically. +// * Apply layer's/group's opacity to Spine slot. +// * Apply layer's/group's blendmode to Spine slot. +// * Export group as Spine slot. +// * Merge group automatically. +// * More ignore options +// * layer/group name begin with underscore ("_"). +// * layer/group with gray label. +// * Corp to Canvas option. +// * Trim option. +// * Setting Spine origin in Photoshop. +// * Choose resample method you want. +// * Resample resolution to 72dpi automatically. +// * Export PNGs through Save For Web. +// * Support Traditional Chinese and Simplified Chinese. +// +// ---------- Changlog -------------------------------------------------------------------------------- +// v1.0.0 +// - Allow defining custom scale and rotation to specified layer by adding suffixes to the layer name, and will apply to exported images and JSON data. +// v1.1.0 +// - Prevent getting a crash from incorrect suffixes. +// - Now export images through 'Save For Web'. +// - Calculate transform data by layer bounds. +// - Allow corp pixels out of canvas. +// - Fixed incorrect position after layer rotated. +// - Setup template attachment correctly after 'Import Data' in Spine. +// v1.2.0 +// - Allow merge group automatically. +// - Allow trim layer or not. +// - Can now ignore layers with the gray color label (Photoshop CC Only!!). +// - Can now ignore layers start with an underscore ("_"). +// v1.3.0 +// - Allow group as a slot (need to add suffix " #SLOT"), and each layer inside as attachment. +// - Fixed wrong slot order while has skins. +// - JSON data format now is the same as exported from Spine. +// v1.3.1 +// - Fixed information of done dialog. +// v1.4.0 +// - Added "Use '_ROOT' layer as origin", will set Spine origin position as the center of layer's bound. +// - Fixed incorrect JSON data when there is only one layer in the slot. +// - Fixed 'use ruler origin as 0,0'. +// - JSON data now include layer's blend mode. +// - JSON data now include layer's opacity. +// - Can now set resample method for resizing. +// v1.4.1 +// - Fixed group with mask bug in PS CS6.(Merge group with mask on CS6 will delete pixels out of canvas.) +// v1.5 +// - Add trim away settings. +// - Now have multi-languages(English, Traditional Chinese, Simplified Chinese). +// - Fixed layer's opacity function. (Incorrect opacity value calculation and forgot to reset layer's opacity to 100.) +// - Fixed failure of writing blendmode when layer has custom suffixes. (Record the wrong layer name, need to record in clean layer name.) +// - Fixed JSON format. +// - Fixed write template slot to default skin only. +// - Fixed wrong display order with default skin(All slots in default skin will go on top of skins). +// (But disable the fix "Fixed wrong slot order while has skins." in v1.3.0, forgot what actually the bug is...) +// v1.5.1 +// - Can resample to screen dpi (72dpi) automatically. +// - Close the temporarily document for merging gourps immediately. +// - More resample methods. +// v1.6 - 2017-11-22 +// - Now can optimize image rotatation while exporting. (Enable/UpdateToPSD/ForceOptimize/RotateStep/ChkThreshold/OptThreshold) +// - Support different resmaple methods between CS6 (and below) and CC. +// - Ignore layers with gray label now works in Photoshop CS6 and below. +// - Remove Fix in v1.4.1. +// - Optmize UX. +// v1.6.1 - 2017-11-28 +// - Fix exporting hidden layers. +// - Fix not writting slot opacity to JSON while layer name has param. +// - Fix error while merging group is the top one. +// v1.6.2 - 2021-11-23 +// - Just Chinese. +// +// ## How to define custom scale and rotation +// Gives you more ability to optimize texture resolution. +// You are able to set export scale and rotation values for a specific layer in Photoshop and keep the size and rotation of attachments after importing to Spine, just the same as you see in Photoshop. +// +// Define scale and rotation by adding suffixes to the layer name. +// Define scale by adding suffix ' #s' to the layer name. +// Define rotation by adding suffix ' #r' to the layer name. +// +// ex. +// 'thisislayername #s0.5' (scale = 50%) +// 'thisislayername #s0.8 #r-12' (scale = 80%, rotation = -12 degree) +// Note: Suffix is case-sensitive. +// +// ## How to use group as slot +// Adding suffix ' #SLOT' to the group name will export group as a slot, and each layer in the group will export as an attachment(PNG). +// Note: Suffix is case-sensitive. +// +// ## How to use '_ROOT' as origin +// Create a layer named '_ROOT', and draw a dot at the origin position. To prevent exporting '_ROOT' layer, you can hide the layer or enable 'Ignore starts with an underscore'. +// Note: Layer name is case-sensitive. +// + +// SETTING DEFAULTS -------------------------------------------------------------------------------- +var writePngs = true; +var writeTemplate = false; +var writeJson = true; +var ignoreHiddenLayers = true; +var pngScale = 1; +const pngScale_DEF = 1; +var groupsAsSkins = false; +var useRulerOrigin = false; +var imagesDir = "./images/"; +var projectDir = ""; +var padding = 1; +const padding_DEF = 1; + +// JH VERSION VARIABLES -------------------------------------------------------------------------------- +var Version = "1.6.2"; +var psVersion_CC = 14; +// PATTERN +var patternRotate = " #r"; +var patternScale = " #s"; +var patternNoSkin = "-NOSKIN"; +var patternAsSlot = " #SLOT"; +var rootLayerName = "_ROOT"; +// MISC +var customScaleMin = 4;// Minimum width/height pixels of output image. +var docWidth; +var docHeight; +var mergeDoc; +var useRootLayer = false; +// LOCALIZATION +var currentLanguage = "CH"; +var languagesList = ["默认中文"]; +var languagesListValue = ["CH"]; +// IGNORE OPTIONS +var ignoreGrayLabelLayers = false; +var ignoreStartsWithUnderscoreLayers = true; +// GROUP +var mergeGroups = true; +// TRIM AND CORP +var allowTrimLayers = true; +var trimAwayLeft = true; +var trimAwayRight = true; +var trimAwayTop = true; +var trimAwayBottom = true; +var corpToCanvas = false; +// OPTIMIZE IMAGE rotation +var enableRotationOptimize = false; +var updateToPSD = true;// Update rotate value to layer/group name. +var updateRotaNameList = {}; +var forceOptRotation = false; +var rotateStep = 5;// Script will detect layer's bounds every 'rotateStep'. +const rotateStep_DEF = 5; +const rotateRange = 90;// Detect in this range. +var rotationChkThreshold = 64;// Try optimization when layer bounds is larger than this value(pixles square). +const rotationChkThreshold_DEF = 64; +var rotationOptThreshold = 10;// Execute optimization when the bounds is reduced more than this value(pixles square). +const rotationOptThreshold_DEF = 10; +// RESAMPLE AND RESOLUTION +var resampleToScreenDpi = false; +const screenDpi = 72; +var resample = 0; +var resampleObject; +var resampleMethods; +if (isPSCC()) { + // Photoshop CC + resampleMethods = { + "Automatic" : ResampleMethod.AUTOMATIC, // Not support in CS6 + "Bicubic Automatic" : ResampleMethod.BICUBICAUTOMATIC, // Not support in CS6 + "Bicubic" : ResampleMethod.BICUBIC, + "Bicubic Sharper" : ResampleMethod.BICUBICSHARPER, + "Bicubic Smoother" : ResampleMethod.BICUBICSMOOTHER, + "Bilinear" : ResampleMethod.BILINEAR, + "Nearest Neighbor" : ResampleMethod.NEARESTNEIGHBOR, + "None" : ResampleMethod.NONE, + "Preseve Details" : ResampleMethod.PRESERVEDETAILS // Not support in CS6 + }; +} else { + // Photoshop CS6 and below + resampleMethods = { + "Bicubic" : ResampleMethod.BICUBIC, + "Bicubic Sharper" : ResampleMethod.BICUBICSHARPER, + "Bicubic Smoother" : ResampleMethod.BICUBICSMOOTHER, + "Bilinear" : ResampleMethod.BILINEAR, + "Nearest Neighbor" : ResampleMethod.NEARESTNEIGHBOR, + "None" : ResampleMethod.NONE, + }; +} +// DEBUG +var startTime; +var endTime; +var autoCloseDoc = true; + +// IDs FOR SAVING SETTINGS -------------------------------------------------------------------------------- +const settingsID = sID("settings"); +const writePngsID = sID("writePngs"); +const writeTemplateID = sID("writeTemplate"); +const writeJsonID = sID("writeJson"); +const ignoreHiddenLayersID = sID("ignoreHiddenLayers"); +const groupsAsSkinsID = sID("groupsAsSkins"); +const useRulerOriginID = sID("useRulerOrigin"); +const pngScaleID = sID("pngScale"); +const paddingID = sID("padding"); +const imagesDirID = sID("imagesDir"); +const projectDirID = sID("projectDir"); + +const currentLanguageID = sID("currentLanguage"); +const ignoreGrayLabelLayersID = sID("ignoreGrayLabelLayers"); +const ignoreStartsWithUnderscoreLayersID = sID("ignoreStartsWithUnderscoreLayers"); +const useRootLayerID = sID("useRootLayer"); +const mergeGroupsID = sID("mergeGroups"); +const allowTrimLayersID = sID("allowTrimLayers"); +const trimAwayLeftID = sID("trimAwayLeft"); +const trimAwayRightID = sID("trimAwayRight"); +const trimAwayTopID = sID("trimAwayTop"); +const trimAwayBottomID = sID("trimAwayBottom"); +const corpToCanvasID = sID("corpToCanvas"); +const enableRotationOptimizeID = sID("enableRotationOptimize"); +const updateToPSDID = sID("updateToPSD"); +const forceOptRotationID = sID("forceOptRotation"); +const rotateStepID = sID("rotateStep"); +const rotationChkThresholdID = sID("rotationChkThreshold"); +const rotationOptThresholdID = sID("rotationOptThreshold"); +const resampleID = sID("resample"); +const resampleToScreenDpiID = sID("resampleToScreenDpi"); + +// LOCALIZATION START -------------------------------------------------------------------------------- +const STR_ROOT_LAYER_NOT_FOUND = { + CH : "找不到 Root 图层 ('_ROOT')! 重置成预设设定." +} +const STR_OPEN_DOC_FIRST = { + CH : "欲运行脚本请先开启文件." +} +const STR_SAVE_DOC_FIRST = { + CH : "欲运行脚本请先储存文件." +} +const STR_WINDOW_TITLE = { + CH : "Spine LayersToPNG v" + Version + " by Hope" +} +const STR_WRITE_LAYERS_AS_PNGS = { + CH : " 导出图层为 PNG" +} +const STR_WRITE_TEMPLATE_PNG = { + CH : " 编写一个模板PNG" +} +const STR_WRITE_SPINE_JSON = { + CH : " 导出 Spine JSON" +} +const STR_WRITE_LAYER_OPACITY = { + CH : " 写入图层透明度" +} +const STR_WRITE_LAYER_BLENDMODE = { + CH : " 写入图层混合模式" +} +const STR_WRITE_CUSTOM_SCALE_AND_ROTATION = { + CH : " 写入图层自订比例与旋转值( #s/ #r)" +} +const STR_IGNORE_HIDDEN_LAYERS = { + CH : " 忽略隐藏的图层" +} +const STR_IGNORE_STARTS_WITH_AN_UNDERSCORE = { + CH : " 忽略名称开头为底线符号的图层" +} +const STR_IGNORE_GRAY_LABEL_LAYERS = { + CH : " 忽略颜色标签为灰色的图层" +} +const STR_TRIM_LAYERS = { + CH : " 修剪图层" +} +const STR_CORP_TO_CANVAS = { + CH : " 裁切图层以符合版面" +} +const STR_USE_GROUPS_AS_SKINS = { + CH : " 将第一层群组视为皮肤" +} +const STR_ALLOW_GROUP_AS_SLOT = { + CH : " 将群组视为SLOT( #SLOT)" +} +const STR_MERGE_GROUPS = { + CH : " 自动合并群组" +} +const STR_ROTA_OPT_TITLE = { + CH : "图像角度优化" +} +const STR_ROTA_OPT_ENABLE = { + CH : " 激活" +} +const STR_ROTA_OPT_UPDATE_TO_PSD = { + CH : " 更新参数至图层/群组名称" +} +const STR_ROTA_OPT_FORCE = { + CH : " 强制优化所有图层" +} +const STR_ROTA_OPT_ROTATE_STEP = { + CH : " 检测精度 (度)" +} +const STR_ROTA_OPT_CHK_THRESHOLD = { + CH : " 检测门槛(平方像素)" +} +const STR_ROTA_OPT_OPT_THRESHOLD = { + CH : " 优化门槛(平方像素)" +} +const STR_RESAMPLE_TO_SCREEN_DPI = { + CH : " 重新取样至 " + screenDpi + " dpi" +} +const STR_TRIM_AWAY_TITLE = { + CH : "修剪部位" +} +const STR_TRIM_AWAY_LEFT = { + CH : " 左边" +} +const STR_TRIM_AWAY_RIGHT = { + CH : " 右边" +} +const STR_TRIM_AWAY_TOP = { + CH : " 顶部" +} +const STR_TRIM_AWAY_BOTTOM = { + CH : " 底部" +} +const STR_ORIGIN_SETTINGS_TITLE = { + CH : "Spine 原点设定" +} +const STR_ORIGIN_DEFAULT = { + CH : " PS版面左下角" +} +const STR_ORIGIN_USE_PS_RULER = { + CH : " PS尺标原点" +} +const STR_ORIGIN_USE_ROOT_LAYER = { + CH : " _ROOT图层中心点" +} +const STR_RESAMPLE_METHOD_TITLE = { + CH : "重新取样演算法" +} +// "Bicubic", "Bicubic Sharper", "Bicubic Smoother", "Bilinear", "Nearest Neighbor" +const STR_RESAMPLE_METHOD_AUTOMATIC = { + CH : "自动" +} +const STR_RESAMPLE_METHOD_BICUBIC_AUTOMATIC = { + CH : "环回增值自动选择" +} +const STR_RESAMPLE_METHOD_BICUBIC = { + CH : "环回增值法(适合平滑渐层)" +} +const STR_RESAMPLE_METHOD_BICUBIC_SHARPER = { + CH : "环回增值更锐利(适合缩小)" +} +const STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER = { + CH : "环回增值法更平滑(适合放大)" +} +const STR_RESAMPLE_METHOD_BILINEAR = { + CH : "纵横增值法" +} +const STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR = { + CH : "最接近像素(保留硬边)" +} +const STR_RESAMPLE_METHOD_NONE = { + CH : "无" +} +const STR_RESAMPLE_METHOD_PRESERVEDETAILS = { + CH : "保持细节" +} +const resampleMethodsDdlList = [ + STR_RESAMPLE_METHOD_BICUBIC_SHARPER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC[currentLanguage], + STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR[currentLanguage], + STR_RESAMPLE_METHOD_BILINEAR[currentLanguage] + ]; +const resampleMethodsDdlList_CC = [ + STR_RESAMPLE_METHOD_AUTOMATIC[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_PRESERVEDETAILS[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_BICUBIC_AUTOMATIC[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_BICUBIC_SHARPER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC[currentLanguage], + STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR[currentLanguage], + STR_RESAMPLE_METHOD_BILINEAR[currentLanguage], + STR_RESAMPLE_METHOD_NONE[currentLanguage] + ]; +const STR_PNG_SCALE = { + CH : "PNG比例" +} +const STR_PADDING = { + CH : "边距" +} +const STR_OUTPUT_DIRECTORIES_TITLE = { + CH : "导出目录" +} +const STR_OUTPUT_DIRECTORIES_IMAGES = { + CH : "图像" +} +const STR_OUTPUT_DIRECTORIES_JSON = { + CH : "JSON" +} +const STR_OUTPUT_DIRECTORIES_HINT = { + CH : "路径以 \"./\" 起始时会视为相对路径." +} +const STR_BUTTON_OK = { + CH : "确认" +} +const STR_BUTTON_CANCEL = { + CH : "取消" +} +const STR_JH_HELP = { + CH : "图层名称后缀说明\n\ +◆ 自订图层导出比例与旋转 :\r为图层名称添加比例或旋转后缀,导出时会应用到图层上\n\ +\t∙ 比例后缀 ' #s'.\n\ +\t∙ 旋转后缀 ' #r'.\n\ +\tex. 'layer1 #s0.5'\r\t\t(缩小成50%)\n\ +\tex. 'layer2 #s0.8 #r-12'\r\t\t(缩小成80%, 旋转-12度)\n\ +◆ 将群组视为 SLOT :\r为群组名称添加后缀 ' #SLOT', 导出时会把该群组视为 SLOT, 群组内的图层或群组则个别导出成PNG(Spine附件).\n\ +\tex. 'group1 #SLOT'\n\ +*** 注意后缀有区分大小写.\r\ +*** 注意后缀皆以空白为开头." +} +const STR_DONE_HEAD = { + CH : "完成!\n皮肤" +} +const STR_DONE_TOTAL_EXPORTED = { + CH : "导出总计 : " +} +const STR_DONE_RESAMPLE_METHOD = { + CH : "重新取样演算法 : " +} +const STR_DONE_ELAPSED = { + CH : "耗时 : " +} +const STR_PNG_SCALE_MUST_BE_BETWEEN = { + CH : "PNG 比例必须介于 0 与 100." +} +const STR_PNG_PADDING_MUST_BE = { + CH : "边距必须大于零" +} +const STR_UNEXPECTED_ERROR_HAS_OCCURRED = { + CH : "发生了未知的错误.\n\ +“若要进行调试,请使用Adobe ExtendScript运行LayersToPNG脚本,并取消选中“调试>保护异常时请勿中断”。" +} +const STR_CHANGE_TO_RGB_MODE = { + CH : "请将图像转为 RGB 色彩模式" +} +// LOCALIZATION END -------------------------------------------------------------------------------- + +// For debug. +startTime = new Date().getTime(); + +var originalDoc; +try { + originalDoc = app.activeDocument; +} catch (ignored) {} + +var settings, progress; + +loadSettings(); +showDialog(); + +// RUN ---------------------------------------------------------------------------------------------------- +// Execute while OK button pressed. +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 + "/"); + + // Save For Web options. + var outputExtension = ".png"; + var sfwOptions; + sfwOptions = new ExportOptionsSaveForWeb(); + sfwOptions.format = SaveDocumentType.PNG; + // Save to PNG24. + sfwOptions.PNG8 = false; + + // Duplicate current document for operating. + activeDocument.duplicate(); + deselectLayers(); + + try { + convertToRGB(); + } catch (ignored) {} + if (app.activeDocument.mode != DocumentMode.RGB) { + alert(STR_CHANGE_TO_RGB_MODE); + return; + } + + // Resample to screen dpi (72dpi) automatically + if (resampleToScreenDpi && activeDocument.resolution != screenDpi) activeDocument.resizeImage(null, null, screenDpi, ResampleMethod.AUTOMATIC); + + var xOffSet = 0, yOffSet = 0; + if (useRootLayer) { + // Custom Origin : set origin by '_ROOT' layer. + // Get root layer. + try{ + var rootLayer = app.activeDocument.layers.getByName(rootLayerName); + } catch (e) { + alert(STR_ROOT_LAYER_NOT_FOUND[currentLanguage]); + useRootLayer = false; + } + // Get center position of root layer's bounds. + if (rootLayer) { + var rootLayerCenter = getBoundsCenter(rootLayer); + xOffSet = rootLayerCenter[0]; + yOffSet = rootLayerCenter[1]; + } + } else if (useRulerOrigin) { + // Get ruler origin. + var ref = new ActionReference(); + ref.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt")); + var desc = executeActionGet(ref); + xOffSet = desc.getInteger(sID("rulerOriginH")) >> 16; + yOffSet = desc.getInteger(sID("rulerOriginV")) >> 16; + } + + // Output template image. + if (writeTemplate) { + if (pngScale != 1) { + scaleImage(); + storeHistory(); + } + + var file = new File(absImagesDir + "template" + outputExtension); + if (file.exists) file.remove(); + + activeDocument.exportDocument(file, ExportType.SAVEFORWEB, sfwOptions); + + if (pngScale != 1) restoreHistory(); + } + + if (writeJson == false && writePngs == false) { + activeDocument.close(SaveOptions.DONOTSAVECHANGES); + return; + } + + // Merge all groups, except slot groups and skin group while groupsAsSkins. + if (mergeGroups) { + mergeDoc = activeDocument; + var groups = []; + // Add a layer at top to prevent occur error while merging group at top. + activeDocument.artLayers.add(); + collectGroups(activeDocument, groups); + mergeEachGroups(groups); + + activeDocument.duplicate(); + + if (mergeDoc) mergeDoc.close(SaveOptions.DONOTSAVECHANGES); + } + + // Rasterize all layers. + try { + executeAction(sID( "rasterizeAll" ), undefined, DialogModes.NO); + } catch (ignored) {} + + // Collect and hide layers. + var layers = []; + var layersOpacity = {}; + var layersBlend = {}; + collectLayers(activeDocument, layers, layersOpacity, layersBlend); + var layersCount = layers.length; + + storeHistory(); + + // Store the slot names and layers for each skin. + var slots = {}; + var skins = { "default": [] }; + var skinsOnlySlots = { "default": [] }; + var lastSlotName = ""; + + for (var i = layersCount - 1; i >= 0; i--) { + var layer = layers[i]; + var currentSlotName = ""; + + // Use groups as skin names. + var potentialSkinName; + if (isGroupAsSlot(layer.parent)) { + // Current layer is in slot group. + potentialSkinName = trim(layer.parent.parent.name); + currentSlotName = layerName(layer.parent) + } else { + // Current layer is in skin group. + potentialSkinName = trim(layer.parent.name); + currentSlotName = layerName(layer) + } + var layerGroupSkin = potentialSkinName.indexOf(patternNoSkin) == -1; + var skinName = (groupsAsSkins && layer.parent.typename == "LayerSet" && layerGroupSkin) ? potentialSkinName : "default"; + + // Store skins' layers + var skinLayers = skins[skinName]; + if (!skinLayers) skins[skinName] = skinLayers = []; + skinLayers[skinLayers.length] = layer; + + // Store skins' slots + if (lastSlotName != currentSlotName) { + var skinSlots = skinsOnlySlots[skinName]; + if (!skinSlots) skinsOnlySlots[skinName] = skinSlots = []; + skinSlots[skinSlots.length] = currentSlotName; + } + + slots[currentSlotName] = true; + + lastSlotName = currentSlotName; + } + + // Resort slots order to fix the orders while groupsAsSkins. + if (groupsAsSkins) slots = resortSlotsOrder(slots, skinsOnlySlots); + + // Output skeleton and bones -------------------------------------------------- + var json = '{\n"skeleton": { "images": "' + relImagesDir + '" },'; + json += '\n"bones": [\n\t{ "name": "root" }\n],\n'; + + // Output slots data -------------------------------------------------- + json += '"slots": [\n'; + var slotsCount = countAssocArray(slots); + var slotIndex = 0; + + // Output template slot to JSON. + if (writeTemplate) { + json += '\t{ "name": "' + "template" + '", "bone": "root", "attachment": "' + "template" + '" }'; + json += slotsCount > 0 ? ",\n" : "\n"; + } + + // Output slots ---------------------------------------------------------------------------------------------------- + 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; + } + } + // Write slot name and attachment name + json += '\t{ "name": "' + slotName + '", "bone": "root", "attachment": "' + attachmentName + '"'; + // Write opacity + if (layersOpacity[slotName] != null) { + // Convert opacity value from 0-100 to 0-255. + json += ', "color": "ffffff' + Math.round(layersOpacity[slotName] * 2.55).toString(16) + '"'; + } + // Write blendmode + if (layersBlend[slotName] != null) { + json += ', "blend": "' + layersBlend[slotName] + '"'; + } + json += ' }'; + slotIndex++; + json += slotIndex < slotsCount ? ",\n" : "\n"; + } + + // Output skins ---------------------------------------------------------------------------------------------------- + json += '],\n"skins": {\n'; + + var skinsCount = countAssocArray(skins); + var skinIndex = 0; + docWidth = activeDocument.width.as("px"); + docHeight = activeDocument.height.as("px"); + + // For debug + var imagesExportedCount = 0; + var imagesExportedCountTotal = 0; + var doneString = STR_DONE_HEAD[currentLanguage] + " ----------"; + + for (var skinName in skins) { + if (!skins.hasOwnProperty(skinName)) continue; + + var skinLayers = skins[skinName]; + var skinLayersCount = skinLayers.length; + var skinLayerIndex = 0; + + json += '\t"' + skinName + '": {'; + if (skinLayersCount > 0) json += "\n"; + + // Output template in skin to JSON + var templateX = docWidth / 2; + var templateY = docHeight / 2; + if (useRulerOrigin || useRootLayer) { + templateX -= xOffSet; + templateY -= (docHeight - yOffSet); + } + if (writeTemplate && skinName == "default") { + json += '\t\t"' + "template" + '": {\n\t\t\t"' + "template" + '": {' + json += '"x": ' + templateX + ',"y": ' + templateY; + json += ',"width": ' + docWidth + ',"height": ' + docHeight + '}\n\t\t}'; + json += skinLayersCount > 0 ? ",\n" : "\n"; + } + + var layersInSlotIndex = 0; + + for (var i = skinLayersCount - 1; i >= 0; i--) { + var layer = skinLayers[i]; + + var sourceLayerName = srcLayerName(layer); + var slotName; + var currentLayerName = layerName(layer); + + if (isGroupAsSlot(layer.parent)) { + // Target layer's parent is group as slot + slotName = layerName(layer.parent); + } else { + slotName = currentLayerName; + } + + var placeholderName, attachmentName; + if (skinName == "default") { + placeholderName = currentLayerName; + attachmentName = currentLayerName; + } else { + placeholderName = currentLayerName; + attachmentName = skinName + "/" + currentLayerName; + } + + layer.visible = true; + + // Get data from bounds + var x = docWidth * 0.5; + var y = docHeight * 0.5; + var boundsCenter = getBoundsCenter(layer); + if (allowTrimLayers) { + x = boundsCenter[0]; + y = boundsCenter[1]; + } + + var customRotate = 0; + var customScale = 1; + + // Get custom scale and rotation from layer name + if (forceOptRotation || hasCustomParameters(sourceLayerName)) { + customRotate = getRotate(layer); + customScale = getScale(layer); + // Prevent resolution of output image too small + if (customScale != 1) customScale = checkMinimumCustomScale(layer, customScale); + } + + // Rotate image + if (customRotate != 0) { + layer.rotate(customRotate, AnchorPosition.MIDDLECENTER); + var rotatedCenter = getBoundsCenter(layer); + var fixedCenter = fixRotatedOffset(rotatedCenter, boundsCenter, customRotate); + x = fixedCenter[0]; + y = fixedCenter[1]; + } + + // Corp to canvas and trim layer + if (corpToCanvas == false || customRotate != 0) extendCanvas(layer); + // .trim(trimtype, top, left, bottom, right) + if (layer.isBackgroundLayer == false && allowTrimLayers) activeDocument.trim(TrimType.TRANSPARENT, false, false, trimAwayBottom, trimAwayRight); + if (corpToCanvas && customRotate == 0) { + x = getCorpBoundsCenter(layer)[0]; + y = getCorpBoundsCenter(layer)[1]; + } + if (layer.isBackgroundLayer == false && allowTrimLayers) activeDocument.trim(TrimType.TRANSPARENT, trimAwayTop, trimAwayLeft, false, false); + + // Scale image + if (pngScale != 1 || customScale != 1) { + scaleImage(customScale); + } + + // Padding + if (padding > 0) { + var canvasWidth = activeDocument.width.as("px") + padding * 2; + var canvasHeight = activeDocument.height.as("px") + padding * 2; + activeDocument.resizeCanvas(canvasWidth, canvasHeight, AnchorPosition.MIDDLECENTER); + } + + // Save image + if (writePngs) { + if (skinName != "default") new Folder(absImagesDir + skinName).create(); + activeDocument.exportDocument(new File(absImagesDir + attachmentName + outputExtension), ExportType.SAVEFORWEB, sfwOptions); + imagesExportedCount += 1; + } + + restoreHistory(); + layer.visible = false; + + // Shift root position, Convert root position's coordinate from Left-Top(Photoshop) to Left-Bottom(Spine) + y = docHeight - y; + // Calculate root position offset + if (useRulerOrigin || useRootLayer) { + x -= xOffSet; + y -= (docHeight - yOffSet); + } + + var outScale = mathRound(1 / customScale, 2); + + // Write JSON + // example : + // "emo_brows": { + // "emo_brows-hit": { "name": "skin1/emo_brows-hit", "x": 418,"y": 713,"width": 100,"height": 60 }, + // "emo_brows-win": { "name": "skin1/emo_brows-win", "x": 410,"y": 710,"width": 103,"height": 63 } + // }, + if (layersInSlotIndex == 0) { + json += '\t\t"' + slotName + '": {'; + } + + if (attachmentName == placeholderName) { + json += '\n\t\t\t"' + placeholderName + '": {'; + } else { + json += '\n\t\t\t"' + placeholderName + '": { "name": "' + attachmentName + '", '; + } + + // Write position + json += '"x": ' + mathRound(x, 1) + ',"y": ' + mathRound(y, 1); + // Write custom scale + if (customScale != 1) json += ',"scaleX": ' + outScale + ',"scaleY": ' + outScale; + // Write custom rotate + if (customRotate != 0) json += ',"rotation": ' + customRotate; + // Write size + json += ',"width": ' + Math.round(canvasWidth) + ',"height": ' + Math.round(canvasHeight); + // Write end of code + skinLayerIndex++; + if (isGroupAsSlot(layer.parent) && layer.parent.layers && layersInSlotIndex < getVisibleSiblingLayersCount(layer, skinLayers) - 1) + { + // Run next layer in slot + json += ' },' + layersInSlotIndex++; + } else { + // End the slot + json += ' }\n\t\t}'; + layersInSlotIndex = 0; + json += skinLayerIndex < skinLayersCount ? ",\n" : "\n"; + } + } + if (skinLayersCount > 0) json += "\t"; + json += "\}"; + + // For debug + imagesExportedCountTotal += imagesExportedCount; + doneString += "\n\t" + skinName + ": " + imagesExportedCount; + imagesExportedCount = 0; + + skinIndex++; + json += skinIndex < skinsCount ? ",\n" : "\n"; + } + + // Output animations. -------------------------------------------------- + json += '},\n"animations":{\n\t"animation": {}\n}\n}'; + + if (autoCloseDoc) 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(); + } + + // Rename layers with new rotate value. + if (updateToPSD) updateCustomRotaToLayerName(); + + // For debug + doneString += "\n- " + STR_DONE_TOTAL_EXPORTED[currentLanguage] + imagesExportedCountTotal; + doneString += "\n\n" + STR_DONE_RESAMPLE_METHOD[currentLanguage] + resampleObject.toString(); + endTime = new Date().getTime(); + var elapsedTime = mathRound(( endTime - startTime ) / 1000, 0); + doneString += "\n" + STR_DONE_ELAPSED[currentLanguage] + elapsedTime + "s"; + alert(doneString); +} + +// Dialog and settings -------------------------------------------------------------------------------- + +function showDialog () { + if (!originalDoc) { + alert(STR_OPEN_DOC_FIRST[currentLanguage]); + return; + } + if (!hasFilePath()) { + alert(STR_SAVE_DOC_FIRST[currentLanguage]); + return; + } + + var dialog = new Window("dialog", STR_WINDOW_TITLE[currentLanguage]); + 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, STR_WRITE_LAYERS_AS_PNGS[currentLanguage]); + writePngsCheckbox.value = writePngs; + var writeTemplateCheckbox = group.add("checkbox", undefined, STR_WRITE_TEMPLATE_PNG[currentLanguage]); + writeTemplateCheckbox.value = writeTemplate; + var writeJsonCheckbox = group.add("checkbox", undefined, STR_WRITE_SPINE_JSON[currentLanguage]); + writeJsonCheckbox.value = writeJson; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_LAYER_OPACITY[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_LAYER_BLENDMODE[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_CUSTOM_SCALE_AND_ROTATION[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var corpToCanvasCheckbox = group.add("checkbox", undefined, STR_CORP_TO_CANVAS[currentLanguage]); + corpToCanvasCheckbox.value = corpToCanvas; + var allowTrimLayersCheckbox = group.add("checkbox", undefined, STR_TRIM_LAYERS[currentLanguage]); + allowTrimLayersCheckbox.value = allowTrimLayers; + group = checkboxGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "left"; + var ignoreHiddenLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_HIDDEN_LAYERS[currentLanguage]); + ignoreHiddenLayersCheckbox.value = ignoreHiddenLayers; + var ignoreStartsWithUnderscoreLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_STARTS_WITH_AN_UNDERSCORE[currentLanguage]); + ignoreStartsWithUnderscoreLayersCheckbox.value = ignoreStartsWithUnderscoreLayers; + var ignoreGrayLabelLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_GRAY_LABEL_LAYERS[currentLanguage]); + ignoreGrayLabelLayersCheckbox.value = ignoreGrayLabelLayers; + var groupsAsSkinsCheckbox = group.add("checkbox", undefined, STR_USE_GROUPS_AS_SKINS[currentLanguage]); + groupsAsSkinsCheckbox.value = groupsAsSkins; + var emptyCheckbox = group.add("checkbox", undefined, STR_ALLOW_GROUP_AS_SLOT[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var mergeGroupsCheckbox = group.add("checkbox", undefined, STR_MERGE_GROUPS[currentLanguage]); + mergeGroupsCheckbox.value = mergeGroups; + var resampleToScreenDpiCheckbox = group.add("checkbox", undefined, STR_RESAMPLE_TO_SCREEN_DPI[currentLanguage]); + resampleToScreenDpiCheckbox.value = resampleToScreenDpi; + var emptyCheckbox = group.add("checkbox", undefined, " Empty"); + emptyCheckbox.visible = false; + emptyCheckbox.enabled = false; + /* + var emptyCheckbox = group.add("checkbox", undefined, " Empty"); + emptyCheckbox.visible = false; + emptyCheckbox.enabled = false; + */ + + var trimAwayGroup = dialog.add("panel", undefined, STR_TRIM_AWAY_TITLE[currentLanguage]); + trimAwayGroup.orientation = "row"; + trimAwayGroup.margins = [10,15,10,10]; + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + var trimAwayLeftCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_LEFT[currentLanguage]); + trimAwayLeftCheckbox.value = trimAwayLeft; + var trimAwayRightCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_RIGHT[currentLanguage]); + trimAwayRightCheckbox.value = trimAwayRight; + var trimAwayTopCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_TOP[currentLanguage]); + trimAwayTopCheckbox.value = trimAwayTop; + var trimAwayBottomCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_BOTTOM[currentLanguage]); + trimAwayBottomCheckbox.value = trimAwayBottom; + + allowTrimLayersCheckbox.onClick = function () { + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + }; + + var originGroup = dialog.add("panel", undefined, STR_ORIGIN_SETTINGS_TITLE[currentLanguage]); + originGroup.orientation = "row"; + originGroup.margins = [10,15,10,10]; + var useDefaultOriginCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_DEFAULT[currentLanguage]); + var useRulerOriginCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_USE_PS_RULER[currentLanguage]); + useRulerOriginCheckbox.value = useRulerOrigin; + var useRootLayerCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_USE_ROOT_LAYER[currentLanguage]); + useRootLayerCheckbox.value = useRootLayer; + if (useRulerOriginCheckbox.value == false && useRootLayerCheckbox.value == false) useDefaultOriginCheckbox.value = true; + + var rotaOptGroup = dialog.add("panel", undefined, STR_ROTA_OPT_TITLE[currentLanguage]); + rotaOptGroup.margins = [10,15,10,10]; + uiGroup = rotaOptGroup.add("group"); + uiGroup.orientation = "row" + uiGroup.alignment = ["fill", ""]; + group = uiGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "left"; + var enableRotationOptimizeCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_ENABLE[currentLanguage]); + enableRotationOptimizeCheckbox.value = enableRotationOptimize; + var updateToPSDCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_UPDATE_TO_PSD[currentLanguage]); + updateToPSDCheckbox.value = updateToPSD; + updateToPSDCheckbox.enabled = enableRotationOptimize; + var forceOptRotationCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_FORCE[currentLanguage]); + forceOptRotationCheckbox.value = forceOptRotation; + forceOptRotationCheckbox.enabled = enableRotationOptimize; + group = uiGroup.add("group"); + group.alignment = ["right", ""]; + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_ROTA_OPT_ROTATE_STEP[currentLanguage]); + group.add("statictext", undefined, STR_ROTA_OPT_CHK_THRESHOLD[currentLanguage]); + group.add("statictext", undefined, STR_ROTA_OPT_OPT_THRESHOLD[currentLanguage]); + group = uiGroup.add("group"); + group.alignment = ["fill", ""]; + group.orientation = "column"; + group.alignChildren = "right"; + var rotateStepText = group.add("edittext", undefined, rotateStep); + rotateStepText.characters = 8; + rotateStepText.enabled = enableRotationOptimize; + rotateStepText.onChanging = function () { + var tempTextValue = parseInt(rotateStepText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = rotateStep_DEF; + rotateStepText.text = tempTextValue; + } + var rotationChkThresholdText = group.add("edittext", undefined, rotationChkThreshold); + rotationChkThresholdText.characters = 8; + rotationChkThresholdText.enabled = enableRotationOptimize; + rotationChkThresholdText.onChanging = function () { + var tempTextValue = parseInt(rotationChkThresholdText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = rotationChkThreshold_DEF; + rotationChkThresholdText.text = tempTextValue; + } + var rotationOptThresholdText = group.add("edittext", undefined, rotationOptThreshold_DEF); + rotationOptThresholdText.characters = 8; + rotationOptThresholdText.enabled = enableRotationOptimize; + rotationOptThresholdText.onChanging = function () { + var tempTextValue = parseInt(rotationOptThresholdText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = 10; + rotationOptThresholdText.text = tempTextValue; + } + + enableRotationOptimizeCheckbox.onClick = function () { + updateToPSDCheckbox.enabled = enableRotationOptimizeCheckbox.value; + forceOptRotationCheckbox.enabled = enableRotationOptimizeCheckbox.value; + rotateStepText.enabled = enableRotationOptimizeCheckbox.value; + rotationChkThresholdText.enabled = enableRotationOptimizeCheckbox.value; + rotationOptThresholdText.enabled = enableRotationOptimizeCheckbox.value; + } + + var resampleGroup = dialog.add("group"); + group = resampleGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_RESAMPLE_METHOD_TITLE[currentLanguage]); + group = resampleGroup.add("group"); + group.alignment = ["fill", ""]; + group.orientation = "column"; + group.alignChildren = ["fill", ""]; + var resampleDdl; + if (isPSCC()) { + // Photoshop CC + resampleDdl = group.add("dropdownlist", undefined, resampleMethodsDdlList_CC); + } else { + // Photoshop CS6 and below + resampleDdl = group.add("dropdownlist", undefined, resampleMethodsDdlList); + } + + resampleDdl.selection = resample; + + var slidersGroup = dialog.add("group"); + group = slidersGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_PNG_SCALE[currentLanguage]); + group.add("statictext", undefined, STR_PADDING[currentLanguage]); + group = slidersGroup.add("group"); + group.orientation = "column"; + var scaleText = group.add("edittext", undefined, pngScale * 100); + scaleText.characters = 4; + scaleText.onChanging = function () { + } + 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 () { + var tempTextValue = parseInt(scaleText.text); + if (tempTextValue < 1 || tempTextValue > 100 || isNaN(tempTextValue)) tempTextValue = pngScale_DEF * 100; + scaleText.text = tempTextValue; + scaleSlider.value = scaleText.text; + }; + scaleSlider.onChanging = function () { + scaleText.text = Math.round(scaleSlider.value); + }; + paddingText.onChanging = function () { + var tempTextValue = parseInt(paddingText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = padding_DEF; + paddingText.text = tempTextValue; + scaleSlider.value = paddingText.text; + }; + paddingSlider.onChanging = function () { + paddingText.text = Math.round(paddingSlider.value); + }; + + var outputGroup = dialog.add("panel", undefined, STR_OUTPUT_DIRECTORIES_TITLE[currentLanguage]); + 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, STR_OUTPUT_DIRECTORIES_IMAGES[currentLanguage]); + group.add("statictext", undefined, STR_OUTPUT_DIRECTORIES_JSON[currentLanguage]); + 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, STR_OUTPUT_DIRECTORIES_HINT[currentLanguage]).alignment = "center"; + + var group = dialog.add("group"); + // group.alignment = "center"; + var runButton = group.add("button", undefined, STR_BUTTON_OK[currentLanguage]); + var cancelButton = group.add("button", undefined, STR_BUTTON_CANCEL[currentLanguage]); + + cancelButton.onClick = function () { + dialog.close(0); + return; + }; + + // JH Version Help dialog. + var JHButton = group.add("button", undefined, "!"); + JHButton.size = [24,20]; + JHButton.onClick = function () { + alert(STR_JH_HELP[currentLanguage]); + }; + + // Languages Drop Down List + var languagesDdl = group.add("dropdownlist", undefined, languagesList); + languagesDdl.selection = languageCodeToIndex(); + languagesDdl.onChanging = function () { + currentLanguage = languagesListValue[languagesDdl.selection.index]; + 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; + corpToCanvas = corpToCanvasCheckbox.value; + mergeGroups = mergeGroupsCheckbox.value; + allowTrimLayers = allowTrimLayersCheckbox.value; + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + ignoreGrayLabelLayers = ignoreGrayLabelLayersCheckbox.value; + ignoreStartsWithUnderscoreLayers = ignoreStartsWithUnderscoreLayersCheckbox.value; + useRootLayer = useRootLayerCheckbox.value; + imagesDir = imagesDirText.text; + projectDir = projectDirText.text; + var paddingValue = parseInt(paddingText.text); + if (paddingValue >= 0) padding = paddingValue; + resample = resampleDdl.selection.index; + resampleObject = resampleDdl.selection; + trimAwayLeft = trimAwayLeftCheckbox.value; + trimAwayRight = trimAwayRightCheckbox.value; + trimAwayTop = trimAwayTopCheckbox.value; + trimAwayBottom = trimAwayBottomCheckbox.value; + currentLanguage = languagesListValue[languagesDdl.selection.index]; + resampleToScreenDpi = resampleToScreenDpiCheckbox.value; + enableRotationOptimize = enableRotationOptimizeCheckbox.value; + updateToPSD = updateToPSDCheckbox.value; + forceOptRotation = forceOptRotationCheckbox.value; + rotateStep = rotateStepText.text; + rotationChkThreshold = rotationChkThresholdText.text; + rotationOptThreshold = rotationOptThresholdText.text; + } + + dialog.onClose = function() { + updateSettings(); + saveSettings(); + }; + + runButton.onClick = function () { + if (scaleText.text <= 0 || scaleText.text > 100) { + alert(STR_PNG_SCALE_MUST_BE_BETWEEN[currentLanguage]); + return; + } + if (paddingText.text < 0) { + alert(STR_PNG_PADDING_MUST_BE[currentLanguage]); + return; + } + dialog.close(0); + + var rulerUnits = app.preferences.rulerUnits; + app.preferences.rulerUnits = Units.PIXELS; + try { + run(); + } catch (e) { + alert(STR_UNEXPECTED_ERROR_HAS_OCCURRED[currentLanguage]); + debugger; + } finally { + if (activeDocument != originalDoc && autoCloseDoc) 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(corpToCanvasID)) corpToCanvas = settings.getBoolean(corpToCanvasID); + if (settings.hasKey(mergeGroupsID)) mergeGroups = settings.getBoolean(mergeGroupsID); + if (settings.hasKey(allowTrimLayersID)) allowTrimLayers = settings.getBoolean(allowTrimLayersID); + if (settings.hasKey(ignoreGrayLabelLayersID)) ignoreGrayLabelLayers = settings.getBoolean(ignoreGrayLabelLayersID); + if (settings.hasKey(ignoreStartsWithUnderscoreLayersID)) ignoreStartsWithUnderscoreLayers = settings.getBoolean(ignoreStartsWithUnderscoreLayersID); + if (settings.hasKey(useRootLayerID)) useRootLayer = settings.getBoolean(useRootLayerID); + if (settings.hasKey(imagesDirID)) imagesDir = settings.getString(imagesDirID); + if (settings.hasKey(projectDirID)) projectDir = settings.getString(projectDirID); + if (settings.hasKey(paddingID)) padding = settings.getDouble(paddingID); + if (settings.hasKey(resampleID)) resample = settings.getDouble(resampleID); + if (settings.hasKey(trimAwayLeftID)) trimAwayLeft = settings.getBoolean(trimAwayLeftID); + if (settings.hasKey(trimAwayRightID)) trimAwayRight = settings.getBoolean(trimAwayRightID); + if (settings.hasKey(trimAwayTopID)) trimAwayTop = settings.getBoolean(trimAwayTopID); + if (settings.hasKey(trimAwayBottomID)) trimAwayBottom = settings.getBoolean(trimAwayBottomID); + if (settings.hasKey(currentLanguageID)) currentLanguage = settings.getString(currentLanguageID); + if (settings.hasKey(resampleToScreenDpiID)) resampleToScreenDpi = settings.getBoolean(resampleToScreenDpiID); + if (settings.hasKey(enableRotationOptimizeID)) enableRotationOptimize = settings.getBoolean(enableRotationOptimizeID); + if (settings.hasKey(updateToPSDID)) updateToPSD = settings.getBoolean(updateToPSDID); + if (settings.hasKey(forceOptRotationID)) forceOptRotation = settings.getBoolean(forceOptRotationID); + if (settings.hasKey(rotateStepID)) rotateStep = settings.getDouble(rotateStepID); + if (settings.hasKey(rotationChkThresholdID)) rotationChkThreshold = settings.getDouble(rotationChkThresholdID); + if (settings.hasKey(rotationOptThresholdID)) rotationOptThreshold = settings.getDouble(rotationOptThresholdID); +} + +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.putBoolean(corpToCanvasID, corpToCanvas); + settings.putBoolean(mergeGroupsID, mergeGroups); + settings.putBoolean(allowTrimLayersID, allowTrimLayers); + settings.putBoolean(ignoreGrayLabelLayersID, ignoreGrayLabelLayers); + settings.putBoolean(ignoreStartsWithUnderscoreLayersID, ignoreStartsWithUnderscoreLayers); + settings.putBoolean(useRootLayerID, useRootLayer); + settings.putString(imagesDirID, imagesDir); + settings.putString(projectDirID, projectDir); + settings.putDouble(paddingID, padding); + settings.putDouble(resampleID, resample); + settings.putBoolean(trimAwayLeftID, trimAwayLeft); + settings.putBoolean(trimAwayRightID, trimAwayRight); + settings.putBoolean(trimAwayTopID, trimAwayTop); + settings.putBoolean(trimAwayBottomID, trimAwayBottom); + settings.putString(currentLanguageID, currentLanguage); + settings.putBoolean(resampleToScreenDpiID, resampleToScreenDpi); + settings.putBoolean(enableRotationOptimizeID, enableRotationOptimize); + settings.putBoolean(updateToPSDID, updateToPSD); + settings.putBoolean(forceOptRotationID, forceOptRotation); + settings.putDouble(rotateStepID, rotateStep); + settings.putDouble(rotationChkThresholdID, rotationChkThreshold); + settings.putDouble(rotationOptThresholdID, rotationOptThreshold); + app.putCustomOptions(settingsID, settings, true); +} + +// Photoshop utility: + +function isPSCC () { + // Check Photoshop version is above CC or not. + return (parseInt(app.version) >= psVersion_CC); +} + +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); + layer.remove(); +} + +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 scaleImage (customScale) { + if (customScale == undefined) customScale = 1; + var imageSize = activeDocument.width.as("px"); + activeDocument.resizeImage(UnitValue(imageSize * pngScale * customScale, "px"), null, null, resampleMethods[resampleObject]); +} + +function rotateImage (customRotate) { + if (customRotate == undefined) customRotate = 0; + activeDocument.rotateCanvas(customRotate); +} + +function duplicateLayerToNewDocument(ID,filename) { + var ref = new ActionReference(); + ref.putIdentifier(cID("Lyr "), ID); + + var desc = new ActionDescriptor(); + desc.putReference(cID("null"), ref ); + + executeAction(cID("slct"), desc, DialogModes.NO ); + desc = new ActionDescriptor(); + ref = new ActionReference(); + ref.putClass( cID("Dcmn") ); + desc.putReference( cID("null"), ref ); + desc.putString( cID("Nm "), filename ); + var ref2 = new ActionReference(); + //This line does not work!!!!!!!!!!! + //It still wants the selected layer(s) + ref2.putIdentifier(cID("Lyr "), ID); + desc.putReference( cID("Usng"), ref2 ); + desc.putInteger( cID("Vrsn"), 5 ); + executeAction( cID("Mk "), desc, DialogModes.NO ); +} + +function getLayerID(layer){ + var tempVisible = layer.visible; + var result; + if (isPSCC()) { + // Photoshop CC + result = layer.id + } else { + activeDocument.activeLayer = layer; + // Assumes activeDocument and activeLayer + var ref = new ActionReference(); + ref.putEnumerated(cID("Lyr "), cID("Ordn"), cID("Trgt")); + d = executeActionGet(ref); + result = d.getInteger(cID('LyrI')); + } + layer.visible = tempVisible; + return result; +}; + +function fixRotatedOffset(rotatedCenter, originalCenter, degrees){ + // Get right position of rotated layer to write JSON data + var rcx = rotatedCenter[0]; + var rcy = rotatedCenter[1]; + var ocx = originalCenter[0]; + var ocy = originalCenter[1]; + // Angle must be counter-clockwise + var theta = toRadians(degrees * -1); + + // Translate point to origin + var tempX = ocx - rcx; + var tempY = ocy - rcy; + + // Now apply rotation + var rotatedX = parseFloat(tempX * Math.cos(theta) - tempY * Math.sin(theta)); + var rotatedY = parseFloat(tempX * Math.sin(theta) + tempY * Math.cos(theta)); + + // Position to JSON + var outX = ocx - rotatedX; + var outY = ocy - rotatedY; + + return [outX, outY]; +} + +function checkMinimumCustomScale (layer, customScale) { + // Prevent resolution of output image too small + var boundsSize = getBoundsSize(layer); + var result = customScale; + var minPixelsOrig = Math.min(boundsSize[0], boundsSize[1]); + var minPixels = minPixelsOrig * pngScale * customScale; + + if (minPixels < customScaleMin) result = Math.ceil(mathRound(customScaleMin / minPixelsOrig, 3)); + + return result; +} + +var historyIndex; +function storeHistory () { + historyIndex = activeDocument.historyStates.length - 1; +} +function restoreHistory () { + activeDocument.activeHistoryState = activeDocument.historyStates[historyIndex]; +} + +function isGroup (layer) { + return layer.typename == "LayerSet"; +} + +function collectLayers (layer, collect, collectOpacity, collectBlendMode) { + for (var i = 0, n = layer.layers.length; i < n; i++) { + var child = layer.layers[i]; + var childID = getLayerID(child); + // Get clean layer name (remove custom suffixes) + var cleanLayerName = layerName(child); + if (ignoreHiddenLayers && child.visible == false) { +// deleteLayer(child); + continue; + } + if (ignoreGrayLabelLayers && getLayerColorByID(childID) == "gray") { + child.visible = false; + continue; + } + if (ignoreStartsWithUnderscoreLayers && startsWith(child.name, "_")) { + child.visible = false; + continue; + } + if (child.bounds[2] == 0 && child.bounds[3] == 0) continue; + // Collect opacity + if (child.opacity != 100) { + // Opacity Types : child.opacity, child.fillOpacity + collectOpacity[cleanLayerName] = child.opacity; + child.opacity = 100; + } + // Collect blend mode + if (child.blendMode == BlendMode.SCREEN || child.blendMode == BlendMode.LINEARDODGE || child.blendMode == BlendMode.MULTIPLY) { + switch(child.blendMode) { + case BlendMode.SCREEN: + collectBlendMode[cleanLayerName] = "screen"; + break; + case BlendMode.LINEARDODGE: + collectBlendMode[cleanLayerName] = "additive"; + break; + case BlendMode.MULTIPLY: + collectBlendMode[cleanLayerName] = "multiply"; + break; + } + } + // Collect valid layers + if (child.layers && child.layers.length > 0) { + collectLayers(child, collect, collectOpacity, collectBlendMode); + } else if (child.kind == LayerKind.NORMAL) { + collect.push(child); + child.visible = false; + } + } +} + +function collectGroups (parent, collect) { + for (var i = 0; i < parent.layers.length; i++) { + var child = parent.layers[i]; + if (child.visible && child.layers && child.layers.length > 0) + { + // Current target is group + if (isGroupAsSlot(child)) { + // Group as slot, don't merge. + collectGroups(child, collect); + } else if (groupsAsSkins == false) { + // No skins, merge all groups. + collect.push(child); + } else if (child.parent.typename == "LayerSet") { + // Target's parent is layerSet + collect.push(child); + } else { + collectGroups(child, collect); + } + } + } +} + +function getVisibleSiblingLayersCount (layer, skinLayers) { + var visibleCount = 0; + for (var i = 0; i < layer.parent.layers.length; i++) { + for (var j = 0; j < skinLayers.length; j++) { + // Check layer's visibility by compares to skinLayers + if (layer.parent.layers[i] == skinLayers[j]) { + visibleCount += 1; + } + } + } + return visibleCount; +} + +function mergeEachGroups (groups) { + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + activeDocument.activeLayer = group; + group.merge(); + /* + if (isOutOfCanvas(group) && isPSCC() == false) { + // Extend canvas then restore, avoid corping pixels out of canvas while merging group at PS CS6 13.0 + // ^^^ This problem doesn't exist in PS CS6 Extended Version(13.0), so it occurs in what situation? + var extendDist = Math.abs(getOutOfCanvasDist(group)) * 2; + extendCanvas(group); + group.merge(); + applyLayerMask(); + shrinkCanvas(extendDist); + } else { + group.merge(); + } + */ + } +} + +function hasFilePath () { + var ref = new ActionReference(); + ref.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt")); + return executeActionGet(ref).hasKey(sID("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; +} + +// Mathematics utility: + +function toRadians (degrees) { + return degrees * Math.PI / 180; +} + +function mathRound(num, decimal) { + var f = decimal > 0 ? Math.pow(10, decimal) : 1; + return Math.round(num * f) / f; +} + +// JavaScript utility: + +function cID (id) { + return charIDToTypeID(id); +} + +function sID (id) { + return stringIDToTypeID(id); +} + +function indexOfSmallest(ary) { + var lowest = 0; + for (var i = 1; i < ary.length; i++) { + if (ary[i] < ary[lowest]) lowest = i; + } + return lowest; +} + +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 stripSuffix (str, suffix) { + if (endsWith(str.toLowerCase(), suffix.toLowerCase())) str = str.substring(0, str.length - suffix.length); + return str; +} + +function layerName (layer) { + var result = stripSuffix(trim(layer.name), ".png").replace(/[:\/\\*\?\"\<\>\|]/g, ""); + // Get layer name without custom parameters string (customScale, customRotate, groupAsSlot). + var endIndex = result.indexOf(" #"); + if (endIndex != -1) { + result = result.substring(0, endIndex); + } + + return result +} + +function srcLayerName (layer) { + // Original version of layerName function + return stripSuffix(trim(layer.name), ".png").replace(/[:\/\\*\?\"\<\>\|]/g, ""); +} + +function isGroupAsSlot (layer) { + // Target is group with pattern, and parent is not as slot. + return (layer.name.indexOf(patternAsSlot) != -1 && layer.typename == "LayerSet" && isGroupAsSlot(layer.parent) == false) ? true : false; +} + +function resortSlotsOrder (slots, skins) { + // Fix slots' order while groupsAsSkins enabled. + // ^^^ Fix what wrong order?? This function output wrong order with default skin. All slots in default skin will on top all skins, but the original order doesn't get this problem. + +/* + var result = {}; + var resorted = []; + var exists = false; + + for (var slotName in slots) { + // first round. send 1 slot name to resorted array. + if (resorted.length == 0) { + resorted.push(slotName); + continue; + } + + // Find current slot in skins. + for (var skinName in skins) { + if (exists) break; + // Find current slot in current skin. + for (var i = 0; i < skins[skinName].length; i++) { + if (slotName == skins[skinName][i]) { + // Found slot's order in skins. + var insertIndex = 0; + + // Insert current slot after previous slot. + if (i > 0) { + // Get the slot before current slot in skins. + var previousSlotName = skins[skinName][i-1]; + for (var j = 0; j < resorted.length; j++) { + // Find the previous slot in resorted array, and get the next index. + if (resorted[j] == previousSlotName) { + insertIndex = j + 1; + break; + } + } + } + + // Insert current slot to resorted array. + resorted.splice(insertIndex, 0, slotName); + exists = true; + break; + } + } + } + exists = false; + } + + // Copy array to dictionary ** Why convert to dict?????? + for (var i in resorted) { + result[resorted[i]] = true; + } + + return result; +*/ + return slots; +} + +// Custom properties utility : + +function hasCustomParameters (layerName) { + var result = false; + if (layerName.search(patternRotate) != -1 || layerName.search(patternScale) != -1) { + result = true; + } + + return result; +} + +function getPropertyValue (layer, pattern) { + var layerName = layer.name; + var propertyValue; + // Return null while no valid property. + var result = null; + // Get last param in layer name. + var patternIndex = layerName.lastIndexOf(pattern); + + if (patternIndex != -1) { + // Parttern exists. + + var patternEndIndex = patternIndex + pattern.length; + var stringAfterPattern = layerName.substring( patternEndIndex, layerName.length ); + var propertyEndIndex = stringAfterPattern.indexOf(" "); + if (propertyEndIndex != -1) { + propertyValue = parseFloat(stringAfterPattern.substring(0, propertyEndIndex)); + } else { + propertyValue = parseFloat(stringAfterPattern); + } + result = propertyValue; + } + + // Return null while no vaild property. + // Return NaN while no vaild property value. + return result; +} + +function setPropertyValue (layer, pattern, value) { + var layerName = layer.name; + var newLayerName = ""; + // Get last param in layer name. + var patternIndex = layerName.lastIndexOf(pattern); + + if (patternIndex != -1) { + // Parttern exists. + var patternEndIndex = patternIndex + pattern.length; + var stringBeforeProperty = layerName.substring(0, patternIndex); + var stringAfterPattern = layerName.substring(patternEndIndex, layerName.length); + var propertyEndIndex = stringAfterPattern.indexOf(" "); + var stringAfterProperty = ""; + if (propertyEndIndex != -1) stringAfterProperty = stringAfterPattern.substring(propertyEndIndex, layerName.length); + + newLayerName = stringBeforeProperty + pattern + value + stringAfterProperty; + + layer.name = newLayerName; + } else if (forceOptRotation) { + newLayerName = layerName + pattern + value; + + layer.name = newLayerName; + } + + return newLayerName; +} + +function getScale (layer) { + var layerName = srcLayerName(layer); + var result = 1; + var value = getPropertyValue(layer, patternScale); + if (isNaN(value) == false) result = value; + + return result; +} + +function getRotate (layer) { + var layerName = srcLayerName(layer); + var result = 0; + var value = getPropertyValue(layer, patternRotate); + if (value) { + // Property is exists and has value, return the value. + result = value; + } else if (enableRotationOptimize) { + // Property is exists without value, generate value automatically. + if (isNaN(value) || forceOptRotation) result = autoOptimzeRotation(layer); + } + + return result; +} + +function autoOptimzeRotation (layer) { + if (getBoundsArea(layer) > rotationChkThreshold) { + var result = 0; +// var originalLayer = layer; + var tempRotaDocName = "OptimizingImageRotation"; + + // Duplicate layer to temp document to optimzing execution speed. + duplicateLayerToNewDocument(getLayerID(layer), tempRotaDocName); + var tempRotaDoc = app.documents.getByName(tempRotaDocName); + var tempLayer = tempRotaDoc.activeLayer; + + var boundsAreaList = new Array(); + + // If current layer is group, merge it first. + if (tempLayer.layers && tempLayer.layers.length > 0) tempLayer = tempLayer.merge(); + + // Save non-rotated and merged history state. + var nonRotatedAndMergedState = app.activeDocument.activeHistoryState; + + // Check layer's bounds area of each rotateStep. + if (rotateStep < 1) rotateStep = 1; + for (var i = 0; i < (rotateRange / rotateStep); i++) { + // Rotate layer. + if (i > 0) tempLayer.rotate(i * rotateStep, AnchorPosition.MIDDLECENTER); + + // Get bounds area and save to array. + var boundsArea = getBoundsArea(tempLayer); + boundsAreaList.push(boundsArea); + + // Restore non-rotated and merged history. + app.activeDocument.activeHistoryState = nonRotatedAndMergedState; + } + + tempRotaDoc.close(SaveOptions.DONOTSAVECHANGES); + + // Find minimum bounds area and get best degree. + var minIndex = indexOfSmallest(boundsAreaList); + var bestRotateValue = minIndex * rotateStep; + var boundsAreaDiff = Math.round(Math.sqrt(boundsAreaList[0] - boundsAreaList[minIndex])); + + if (boundsAreaDiff > rotationOptThreshold) { + // Save rotate value for layer renaming (UpdateToPSD) at end of script progress. + if (updateToPSD) updateRotaNameList[getLayerID(layer).toString()] = bestRotateValue; + result = bestRotateValue; + } + return result; + } +} + +function updateCustomRotaToLayerName () { + for (var id in updateRotaNameList) { + var layerID = parseInt(id); + selectLayerByID(layerID); + setPropertyValue(app.activeDocument.activeLayer, patternRotate, updateRotaNameList[id]); + } +} + +function getLayerColorByID( ID ) { + var ref = new ActionReference(); + ref.putProperty( cID("Prpr") ,sID('color')); + ref.putIdentifier(cID( "Lyr " ), ID ); + return typeIDToStringID(executeActionGet(ref).getEnumerationValue(sID('color'))); +} + +function selectLayerByID(layerID, add){ + var result = getLayerIndexByLayerID(layerID); + if(result){ + selectLayerByIndex(result, add); + }else{ + alert("Layer does not exist"); + } +}; +function selectLayerByIndex(index, add){ + add = undefined ? add = false:add + var ref = new ActionReference(); + ref.putIndex(cID("Lyr "), index); + var desc = new ActionDescriptor(); + desc.putReference(cID("null"), ref ); + if(add) desc.putEnumerated( sID( "selectionModifier" ), sID( "selectionModifierType" ), sID( "addToSelection" ) ); + desc.putBoolean( cID( "MkVs" ), false ); + try{ + executeAction(cID("slct"), desc, DialogModes.NO ); + }catch(e){ + alert(e.message); + } +}; +function getLayerIndexByLayerID(id) { + var ref = new ActionReference(); + ref.putProperty( cID("Prpr") , cID( "ItmI" )); + ref.putIdentifier( cID( "Lyr " ), id ); + try{ + return executeActionGet(ref).getInteger(cID( "ItmI" )); + }catch(e){ + return false; + } +}; + +// Bounds and Corp to canvas utility : + +function clampXToCanvas(num, axis) { + return Math.min( Math.max(num, 0), docWidth ); +} + +function clampYToCanvas(num, axis) { + return Math.min( Math.max(num, 0), docHeight ); +} + +function clampBoundsToCanvas(bounds) { + var x1 = clampXToCanvas(bounds[0]); + var y1 = clampYToCanvas(bounds[1]); + var x2 = clampXToCanvas(bounds[2]); + var y2 = clampYToCanvas(bounds[3]); + + return [x1, y1, x2, y2]; +} + +function getBoundsValue(layer) { + return [layer.bounds[0].value, layer.bounds[1].value, layer.bounds[2].value, layer.bounds[3].value]; +} + +function getBoundsSize(layer) { + var bounds = getBoundsValue(layer); + var width = bounds[2] - bounds[0]; + var height = bounds[3] - bounds[1]; + + return [width, height]; +} + +function getBoundsArea(layer) { + return (( layer.bounds[2] - layer.bounds[0] ) * ( layer.bounds[3] - layer.bounds[1] )); +} + +function getBoundsCenter(layer) { + var bounds = getBoundsValue(layer); + var size = getBoundsSize(layer); + var x = ( bounds[0] + size[0] / 2 ) * pngScale; + var y = ( bounds[1] + size[1] / 2 ) * pngScale; + + return [x, y]; +} + +function getCorpBoundsCenter(layer) { + var clampBounds = clampBoundsToCanvas(getBoundsValue(layer)); + var x1 = clampBounds[0]; + var y1 = clampBounds[1]; + var x2 = clampBounds[2]; + var y2 = clampBounds[3]; + var width = x2 - x1; + var height = y2 - y1; + var x = ( x1 + width / 2 ) * pngScale; + var y = ( y1 + height / 2 ) * pngScale; + + return [x, y]; +} + +function getOutOfCanvasDist(layer) { + var bounds = getBoundsValue(layer); + var x1 = bounds[0]; + var y1 = bounds[1]; + var x2 = activeDocument.width.as("px") - bounds[2]; + var y2 = activeDocument.height.as("px") - bounds[3]; + var dist = Math.min(x1, y1, x2, y2); + + return dist; +} + +function isOutOfCanvas(layer) { + if (getOutOfCanvasDist(layer) < 0) { + return true; + } else { + return false; + } +} + +function extendCanvas(layer) { + // Extend Canvas size for not corpping visible pixels + if (isOutOfCanvas(layer) == false) return; + var dist = Math.abs(getOutOfCanvasDist(layer)) * 2; + var w = activeDocument.width.as("px") + dist; + var h = activeDocument.height.as("px") + dist; + activeDocument.resizeCanvas(w, h, AnchorPosition.MIDDLECENTER); +} + +function shrinkCanvas(dist) { + // Extend Canvas size for not corpping visible pixels + var w = activeDocument.width.as("px") - dist; + var h = activeDocument.height.as("px") - dist; + activeDocument.resizeCanvas(w, h, AnchorPosition.MIDDLECENTER); +} + +/////////////////////////////////////////////////////////////////////////////// +// Function: applyLayerMask +// Usage: apply the vector mask on the current layer +// Input: Must have an open document +// Return: +/////////////////////////////////////////////////////////////////////////////// +function applyLayerMask() { + try{ + var id765 = cID( "Dlt " ); + var desc154 = new ActionDescriptor(); + var id766 = cID( "null" ); + var ref93 = new ActionReference(); + var id767 = cID( "Chnl" ); + var id768 = cID( "Ordn" ); + var id769 = cID( "Trgt" ); + ref93.putEnumerated( id767, id768, id769 ); + desc154.putReference( id766, ref93 ); + var id770 = cID( "Aply" ); + desc154.putBoolean( id770, true ); + executeAction( id765, desc154, DialogModes.NO ); + }catch(e) { + ; // do nothing + } +} + +function languageCodeToIndex() { + var currentLanguageIndex = 0; + for (var i = 0; i < languagesListValue.length; i++) { + if (currentLanguage == languagesListValue[i]) { + currentLanguageIndex = i; + } + } + return currentLanguageIndex; +} \ No newline at end of file diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_JH_v1.6.1.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_JH_v1.6.1.jsx new file mode 100644 index 0000000..ee1fbfc --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG_JH_v1.6.1.jsx @@ -0,0 +1,2124 @@ +#target photoshop +app.bringToFront(); + +// Photoshop Layers to PNG (for Spine) +// 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. +// +// This script is based on the older version created by NathanSweet from Esoteric Software. +// Offical Version : +// https://github.com/EsotericSoftware/spine-scripts +// https://github.com/EsotericSoftware/spine-scripts/blob/master/photoshop/PhotoshopToSpine.jsx +// +// 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. + +// +// Modified by Janus Huang +// E-mail : januswow@gmail.com +// QQ : 2646089959 +// +// ---------- New Features -------------------------------------------------------------------------------- +// * Export layer/group to PNG with spicific scale and rotation by adding suffix to layer/group name. +// * Optimize image rotation while exporting automatically. +// * Apply layer's/group's opacity to Spine slot. +// * Apply layer's/group's blendmode to Spine slot. +// * Export group as Spine slot. +// * Merge group automatically. +// * More ignore options +// * layer/group name begin with underscore ("_"). +// * layer/group with gray label. +// * Corp to Canvas option. +// * Trim option. +// * Setting Spine origin in Photoshop. +// * Choose resample method you want. +// * Resample resolution to 72dpi automatically. +// * Export PNGs through Save For Web. +// * Support Traditional Chinese and Simplified Chinese. +// +// ---------- Changlog -------------------------------------------------------------------------------- +// v1.0.0 +// - Allow defining custom scale and rotation to specified layer by adding suffixes to the layer name, and will apply to exported images and JSON data. +// v1.1.0 +// - Prevent getting a crash from incorrect suffixes. +// - Now export images through 'Save For Web'. +// - Calculate transform data by layer bounds. +// - Allow corp pixels out of canvas. +// - Fixed incorrect position after layer rotated. +// - Setup template attachment correctly after 'Import Data' in Spine. +// v1.2.0 +// - Allow merge group automatically. +// - Allow trim layer or not. +// - Can now ignore layers with the gray color label (Photoshop CC Only!!). +// - Can now ignore layers start with an underscore ("_"). +// v1.3.0 +// - Allow group as a slot (need to add suffix " #SLOT"), and each layer inside as attachment. +// - Fixed wrong slot order while has skins. +// - JSON data format now is the same as exported from Spine. +// v1.3.1 +// - Fixed information of done dialog. +// v1.4.0 +// - Added "Use '_ROOT' layer as origin", will set Spine origin position as the center of layer's bound. +// - Fixed incorrect JSON data when there is only one layer in the slot. +// - Fixed 'use ruler origin as 0,0'. +// - JSON data now include layer's blend mode. +// - JSON data now include layer's opacity. +// - Can now set resample method for resizing. +// v1.4.1 +// - Fixed group with mask bug in PS CS6.(Merge group with mask on CS6 will delete pixels out of canvas.) +// v1.5 +// - Add trim away settings. +// - Now have multi-languages(English, Traditional Chinese, Simplified Chinese). +// - Fixed layer's opacity function. (Incorrect opacity value calculation and forgot to reset layer's opacity to 100.) +// - Fixed failure of writing blendmode when layer has custom suffixes. (Record the wrong layer name, need to record in clean layer name.) +// - Fixed JSON format. +// - Fixed write template slot to default skin only. +// - Fixed wrong display order with default skin(All slots in default skin will go on top of skins). +// (But disable the fix "Fixed wrong slot order while has skins." in v1.3.0, forgot what actually the bug is...) +// v1.5.1 +// - Can resample to screen dpi (72dpi) automatically. +// - Close the temporarily document for merging gourps immediately. +// - More resample methods. +// v1.6 - 2017-11-22 +// - Now can optimize image rotatation while exporting. (Enable/UpdateToPSD/ForceOptimize/RotateStep/ChkThreshold/OptThreshold) +// - Support different resmaple methods between CS6 (and below) and CC. +// - Ignore layers with gray label now works in Photoshop CS6 and below. +// - Remove Fix in v1.4.1. +// - Optmize UX. +// v1.6.1 - 2017-11-28 +// - Fix exporting hidden layers. +// - Fix not writting slot opacity to JSON while layer name has param. +// - Fix error while merging group is the top one. +// +// ## How to define custom scale and rotation +// Gives you more ability to optimize texture resolution. +// You are able to set export scale and rotation values for a specific layer in Photoshop and keep the size and rotation of attachments after importing to Spine, just the same as you see in Photoshop. +// +// Define scale and rotation by adding suffixes to the layer name. +// Define scale by adding suffix ' #s' to the layer name. +// Define rotation by adding suffix ' #r' to the layer name. +// +// ex. +// 'thisislayername #s0.5' (scale = 50%) +// 'thisislayername #s0.8 #r-12' (scale = 80%, rotation = -12 degree) +// Note: Suffix is case-sensitive. +// +// ## How to use group as slot +// Adding suffix ' #SLOT' to the group name will export group as a slot, and each layer in the group will export as an attachment(PNG). +// Note: Suffix is case-sensitive. +// +// ## How to use '_ROOT' as origin +// Create a layer named '_ROOT', and draw a dot at the origin position. To prevent exporting '_ROOT' layer, you can hide the layer or enable 'Ignore starts with an underscore'. +// Note: Layer name is case-sensitive. +// + +// SETTING DEFAULTS -------------------------------------------------------------------------------- +var writePngs = true; +var writeTemplate = false; +var writeJson = true; +var ignoreHiddenLayers = true; +var pngScale = 1; +const pngScale_DEF = 1; +var groupsAsSkins = false; +var useRulerOrigin = false; +var imagesDir = "./images/"; +var projectDir = ""; +var padding = 1; +const padding_DEF = 1; + +// JH VERSION VARIABLES -------------------------------------------------------------------------------- +var JHVersion = "1.6.1"; +var psVersion_CC = 14; +// PATTERN +var patternRotate = " #r"; +var patternScale = " #s"; +var patternNoSkin = "-NOSKIN"; +var patternAsSlot = " #SLOT"; +var rootLayerName = "_ROOT"; +// MISC +var customScaleMin = 4;// Minimum width/height pixels of output image. +var docWidth; +var docHeight; +var mergeDoc; +var useRootLayer = false; +// LOCALIZATION +var currentLanguage = "EN"; +var languagesList = ["English", "繁體中文", "简体中文"]; +var languagesListValue = ["EN", "TC", "SC"]; +// IGNORE OPTIONS +var ignoreGrayLabelLayers = false; +var ignoreStartsWithUnderscoreLayers = true; +// GROUP +var mergeGroups = true; +// TRIM AND CORP +var allowTrimLayers = true; +var trimAwayLeft = true; +var trimAwayRight = true; +var trimAwayTop = true; +var trimAwayBottom = true; +var corpToCanvas = false; +// OPTIMIZE IMAGE rotation +var enableRotationOptimize = false; +var updateToPSD = true;// Update rotate value to layer/group name. +var updateRotaNameList = {}; +var forceOptRotation = false; +var rotateStep = 5;// Script will detect layer's bounds every 'rotateStep'. +const rotateStep_DEF = 5; +const rotateRange = 90;// Detect in this range. +var rotationChkThreshold = 64;// Try optimization when layer bounds is larger than this value(pixles square). +const rotationChkThreshold_DEF = 64; +var rotationOptThreshold = 10;// Execute optimization when the bounds is reduced more than this value(pixles square). +const rotationOptThreshold_DEF = 10; +// RESAMPLE AND RESOLUTION +var resampleToScreenDpi = false; +const screenDpi = 72; +var resample = 0; +var resampleObject; +var resampleMethods; +if (isPSCC()) { + // Photoshop CC + resampleMethods = { + "Automatic" : ResampleMethod.AUTOMATIC, // Not support in CS6 + "Bicubic Automatic" : ResampleMethod.BICUBICAUTOMATIC, // Not support in CS6 + "Bicubic" : ResampleMethod.BICUBIC, + "Bicubic Sharper" : ResampleMethod.BICUBICSHARPER, + "Bicubic Smoother" : ResampleMethod.BICUBICSMOOTHER, + "Bilinear" : ResampleMethod.BILINEAR, + "Nearest Neighbor" : ResampleMethod.NEARESTNEIGHBOR, + "None" : ResampleMethod.NONE, + "Preseve Details" : ResampleMethod.PRESERVEDETAILS // Not support in CS6 + }; +} else { + // Photoshop CS6 and below + resampleMethods = { + "Bicubic" : ResampleMethod.BICUBIC, + "Bicubic Sharper" : ResampleMethod.BICUBICSHARPER, + "Bicubic Smoother" : ResampleMethod.BICUBICSMOOTHER, + "Bilinear" : ResampleMethod.BILINEAR, + "Nearest Neighbor" : ResampleMethod.NEARESTNEIGHBOR, + "None" : ResampleMethod.NONE, + }; +} +// DEBUG +var startTime;  +var endTime; +var autoCloseDoc = true; + +// IDs FOR SAVING SETTINGS -------------------------------------------------------------------------------- +const settingsID = sID("settings"); +const writePngsID = sID("writePngs"); +const writeTemplateID = sID("writeTemplate"); +const writeJsonID = sID("writeJson"); +const ignoreHiddenLayersID = sID("ignoreHiddenLayers"); +const groupsAsSkinsID = sID("groupsAsSkins"); +const useRulerOriginID = sID("useRulerOrigin"); +const pngScaleID = sID("pngScale"); +const paddingID = sID("padding"); +const imagesDirID = sID("imagesDir"); +const projectDirID = sID("projectDir"); + +const currentLanguageID = sID("currentLanguage"); +const ignoreGrayLabelLayersID = sID("ignoreGrayLabelLayers"); +const ignoreStartsWithUnderscoreLayersID = sID("ignoreStartsWithUnderscoreLayers"); +const useRootLayerID = sID("useRootLayer"); +const mergeGroupsID = sID("mergeGroups"); +const allowTrimLayersID = sID("allowTrimLayers"); +const trimAwayLeftID = sID("trimAwayLeft"); +const trimAwayRightID = sID("trimAwayRight"); +const trimAwayTopID = sID("trimAwayTop"); +const trimAwayBottomID = sID("trimAwayBottom"); +const corpToCanvasID = sID("corpToCanvas"); +const enableRotationOptimizeID = sID("enableRotationOptimize"); +const updateToPSDID = sID("updateToPSD"); +const forceOptRotationID = sID("forceOptRotation"); +const rotateStepID = sID("rotateStep"); +const rotationChkThresholdID = sID("rotationChkThreshold"); +const rotationOptThresholdID = sID("rotationOptThreshold"); +const resampleID = sID("resample"); +const resampleToScreenDpiID = sID("resampleToScreenDpi"); + +// LOCALIZATION START -------------------------------------------------------------------------------- +const STR_ROOT_LAYER_NOT_FOUND = { + EN : "Root layer ('_ROOT') not found! Reset to default origin.", + TC : "找不到 Root 圖層 ('_ROOT')! 重置成預設設定.", + SC : "找不到 Root 图层 ('_ROOT')! 重置成预设设定." +} +const STR_OPEN_DOC_FIRST = { + EN : "Please open a document before running the LayersToPNG script.", + TC : "欲執行腳本請先開啟文件.", + SC : "欲运行脚本请先开启文件." +} +const STR_SAVE_DOC_FIRST = { + EN : "Please save the document before running the LayersToPNG script.", + TC : "欲執行腳本請先儲存文件.", + SC : "欲运行脚本请先储存文件." +} +const STR_WINDOW_TITLE = { + EN : "Spine LayersToPNG JH v" + JHVersion + " by JanusHuang", + TC : "Spine LayersToPNG JH v" + JHVersion + " by JanusHuang", + SC : "Spine LayersToPNG JH v" + JHVersion + " by JanusHuang" +} +const STR_WRITE_LAYERS_AS_PNGS = { + EN : " Write layers as PNGs", + TC : " 輸出圖層為 PNG", + SC : " 导出图层为 PNG" +} +const STR_WRITE_TEMPLATE_PNG = { + EN : " Write a template PNG", + TC : " 輸出範本 PNG", + SC : " 导出样本 PNG" +} +const STR_WRITE_SPINE_JSON = { + EN : " Write Spine JSON", + TC : " 輸出 Spine JSON", + SC : " 导出 Spine JSON" +} +const STR_WRITE_LAYER_OPACITY = { + EN : " Write layer opacity", + TC : " 寫入圖層透明度", + SC : " 写入图层透明度" +} +const STR_WRITE_LAYER_BLENDMODE = { + EN : " Write layer blendmode", + TC : " 寫入圖層混合模式", + SC : " 写入图层混合模式" +} +const STR_WRITE_CUSTOM_SCALE_AND_ROTATION = { + EN : " Write custom S/R ( #s/ #r)", + TC : " 寫入圖層自訂比例與旋轉值( #s/ #r)", + SC : " 写入图层自订比例与旋转值( #s/ #r)" +} +const STR_IGNORE_HIDDEN_LAYERS = { + EN : " Ignore hidden layers", + TC : " 忽略隱藏的圖層", + SC : " 忽略隐藏的图层" +} +const STR_IGNORE_STARTS_WITH_AN_UNDERSCORE = { + EN : " Ignore starts with an underscore", + TC : " 忽略名稱開頭為底線符號的圖層", + SC : " 忽略名称开头为底线符号的图层" +} +const STR_IGNORE_GRAY_LABEL_LAYERS = { + EN : " Ignore gray label layers", + TC : " 忽略顏色標籤為灰色的圖層", + SC : " 忽略颜色标签为灰色的图层" +} +const STR_TRIM_LAYERS = { + EN : " Trim layers", + TC : " 修剪圖層", + SC : " 修剪图层" +} +const STR_CORP_TO_CANVAS = { + EN : " Corp to canvas", + TC : " 裁切圖層以符合版面", + SC : " 裁切图层以符合版面" +} +const STR_USE_GROUPS_AS_SKINS = { + EN : " Use groups as skins", + TC : " 將第一層群組視為皮膚", + SC : " 将第一层群组视为皮肤" +} +const STR_ALLOW_GROUP_AS_SLOT = { + EN : " Allow group as slot ( #SLOT)", + TC : " 將群組視為SLOT( #SLOT)", + SC : " 将群组视为SLOT( #SLOT)" +} +const STR_MERGE_GROUPS = { + EN : " Merge groups", + TC : " 自動合併群組", + SC : " 自动合并群组" +} +const STR_ROTA_OPT_TITLE = { + EN : "Optimize image rotation", + TC : "图像角度優化", + SC : "图像角度优化" +} +const STR_ROTA_OPT_ENABLE = { + EN : " Enable", + TC : " 啟用", + SC : " 激活" +} +const STR_ROTA_OPT_UPDATE_TO_PSD = { + EN : " Update layer name", + TC : " 更新參數至圖層/群組名稱", + SC : " 更新参数至图层/群组名称" +} +const STR_ROTA_OPT_FORCE = { + EN : " Force optimize", + TC : " 強制優化所有圖層", + SC : " 强制优化所有图层" +} +const STR_ROTA_OPT_ROTATE_STEP = { + EN : " Precision (deg.)", + TC : " 檢測精度 (度)", + SC : " 检测精度 (度)" +} +const STR_ROTA_OPT_CHK_THRESHOLD = { + EN : " Detect THOLD.(sq px)", + TC : " 檢測門檻(平方像素)", + SC : " 检测门槛(平方像素)" +} +const STR_ROTA_OPT_OPT_THRESHOLD = { + EN : " Optimize THOLD.(sq px)", + TC : " 優化門檻(平方像素)", + SC : " 优化门槛(平方像素)" +} +const STR_RESAMPLE_TO_SCREEN_DPI = { + EN : " Resample to " + screenDpi + " dpi", + TC : " 重新取樣至 " + screenDpi + " dpi", + SC : " 重新取样至 " + screenDpi + " dpi" +} +const STR_TRIM_AWAY_TITLE = { + EN : "Trim Away", + TC : "修剪部位", + SC : "修剪部位" +} +const STR_TRIM_AWAY_LEFT = { + EN : " Left", + TC : " 左邊", + SC : " 左边" +} +const STR_TRIM_AWAY_RIGHT = { + EN : " Right", + TC : " 右邊", + SC : " 右边" +} +const STR_TRIM_AWAY_TOP = { + EN : " Top", + TC : " 頂部", + SC : " 顶部" +} +const STR_TRIM_AWAY_BOTTOM = { + EN : " Bottom", + TC : " 底部", + SC : " 底部" +} +const STR_ORIGIN_SETTINGS_TITLE = { + EN : "Origin Settings (as 0,0)", + TC : "Spine 原點設定", + SC : "Spine 原点设定" +} +const STR_ORIGIN_DEFAULT = { + EN : " Default (LB)", + TC : " 預設(PS版面左下)", + SC : " 预设(PS版面左下)" +} +const STR_ORIGIN_USE_PS_RULER = { + EN : " Use ruler origin", + TC : " PS尺標原點", + SC : " PS尺标原点" +} +const STR_ORIGIN_USE_ROOT_LAYER = { + EN : " Use '_ROOT' layer", + TC : " 依據_ROOT圖層", + SC : " 依据_ROOT图层" +} +const STR_RESAMPLE_METHOD_TITLE = { + EN : "Resample Method", + TC : "重新取樣演算法", + SC : "重新取样演算法" +} +// "Bicubic", "Bicubic Sharper", "Bicubic Smoother", "Bilinear", "Nearest Neighbor" +const STR_RESAMPLE_METHOD_AUTOMATIC = { + EN : "Automatic", + TC : "Automatic",//自動 + SC : "Automatic" +} +const STR_RESAMPLE_METHOD_BICUBIC_AUTOMATIC = { + EN : "Bicubic Automatic", + TC : "Bicubic Automatic",//環迴增值法自動選擇 + SC : "Bicubic Automatic" +} +const STR_RESAMPLE_METHOD_BICUBIC = { + EN : "Bicubic", + TC : "Bicubic",//環迴增值法(適合平滑漸層) + SC : "Bicubic" +} +const STR_RESAMPLE_METHOD_BICUBIC_SHARPER = { + EN : "Bicubic Sharper", + TC : "Bicubic Sharper",//環迴增值法更銳利(適合縮小) + SC : "Bicubic Sharper" +} +const STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER = { + EN : "Bicubic Smoother", + TC : "Bicubic Smoother",//環迴增值法更平滑(適合放大) + SC : "Bicubic Smoother" +} +const STR_RESAMPLE_METHOD_BILINEAR = { + EN : "Bilinear", + TC : "Bilinear",//縱橫增值法 + SC : "Bilinear" +} +const STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR = { + EN : "Nearest Neighbor", + TC : "Nearest Neighbor",//最接近像素(保留硬邊) + SC : "Nearest Neighbor" +} +const STR_RESAMPLE_METHOD_NONE = { + EN : "None", + TC : "None",//無 + SC : "None" +} +const STR_RESAMPLE_METHOD_PRESERVEDETAILS = { + EN : "Preseve Details", + TC : "Preseve Details",//保持細節 + SC : "Preseve Details" +} +const resampleMethodsDdlList = [ + STR_RESAMPLE_METHOD_BICUBIC_SHARPER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC[currentLanguage], + STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR[currentLanguage], + STR_RESAMPLE_METHOD_BILINEAR[currentLanguage] + ]; +const resampleMethodsDdlList_CC = [ + STR_RESAMPLE_METHOD_AUTOMATIC[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_PRESERVEDETAILS[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_BICUBIC_AUTOMATIC[currentLanguage], // Not support in CS6 + STR_RESAMPLE_METHOD_BICUBIC_SHARPER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC_SMOOTHER[currentLanguage], + STR_RESAMPLE_METHOD_BICUBIC[currentLanguage], + STR_RESAMPLE_METHOD_NEAREST_NEIGHBOR[currentLanguage], + STR_RESAMPLE_METHOD_BILINEAR[currentLanguage], + STR_RESAMPLE_METHOD_NONE[currentLanguage] + ]; +const STR_PNG_SCALE = { + EN : "PNG scale", + TC : "PNG比例", + SC : "PNG比例" +} +const STR_PADDING = { + EN : "Padding", + TC : "邊距", + SC : "边距" +} +const STR_OUTPUT_DIRECTORIES_TITLE = { + EN : "Output directories", + TC : "輸出目錄", + SC : "导出目录" +} +const STR_OUTPUT_DIRECTORIES_IMAGES = { + EN : "Images", + TC : "圖像", + SC : "图像" +} +const STR_OUTPUT_DIRECTORIES_JSON = { + EN : "JSON", + TC : "JSON", + SC : "JSON" +} +const STR_OUTPUT_DIRECTORIES_HINT = { + EN : "Begin paths with \"./\" to be relative to the PSD file.", + TC : "路徑以 \"./\" 起始時會視為相對路徑.", + SC : "路径以 \"./\" 起始时会视为相对路径." +} +const STR_BUTTON_OK = { + EN : "OK", + TC : "確認", + SC : "确认" +} +const STR_BUTTON_CANCEL = { + EN : "Cancel", + TC : "取消", + SC : "取消" +} +const STR_JH_HELP = { + EN : "Adding suffix...\n\ +◆ Define scale and rotation by adding suffixes to layer name.\n\ +\t∙ Scale suffix ' #s'.\n\ +\t∙ Rotation suffix ' #r'.\n\ +\tex. 'thisislayername #s0.5'\r\t\t(scale = 50%)\n\ +\tex. 'thisislayername #s0.8 #r-12'\r\t\t(scale = 80%, rotation = -12 degree)\n\ +◆ Set group as slot by adding suffix ' #SLOT' to group name, and the layers/groups inside will be attchments.\n\ +\tex. 'thisisgroupname #SLOT'\n\ +*** Suffix is case-sensitive.", + TC : "圖層名稱後綴說明\n\ +◆ 自訂圖層輸出比例與旋轉 :\r為圖層名稱添加比例或旋轉後綴,輸出時會套用到圖層上\n\ +\t∙ 比例後綴 ' #s'.\n\ +\t∙ 旋轉後綴 ' #r'.\n\ +\tex. 'layer1 #s0.5'\r\t\t(縮小成50%)\n\ +\tex. 'layer2 #s0.8 #r-12'\r\t\t(縮小成80%, 旋轉-12度)\n\ +◆ 將群組視為 SLOT :\r為群組名稱添加後綴 ' #SLOT', 輸出時會把該群組視為 SLOT, 群組內的圖層或群組則各別輸出成PNG(Spine附件).\n\ +\tex. 'group1 #SLOT'\n\ +*** 注意後綴有區分大小寫.\r\ +*** 注意後綴皆以空白為開頭.", + SC : "图层名称后缀说明\n\ +◆ 自订图层导出比例与旋转 :\r为图层名称添加比例或旋转后缀,导出时会应用到图层上\n\ +\t∙ 比例后缀 ' #s'.\n\ +\t∙ 旋转后缀 ' #r'.\n\ +\tex. 'layer1 #s0.5'\r\t\t(缩小成50%)\n\ +\tex. 'layer2 #s0.8 #r-12'\r\t\t(缩小成80%, 旋转-12度)\n\ +◆ 将群组视为 SLOT :\r为群组名称添加后缀 ' #SLOT', 导出时会把该群组视为 SLOT, 群组内的图层或群组则个别导出成PNG(Spine附件).\n\ +\tex. 'group1 #SLOT'\n\ +*** 注意后缀有区分大小写.\r\ +*** 注意后缀皆以空白为开头." +} +const STR_DONE_HEAD = { + EN : "Done!\nSkins", + TC : "完成!\n皮膚", + SC : "完成!\n皮肤" +} +const STR_DONE_TOTAL_EXPORTED = { + EN : "Total exported : ", + TC : "輸出總計 : ", + SC : "导出总计 : " +} +const STR_DONE_RESAMPLE_METHOD = { + EN : "Resample method : ", + TC : "重新取樣演算法 : ", + SC : "重新取样演算法 : " +} +const STR_DONE_ELAPSED = { + EN : "Elapsed time : ", + TC : "耗時 : ", + SC : "耗时 : " +} +const STR_PNG_SCALE_MUST_BE_BETWEEN = { + EN : "PNG scale must be between > 0 and <= 100.", + TC : "PNG 比例必須介於 0 與 100.", + SC : "PNG 比例必须介于 0 与 100." +} +const STR_PNG_PADDING_MUST_BE = { + EN : "Padding must be >= 0.", + TC : "邊距必須大於零.", + SC : "边距必须大于零" +} +const STR_UNEXPECTED_ERROR_HAS_OCCURRED = { + EN : "An unexpected error has occurred.\n\ +To debug, run the LayersToPNG script using Adobe ExtendScript with \"Debug > Do not break on guarded exceptions\" unchecked.", + TC : "發生了未知的錯誤.\n\ +To debug, run the LayersToPNG script using Adobe ExtendScript with \"Debug > Do not break on guarded exceptions\" unchecked.", + SC : "发生了未知的错误.\n\ +To debug, run the LayersToPNG script using Adobe ExtendScript with \"Debug > Do not break on guarded exceptions\" unchecked." +} +const STR_CHANGE_TO_RGB_MODE = { + EN : "Please change the image mode to RGB color.", + TC : "請將圖像轉為 RGB 色彩模式", + SC : "请将图像转为 RGB 色彩模式" +} +// LOCALIZATION END -------------------------------------------------------------------------------- + +// For debug. +startTime = new Date().getTime(); + +var originalDoc; +try { + originalDoc = app.activeDocument; +} catch (ignored) {} + +var settings, progress; + +loadSettings(); +showDialog(); + +// RUN ---------------------------------------------------------------------------------------------------- +// Execute while OK button pressed. +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 + "/"); + + // Save For Web options. + var outputExtension = ".png"; + var sfwOptions; + sfwOptions = new ExportOptionsSaveForWeb(); + sfwOptions.format = SaveDocumentType.PNG; + // Save to PNG24. + sfwOptions.PNG8 = false; + + // Duplicate current document for operating. + activeDocument.duplicate(); + deselectLayers(); + + try { + convertToRGB(); + } catch (ignored) {} + if (app.activeDocument.mode != DocumentMode.RGB) { + alert(STR_CHANGE_TO_RGB_MODE); + return; + } + + // Resample to screen dpi (72dpi) automatically + if (resampleToScreenDpi && activeDocument.resolution != screenDpi) activeDocument.resizeImage(null, null, screenDpi, ResampleMethod.AUTOMATIC); + + var xOffSet = 0, yOffSet = 0; + if (useRootLayer) { + // Custom Origin : set origin by '_ROOT' layer. + // Get root layer. + try{ + var rootLayer = app.activeDocument.layers.getByName(rootLayerName); + } catch (e) { + alert(STR_ROOT_LAYER_NOT_FOUND[currentLanguage]); + useRootLayer = false; + } + // Get center position of root layer's bounds. + if (rootLayer) { + var rootLayerCenter = getBoundsCenter(rootLayer); + xOffSet = rootLayerCenter[0]; + yOffSet = rootLayerCenter[1]; + } + } else if (useRulerOrigin) { + // Get ruler origin. + var ref = new ActionReference(); + ref.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt")); + var desc = executeActionGet(ref); + xOffSet = desc.getInteger(sID("rulerOriginH")) >> 16; + yOffSet = desc.getInteger(sID("rulerOriginV")) >> 16; + } + + // Output template image. + if (writeTemplate) { + if (pngScale != 1) { + scaleImage(); + storeHistory(); + } + + var file = new File(absImagesDir + "template" + outputExtension); + if (file.exists) file.remove(); + + activeDocument.exportDocument(file, ExportType.SAVEFORWEB, sfwOptions); + + if (pngScale != 1) restoreHistory(); + } + + if (writeJson == false && writePngs == false) { + activeDocument.close(SaveOptions.DONOTSAVECHANGES); + return; + } + + // Merge all groups, except slot groups and skin group while groupsAsSkins. + if (mergeGroups) { + mergeDoc = activeDocument; + var groups = []; + // Add a layer at top to prevent occur error while merging group at top. + activeDocument.artLayers.add(); + collectGroups(activeDocument, groups); + mergeEachGroups(groups); + + activeDocument.duplicate(); + + if (mergeDoc) mergeDoc.close(SaveOptions.DONOTSAVECHANGES); + } + + // Rasterize all layers. + try { + executeAction(sID( "rasterizeAll" ), undefined, DialogModes.NO); + } catch (ignored) {} + + // Collect and hide layers. + var layers = []; + var layersOpacity = {}; + var layersBlend = {}; + collectLayers(activeDocument, layers, layersOpacity, layersBlend); + var layersCount = layers.length; + + storeHistory(); + + // Store the slot names and layers for each skin. + var slots = {}; + var skins = { "default": [] }; + var skinsOnlySlots = { "default": [] }; + var lastSlotName = ""; + + for (var i = layersCount - 1; i >= 0; i--) { + var layer = layers[i]; + var currentSlotName = ""; + + // Use groups as skin names. + var potentialSkinName; + if (isGroupAsSlot(layer.parent)) { + // Current layer is in slot group. + potentialSkinName = trim(layer.parent.parent.name); + currentSlotName = layerName(layer.parent) + } else { + // Current layer is in skin group. + potentialSkinName = trim(layer.parent.name); + currentSlotName = layerName(layer) + } + var layerGroupSkin = potentialSkinName.indexOf(patternNoSkin) == -1; + var skinName = (groupsAsSkins && layer.parent.typename == "LayerSet" && layerGroupSkin) ? potentialSkinName : "default"; + + // Store skins' layers + var skinLayers = skins[skinName]; + if (!skinLayers) skins[skinName] = skinLayers = []; + skinLayers[skinLayers.length] = layer; + + // Store skins' slots + if (lastSlotName != currentSlotName) { + var skinSlots = skinsOnlySlots[skinName]; + if (!skinSlots) skinsOnlySlots[skinName] = skinSlots = []; + skinSlots[skinSlots.length] = currentSlotName; + } + + slots[currentSlotName] = true; + + lastSlotName = currentSlotName; + } + + // Resort slots order to fix the orders while groupsAsSkins. + if (groupsAsSkins) slots = resortSlotsOrder(slots, skinsOnlySlots); + + // Output skeleton and bones -------------------------------------------------- + var json = '{\n"skeleton": { "images": "' + relImagesDir + '" },'; + json += '\n"bones": [\n\t{ "name": "root" }\n],\n'; + + // Output slots data -------------------------------------------------- + json += '"slots": [\n'; + var slotsCount = countAssocArray(slots); + var slotIndex = 0; + + // Output template slot to JSON. + if (writeTemplate) { + json += '\t{ "name": "' + "template" + '", "bone": "root", "attachment": "' + "template" + '" }'; + json += slotsCount > 0 ? ",\n" : "\n"; + } + + // Output slots ---------------------------------------------------------------------------------------------------- + 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; + } + } + // Write slot name and attachment name + json += '\t{ "name": "' + slotName + '", "bone": "root", "attachment": "' + attachmentName + '"'; + // Write opacity + if (layersOpacity[slotName] != null) { + // Convert opacity value from 0-100 to 0-255. + json += ', "color": "ffffff' + Math.round(layersOpacity[slotName] * 2.55).toString(16) + '"'; + } + // Write blendmode + if (layersBlend[slotName] != null) { + json += ', "blend": "' + layersBlend[slotName] + '"'; + } + json += ' }'; + slotIndex++; + json += slotIndex < slotsCount ? ",\n" : "\n"; + } + + // Output skins ---------------------------------------------------------------------------------------------------- + json += '],\n"skins": {\n'; + + var skinsCount = countAssocArray(skins); + var skinIndex = 0; + docWidth = activeDocument.width.as("px"); + docHeight = activeDocument.height.as("px"); + + // For debug + var imagesExportedCount = 0; + var imagesExportedCountTotal = 0; + var doneString = STR_DONE_HEAD[currentLanguage] + " ----------"; + + for (var skinName in skins) { + if (!skins.hasOwnProperty(skinName)) continue; + + var skinLayers = skins[skinName]; + var skinLayersCount = skinLayers.length; + var skinLayerIndex = 0; + + json += '\t"' + skinName + '": {'; + if (skinLayersCount > 0) json += "\n"; + + // Output template in skin to JSON + var templateX = docWidth / 2; + var templateY = docHeight / 2; + if (useRulerOrigin || useRootLayer) { + templateX -= xOffSet; + templateY -= (docHeight - yOffSet); + } + if (writeTemplate && skinName == "default") { + json += '\t\t"' + "template" + '": {\n\t\t\t"' + "template" + '": {' + json += '"x": ' + templateX + ',"y": ' + templateY; + json += ',"width": ' + docWidth + ',"height": ' + docHeight + '}\n\t\t}'; + json += skinLayersCount > 0 ? ",\n" : "\n"; + } + + var layersInSlotIndex = 0; + + for (var i = skinLayersCount - 1; i >= 0; i--) { + var layer = skinLayers[i]; + + var sourceLayerName = srcLayerName(layer); + var slotName; + var currentLayerName = layerName(layer); + + if (isGroupAsSlot(layer.parent)) { + // Target layer's parent is group as slot + slotName = layerName(layer.parent); + } else { + slotName = currentLayerName; + } + + var placeholderName, attachmentName; + if (skinName == "default") { + placeholderName = currentLayerName; + attachmentName = currentLayerName; + } else { + placeholderName = currentLayerName; + attachmentName = skinName + "/" + currentLayerName; + } + + layer.visible = true; + + // Get data from bounds + var x = docWidth * 0.5; + var y = docHeight * 0.5; + var boundsCenter = getBoundsCenter(layer); + if (allowTrimLayers) { + x = boundsCenter[0]; + y = boundsCenter[1]; + } + + var customRotate = 0; + var customScale = 1; + + // Get custom scale and rotation from layer name + if (forceOptRotation || hasCustomParameters(sourceLayerName)) { + customRotate = getRotate(layer); + customScale = getScale(layer); + // Prevent resolution of output image too small + if (customScale != 1) customScale = checkMinimumCustomScale(layer, customScale); + } + + // Rotate image + if (customRotate != 0) { + layer.rotate(customRotate, AnchorPosition.MIDDLECENTER); + var rotatedCenter = getBoundsCenter(layer); + var fixedCenter = fixRotatedOffset(rotatedCenter, boundsCenter, customRotate); + x = fixedCenter[0]; + y = fixedCenter[1]; + } + + // Corp to canvas and trim layer + if (corpToCanvas == false || customRotate != 0) extendCanvas(layer); + // .trim(trimtype, top, left, bottom, right) + if (layer.isBackgroundLayer == false && allowTrimLayers) activeDocument.trim(TrimType.TRANSPARENT, false, false, trimAwayBottom, trimAwayRight); + if (corpToCanvas && customRotate == 0) { + x = getCorpBoundsCenter(layer)[0]; + y = getCorpBoundsCenter(layer)[1]; + } + if (layer.isBackgroundLayer == false && allowTrimLayers) activeDocument.trim(TrimType.TRANSPARENT, trimAwayTop, trimAwayLeft, false, false); + + // Scale image + if (pngScale != 1 || customScale != 1) { + scaleImage(customScale); + } + + // Padding + if (padding > 0) { + var canvasWidth = activeDocument.width.as("px") + padding * 2; + var canvasHeight = activeDocument.height.as("px") + padding * 2; + activeDocument.resizeCanvas(canvasWidth, canvasHeight, AnchorPosition.MIDDLECENTER); + } + + // Save image + if (writePngs) { + if (skinName != "default") new Folder(absImagesDir + skinName).create(); + activeDocument.exportDocument(new File(absImagesDir + attachmentName + outputExtension), ExportType.SAVEFORWEB, sfwOptions); + imagesExportedCount += 1; + } + + restoreHistory(); + layer.visible = false; + + // Shift root position, Convert root position's coordinate from Left-Top(Photoshop) to Left-Bottom(Spine) + y = docHeight - y; + // Calculate root position offset + if (useRulerOrigin || useRootLayer) { + x -= xOffSet; + y -= (docHeight - yOffSet); + } + + var outScale = mathRound(1 / customScale, 2); + + // Write JSON + // example : + // "emo_brows": { + // "emo_brows-hit": { "name": "skin1/emo_brows-hit", "x": 418,"y": 713,"width": 100,"height": 60 }, + // "emo_brows-win": { "name": "skin1/emo_brows-win", "x": 410,"y": 710,"width": 103,"height": 63 } + // }, + if (layersInSlotIndex == 0) { + json += '\t\t"' + slotName + '": {'; + } + + if (attachmentName == placeholderName) { + json += '\n\t\t\t"' + placeholderName + '": {'; + } else { + json += '\n\t\t\t"' + placeholderName + '": { "name": "' + attachmentName + '", '; + } + + // Write position + json += '"x": ' + mathRound(x, 1) + ',"y": ' + mathRound(y, 1); + // Write custom scale + if (customScale != 1) json += ',"scaleX": ' + outScale + ',"scaleY": ' + outScale; + // Write custom rotate + if (customRotate != 0) json += ',"rotation": ' + customRotate; + // Write size + json += ',"width": ' + Math.round(canvasWidth) + ',"height": ' + Math.round(canvasHeight); + // Write end of code + skinLayerIndex++; + if (isGroupAsSlot(layer.parent) && layer.parent.layers && layersInSlotIndex < getVisibleSiblingLayersCount(layer, skinLayers) - 1) + { + // Run next layer in slot + json += ' },' + layersInSlotIndex++; + } else { + // End the slot + json += ' }\n\t\t}'; + layersInSlotIndex = 0; + json += skinLayerIndex < skinLayersCount ? ",\n" : "\n"; + } + } + if (skinLayersCount > 0) json += "\t"; + json += "\}"; + + // For debug + imagesExportedCountTotal += imagesExportedCount; + doneString += "\n\t" + skinName + ": " + imagesExportedCount; + imagesExportedCount = 0; + + skinIndex++; + json += skinIndex < skinsCount ? ",\n" : "\n"; + } + + // Output animations. -------------------------------------------------- + json += '},\n"animations":{\n\t"animation": {}\n}\n}'; + + if (autoCloseDoc) 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(); + } + + // Rename layers with new rotate value. + if (updateToPSD) updateCustomRotaToLayerName(); + + // For debug + doneString += "\n- " + STR_DONE_TOTAL_EXPORTED[currentLanguage] + imagesExportedCountTotal; + doneString += "\n\n" + STR_DONE_RESAMPLE_METHOD[currentLanguage] + resampleObject.toString(); + endTime = new Date().getTime(); + var elapsedTime = mathRound(( endTime - startTime ) / 1000, 0); + doneString += "\n" + STR_DONE_ELAPSED[currentLanguage] + elapsedTime + "s"; + alert(doneString); +} + +// Dialog and settings -------------------------------------------------------------------------------- + +function showDialog () { + if (!originalDoc) { + alert(STR_OPEN_DOC_FIRST[currentLanguage]); + return; + } + if (!hasFilePath()) { + alert(STR_SAVE_DOC_FIRST[currentLanguage]); + return; + } + + var dialog = new Window("dialog", STR_WINDOW_TITLE[currentLanguage]); + 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, STR_WRITE_LAYERS_AS_PNGS[currentLanguage]); + writePngsCheckbox.value = writePngs; + var writeTemplateCheckbox = group.add("checkbox", undefined, STR_WRITE_TEMPLATE_PNG[currentLanguage]); + writeTemplateCheckbox.value = writeTemplate; + var writeJsonCheckbox = group.add("checkbox", undefined, STR_WRITE_SPINE_JSON[currentLanguage]); + writeJsonCheckbox.value = writeJson; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_LAYER_OPACITY[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_LAYER_BLENDMODE[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var emptyCheckbox = group.add("checkbox", undefined, STR_WRITE_CUSTOM_SCALE_AND_ROTATION[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var corpToCanvasCheckbox = group.add("checkbox", undefined, STR_CORP_TO_CANVAS[currentLanguage]); + corpToCanvasCheckbox.value = corpToCanvas; + var allowTrimLayersCheckbox = group.add("checkbox", undefined, STR_TRIM_LAYERS[currentLanguage]); + allowTrimLayersCheckbox.value = allowTrimLayers; + group = checkboxGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "left"; + var ignoreHiddenLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_HIDDEN_LAYERS[currentLanguage]); + ignoreHiddenLayersCheckbox.value = ignoreHiddenLayers; + var ignoreStartsWithUnderscoreLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_STARTS_WITH_AN_UNDERSCORE[currentLanguage]); + ignoreStartsWithUnderscoreLayersCheckbox.value = ignoreStartsWithUnderscoreLayers; + var ignoreGrayLabelLayersCheckbox = group.add("checkbox", undefined, STR_IGNORE_GRAY_LABEL_LAYERS[currentLanguage]); + ignoreGrayLabelLayersCheckbox.value = ignoreGrayLabelLayers; + var groupsAsSkinsCheckbox = group.add("checkbox", undefined, STR_USE_GROUPS_AS_SKINS[currentLanguage]); + groupsAsSkinsCheckbox.value = groupsAsSkins; + var emptyCheckbox = group.add("checkbox", undefined, STR_ALLOW_GROUP_AS_SLOT[currentLanguage]); + emptyCheckbox.value = true; + emptyCheckbox.enabled = false; + var mergeGroupsCheckbox = group.add("checkbox", undefined, STR_MERGE_GROUPS[currentLanguage]); + mergeGroupsCheckbox.value = mergeGroups; + var resampleToScreenDpiCheckbox = group.add("checkbox", undefined, STR_RESAMPLE_TO_SCREEN_DPI[currentLanguage]); + resampleToScreenDpiCheckbox.value = resampleToScreenDpi; + var emptyCheckbox = group.add("checkbox", undefined, " Empty"); + emptyCheckbox.visible = false; + emptyCheckbox.enabled = false; + /* + var emptyCheckbox = group.add("checkbox", undefined, " Empty"); + emptyCheckbox.visible = false; + emptyCheckbox.enabled = false; + */ + + var trimAwayGroup = dialog.add("panel", undefined, STR_TRIM_AWAY_TITLE[currentLanguage]); + trimAwayGroup.orientation = "row"; + trimAwayGroup.margins = [10,15,10,10]; + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + var trimAwayLeftCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_LEFT[currentLanguage]); + trimAwayLeftCheckbox.value = trimAwayLeft; + var trimAwayRightCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_RIGHT[currentLanguage]); + trimAwayRightCheckbox.value = trimAwayRight; + var trimAwayTopCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_TOP[currentLanguage]); + trimAwayTopCheckbox.value = trimAwayTop; + var trimAwayBottomCheckbox = trimAwayGroup.add("checkbox", undefined, STR_TRIM_AWAY_BOTTOM[currentLanguage]); + trimAwayBottomCheckbox.value = trimAwayBottom; + + allowTrimLayersCheckbox.onClick = function () { + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + }; + + var originGroup = dialog.add("panel", undefined, STR_ORIGIN_SETTINGS_TITLE[currentLanguage]); + originGroup.orientation = "row"; + originGroup.margins = [10,15,10,10]; + var useDefaultOriginCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_DEFAULT[currentLanguage]); + var useRulerOriginCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_USE_PS_RULER[currentLanguage]); + useRulerOriginCheckbox.value = useRulerOrigin; + var useRootLayerCheckbox = originGroup.add("radiobutton", undefined, STR_ORIGIN_USE_ROOT_LAYER[currentLanguage]); + useRootLayerCheckbox.value = useRootLayer; + if (useRulerOriginCheckbox.value == false && useRootLayerCheckbox.value == false) useDefaultOriginCheckbox.value = true; + + var rotaOptGroup = dialog.add("panel", undefined, STR_ROTA_OPT_TITLE[currentLanguage]); + rotaOptGroup.margins = [10,15,10,10]; + uiGroup = rotaOptGroup.add("group"); + uiGroup.orientation = "row" + uiGroup.alignment = ["fill", ""]; + group = uiGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "left"; + var enableRotationOptimizeCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_ENABLE[currentLanguage]); + enableRotationOptimizeCheckbox.value = enableRotationOptimize; + var updateToPSDCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_UPDATE_TO_PSD[currentLanguage]); + updateToPSDCheckbox.value = updateToPSD; + updateToPSDCheckbox.enabled = enableRotationOptimize; + var forceOptRotationCheckbox = group.add("checkbox", undefined, STR_ROTA_OPT_FORCE[currentLanguage]); + forceOptRotationCheckbox.value = forceOptRotation; + forceOptRotationCheckbox.enabled = enableRotationOptimize; + group = uiGroup.add("group"); + group.alignment = ["right", ""]; + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_ROTA_OPT_ROTATE_STEP[currentLanguage]); + group.add("statictext", undefined, STR_ROTA_OPT_CHK_THRESHOLD[currentLanguage]); + group.add("statictext", undefined, STR_ROTA_OPT_OPT_THRESHOLD[currentLanguage]); + group = uiGroup.add("group"); + group.alignment = ["fill", ""]; + group.orientation = "column"; + group.alignChildren = "right"; + var rotateStepText = group.add("edittext", undefined, rotateStep); + rotateStepText.characters = 8; + rotateStepText.enabled = enableRotationOptimize; + rotateStepText.onChanging = function () { + var tempTextValue = parseInt(rotateStepText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = rotateStep_DEF; + rotateStepText.text = tempTextValue; + } + var rotationChkThresholdText = group.add("edittext", undefined, rotationChkThreshold); + rotationChkThresholdText.characters = 8; + rotationChkThresholdText.enabled = enableRotationOptimize; + rotationChkThresholdText.onChanging = function () { + var tempTextValue = parseInt(rotationChkThresholdText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = rotationChkThreshold_DEF; + rotationChkThresholdText.text = tempTextValue; + } + var rotationOptThresholdText = group.add("edittext", undefined, rotationOptThreshold_DEF); + rotationOptThresholdText.characters = 8; + rotationOptThresholdText.enabled = enableRotationOptimize; + rotationOptThresholdText.onChanging = function () { + var tempTextValue = parseInt(rotationOptThresholdText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = 10; + rotationOptThresholdText.text = tempTextValue; + } + + enableRotationOptimizeCheckbox.onClick = function () { + updateToPSDCheckbox.enabled = enableRotationOptimizeCheckbox.value; + forceOptRotationCheckbox.enabled = enableRotationOptimizeCheckbox.value; + rotateStepText.enabled = enableRotationOptimizeCheckbox.value; + rotationChkThresholdText.enabled = enableRotationOptimizeCheckbox.value; + rotationOptThresholdText.enabled = enableRotationOptimizeCheckbox.value; + } + + var resampleGroup = dialog.add("group"); + group = resampleGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_RESAMPLE_METHOD_TITLE[currentLanguage]); + group = resampleGroup.add("group"); + group.alignment = ["fill", ""]; + group.orientation = "column"; + group.alignChildren = ["fill", ""]; + var resampleDdl; + if (isPSCC()) { + // Photoshop CC + resampleDdl = group.add("dropdownlist", undefined, resampleMethodsDdlList_CC); + } else { + // Photoshop CS6 and below + resampleDdl = group.add("dropdownlist", undefined, resampleMethodsDdlList); + } + + resampleDdl.selection = resample; + + var slidersGroup = dialog.add("group"); + group = slidersGroup.add("group"); + group.orientation = "column"; + group.alignChildren = "right"; + group.add("statictext", undefined, STR_PNG_SCALE[currentLanguage]); + group.add("statictext", undefined, STR_PADDING[currentLanguage]); + group = slidersGroup.add("group"); + group.orientation = "column"; + var scaleText = group.add("edittext", undefined, pngScale * 100); + scaleText.characters = 4; + scaleText.onChanging = function () { + } + 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 () { + var tempTextValue = parseInt(scaleText.text); + if (tempTextValue < 1 || tempTextValue > 100 || isNaN(tempTextValue)) tempTextValue = pngScale_DEF * 100; + scaleText.text = tempTextValue; + scaleSlider.value = scaleText.text; + }; + scaleSlider.onChanging = function () { + scaleText.text = Math.round(scaleSlider.value); + }; + paddingText.onChanging = function () { + var tempTextValue = parseInt(paddingText.text); + if (tempTextValue < 1 || isNaN(tempTextValue)) tempTextValue = padding_DEF; + paddingText.text = tempTextValue; + scaleSlider.value = paddingText.text; + }; + paddingSlider.onChanging = function () { + paddingText.text = Math.round(paddingSlider.value); + }; + + var outputGroup = dialog.add("panel", undefined, STR_OUTPUT_DIRECTORIES_TITLE[currentLanguage]); + 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, STR_OUTPUT_DIRECTORIES_IMAGES[currentLanguage]); + group.add("statictext", undefined, STR_OUTPUT_DIRECTORIES_JSON[currentLanguage]); + 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, STR_OUTPUT_DIRECTORIES_HINT[currentLanguage]).alignment = "center"; + + var group = dialog.add("group"); + // group.alignment = "center"; + var runButton = group.add("button", undefined, STR_BUTTON_OK[currentLanguage]); + var cancelButton = group.add("button", undefined, STR_BUTTON_CANCEL[currentLanguage]); + + cancelButton.onClick = function () { + dialog.close(0); + return; + }; + + // JH Version Help dialog. + var JHButton = group.add("button", undefined, "!"); + JHButton.size = [24,20]; + JHButton.onClick = function () { + alert(STR_JH_HELP[currentLanguage]); + }; + + // Languages Drop Down List + var languagesDdl = group.add("dropdownlist", undefined, languagesList); + languagesDdl.selection = languageCodeToIndex(); + languagesDdl.onChanging = function () { + currentLanguage = languagesListValue[languagesDdl.selection.index]; + 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; + corpToCanvas = corpToCanvasCheckbox.value; + mergeGroups = mergeGroupsCheckbox.value; + allowTrimLayers = allowTrimLayersCheckbox.value; + trimAwayGroup.enabled = allowTrimLayersCheckbox.value; + ignoreGrayLabelLayers = ignoreGrayLabelLayersCheckbox.value; + ignoreStartsWithUnderscoreLayers = ignoreStartsWithUnderscoreLayersCheckbox.value; + useRootLayer = useRootLayerCheckbox.value; + imagesDir = imagesDirText.text; + projectDir = projectDirText.text; + var paddingValue = parseInt(paddingText.text); + if (paddingValue >= 0) padding = paddingValue; + resample = resampleDdl.selection.index; + resampleObject = resampleDdl.selection; + trimAwayLeft = trimAwayLeftCheckbox.value; + trimAwayRight = trimAwayRightCheckbox.value; + trimAwayTop = trimAwayTopCheckbox.value; + trimAwayBottom = trimAwayBottomCheckbox.value; + currentLanguage = languagesListValue[languagesDdl.selection.index]; + resampleToScreenDpi = resampleToScreenDpiCheckbox.value; + enableRotationOptimize = enableRotationOptimizeCheckbox.value; + updateToPSD = updateToPSDCheckbox.value; + forceOptRotation = forceOptRotationCheckbox.value; + rotateStep = rotateStepText.text; + rotationChkThreshold = rotationChkThresholdText.text; + rotationOptThreshold = rotationOptThresholdText.text; + } + + dialog.onClose = function() { + updateSettings(); + saveSettings(); + }; + + runButton.onClick = function () { + if (scaleText.text <= 0 || scaleText.text > 100) { + alert(STR_PNG_SCALE_MUST_BE_BETWEEN[currentLanguage]); + return; + } + if (paddingText.text < 0) { + alert(STR_PNG_PADDING_MUST_BE[currentLanguage]); + return; + } + dialog.close(0); + + var rulerUnits = app.preferences.rulerUnits; + app.preferences.rulerUnits = Units.PIXELS; + try { + run(); + } catch (e) { + alert(STR_UNEXPECTED_ERROR_HAS_OCCURRED[currentLanguage]); + debugger; + } finally { + if (activeDocument != originalDoc && autoCloseDoc) 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(corpToCanvasID)) corpToCanvas = settings.getBoolean(corpToCanvasID); + if (settings.hasKey(mergeGroupsID)) mergeGroups = settings.getBoolean(mergeGroupsID); + if (settings.hasKey(allowTrimLayersID)) allowTrimLayers = settings.getBoolean(allowTrimLayersID); + if (settings.hasKey(ignoreGrayLabelLayersID)) ignoreGrayLabelLayers = settings.getBoolean(ignoreGrayLabelLayersID); + if (settings.hasKey(ignoreStartsWithUnderscoreLayersID)) ignoreStartsWithUnderscoreLayers = settings.getBoolean(ignoreStartsWithUnderscoreLayersID); + if (settings.hasKey(useRootLayerID)) useRootLayer = settings.getBoolean(useRootLayerID); + if (settings.hasKey(imagesDirID)) imagesDir = settings.getString(imagesDirID); + if (settings.hasKey(projectDirID)) projectDir = settings.getString(projectDirID); + if (settings.hasKey(paddingID)) padding = settings.getDouble(paddingID); + if (settings.hasKey(resampleID)) resample = settings.getDouble(resampleID); + if (settings.hasKey(trimAwayLeftID)) trimAwayLeft = settings.getBoolean(trimAwayLeftID); + if (settings.hasKey(trimAwayRightID)) trimAwayRight = settings.getBoolean(trimAwayRightID); + if (settings.hasKey(trimAwayTopID)) trimAwayTop = settings.getBoolean(trimAwayTopID); + if (settings.hasKey(trimAwayBottomID)) trimAwayBottom = settings.getBoolean(trimAwayBottomID); + if (settings.hasKey(currentLanguageID)) currentLanguage = settings.getString(currentLanguageID); + if (settings.hasKey(resampleToScreenDpiID)) resampleToScreenDpi = settings.getBoolean(resampleToScreenDpiID); + if (settings.hasKey(enableRotationOptimizeID)) enableRotationOptimize = settings.getBoolean(enableRotationOptimizeID); + if (settings.hasKey(updateToPSDID)) updateToPSD = settings.getBoolean(updateToPSDID); + if (settings.hasKey(forceOptRotationID)) forceOptRotation = settings.getBoolean(forceOptRotationID); + if (settings.hasKey(rotateStepID)) rotateStep = settings.getDouble(rotateStepID); + if (settings.hasKey(rotationChkThresholdID)) rotationChkThreshold = settings.getDouble(rotationChkThresholdID); + if (settings.hasKey(rotationOptThresholdID)) rotationOptThreshold = settings.getDouble(rotationOptThresholdID); +} + +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.putBoolean(corpToCanvasID, corpToCanvas); + settings.putBoolean(mergeGroupsID, mergeGroups); + settings.putBoolean(allowTrimLayersID, allowTrimLayers); + settings.putBoolean(ignoreGrayLabelLayersID, ignoreGrayLabelLayers); + settings.putBoolean(ignoreStartsWithUnderscoreLayersID, ignoreStartsWithUnderscoreLayers); + settings.putBoolean(useRootLayerID, useRootLayer); + settings.putString(imagesDirID, imagesDir); + settings.putString(projectDirID, projectDir); + settings.putDouble(paddingID, padding); + settings.putDouble(resampleID, resample); + settings.putBoolean(trimAwayLeftID, trimAwayLeft); + settings.putBoolean(trimAwayRightID, trimAwayRight); + settings.putBoolean(trimAwayTopID, trimAwayTop); + settings.putBoolean(trimAwayBottomID, trimAwayBottom); + settings.putString(currentLanguageID, currentLanguage); + settings.putBoolean(resampleToScreenDpiID, resampleToScreenDpi); + settings.putBoolean(enableRotationOptimizeID, enableRotationOptimize); + settings.putBoolean(updateToPSDID, updateToPSD); + settings.putBoolean(forceOptRotationID, forceOptRotation); + settings.putDouble(rotateStepID, rotateStep); + settings.putDouble(rotationChkThresholdID, rotationChkThreshold); + settings.putDouble(rotationOptThresholdID, rotationOptThreshold); + app.putCustomOptions(settingsID, settings, true); +} + +// Photoshop utility: + +function isPSCC () { + // Check Photoshop version is above CC or not. + return (parseInt(app.version) >= psVersion_CC); +} + +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); + layer.remove(); +} + +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 scaleImage (customScale) { + if (customScale == undefined) customScale = 1; + var imageSize = activeDocument.width.as("px"); + activeDocument.resizeImage(UnitValue(imageSize * pngScale * customScale, "px"), null, null, resampleMethods[resampleObject]); +} + +function rotateImage (customRotate) { + if (customRotate == undefined) customRotate = 0; + activeDocument.rotateCanvas(customRotate); +} + +function duplicateLayerToNewDocument(ID,filename) { + var ref = new ActionReference(); + ref.putIdentifier(cID("Lyr "), ID); + + var desc = new ActionDescriptor(); + desc.putReference(cID("null"), ref ); + + executeAction(cID("slct"), desc, DialogModes.NO ); + desc = new ActionDescriptor(); + ref = new ActionReference(); + ref.putClass( cID("Dcmn") ); + desc.putReference( cID("null"), ref ); + desc.putString( cID("Nm "), filename ); + var ref2 = new ActionReference(); + //This line does not work!!!!!!!!!!! + //It still wants the selected layer(s) + ref2.putIdentifier(cID("Lyr "), ID); + desc.putReference( cID("Usng"), ref2 ); + desc.putInteger( cID("Vrsn"), 5 ); + executeAction( cID("Mk "), desc, DialogModes.NO ); +} + +function getLayerID(layer){ + var tempVisible = layer.visible; + var result; + if (isPSCC()) { + // Photoshop CC + result = layer.id + } else { + activeDocument.activeLayer = layer; + // Assumes activeDocument and activeLayer + var ref = new ActionReference(); + ref.putEnumerated(cID("Lyr "), cID("Ordn"), cID("Trgt")); + d = executeActionGet(ref); + result = d.getInteger(cID('LyrI')); + } + layer.visible = tempVisible; + return result; +}; + +function fixRotatedOffset(rotatedCenter, originalCenter, degrees){ + // Get right position of rotated layer to write JSON data + var rcx = rotatedCenter[0]; + var rcy = rotatedCenter[1]; + var ocx = originalCenter[0]; + var ocy = originalCenter[1]; + // Angle must be counter-clockwise + var theta = toRadians(degrees * -1); + + // Translate point to origin + var tempX = ocx - rcx; + var tempY = ocy - rcy; + + // Now apply rotation + var rotatedX = parseFloat(tempX * Math.cos(theta) - tempY * Math.sin(theta)); + var rotatedY = parseFloat(tempX * Math.sin(theta) + tempY * Math.cos(theta)); + + // Position to JSON + var outX = ocx - rotatedX; + var outY = ocy - rotatedY; + + return [outX, outY]; +} + +function checkMinimumCustomScale (layer, customScale) { + // Prevent resolution of output image too small + var boundsSize = getBoundsSize(layer); + var result = customScale; + var minPixelsOrig = Math.min(boundsSize[0], boundsSize[1]); + var minPixels = minPixelsOrig * pngScale * customScale; + + if (minPixels < customScaleMin) result = Math.ceil(mathRound(customScaleMin / minPixelsOrig, 3)); + + return result; +} + +var historyIndex; +function storeHistory () { + historyIndex = activeDocument.historyStates.length - 1; +} +function restoreHistory () { + activeDocument.activeHistoryState = activeDocument.historyStates[historyIndex]; +} + +function isGroup (layer) { + return layer.typename == "LayerSet"; +} + +function collectLayers (layer, collect, collectOpacity, collectBlendMode) { + for (var i = 0, n = layer.layers.length; i < n; i++) { + var child = layer.layers[i]; + var childID = getLayerID(child); + // Get clean layer name (remove custom suffixes) + var cleanLayerName = layerName(child); + if (ignoreHiddenLayers && child.visible == false) { +// deleteLayer(child); + continue; + } + if (ignoreGrayLabelLayers && getLayerColorByID(childID) == "gray") { + child.visible = false; + continue; + } + if (ignoreStartsWithUnderscoreLayers && startsWith(child.name, "_")) { + child.visible = false; + continue; + } + if (child.bounds[2] == 0 && child.bounds[3] == 0) continue; + // Collect opacity + if (child.opacity != 100) { + // Opacity Types : child.opacity, child.fillOpacity + collectOpacity[cleanLayerName] = child.opacity; + child.opacity = 100; + } + // Collect blend mode + if (child.blendMode == BlendMode.SCREEN || child.blendMode == BlendMode.LINEARDODGE || child.blendMode == BlendMode.MULTIPLY) { + switch(child.blendMode) { + case BlendMode.SCREEN: + collectBlendMode[cleanLayerName] = "screen"; + break; + case BlendMode.LINEARDODGE: + collectBlendMode[cleanLayerName] = "additive"; + break; + case BlendMode.MULTIPLY: + collectBlendMode[cleanLayerName] = "multiply"; + break; + } + } + // Collect valid layers + if (child.layers && child.layers.length > 0) { + collectLayers(child, collect, collectOpacity, collectBlendMode); + } else if (child.kind == LayerKind.NORMAL) { + collect.push(child); + child.visible = false; + } + } +} + +function collectGroups (parent, collect) { + for (var i = 0; i < parent.layers.length; i++) { + var child = parent.layers[i]; + if (child.visible && child.layers && child.layers.length > 0) + { + // Current target is group + if (isGroupAsSlot(child)) { + // Group as slot, don't merge. + collectGroups(child, collect); + } else if (groupsAsSkins == false) { + // No skins, merge all groups. + collect.push(child); + } else if (child.parent.typename == "LayerSet") { + // Target's parent is layerSet + collect.push(child); + } else { + collectGroups(child, collect); + } + } + } +} + +function getVisibleSiblingLayersCount (layer, skinLayers) { + var visibleCount = 0; + for (var i = 0; i < layer.parent.layers.length; i++) { + for (var j = 0; j < skinLayers.length; j++) { + // Check layer's visibility by compares to skinLayers + if (layer.parent.layers[i] == skinLayers[j]) { + visibleCount += 1; + } + } + } + return visibleCount; +} + +function mergeEachGroups (groups) { + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + activeDocument.activeLayer = group; + group.merge(); + /* + if (isOutOfCanvas(group) && isPSCC() == false) { + // Extend canvas then restore, avoid corping pixels out of canvas while merging group at PS CS6 13.0 + // ^^^ This problem doesn't exist in PS CS6 Extended Version(13.0), so it occurs in what situation? + var extendDist = Math.abs(getOutOfCanvasDist(group)) * 2; + extendCanvas(group); + group.merge(); + applyLayerMask(); + shrinkCanvas(extendDist); + } else { + group.merge(); + } + */ + } +} + +function hasFilePath () { + var ref = new ActionReference(); + ref.putEnumerated(cID("Dcmn"), cID("Ordn"), cID("Trgt")); + return executeActionGet(ref).hasKey(sID("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; +} + +// Mathematics utility: + +function toRadians (degrees) { + return degrees * Math.PI / 180; +} + +function mathRound(num, decimal) { + var f = decimal > 0 ? Math.pow(10, decimal) : 1; + return Math.round(num * f) / f; +} + +// JavaScript utility: + +function cID (id) { + return charIDToTypeID(id); +} + +function sID (id) { + return stringIDToTypeID(id); +} + +function indexOfSmallest(ary) { + var lowest = 0; + for (var i = 1; i < ary.length; i++) { + if (ary[i] < ary[lowest]) lowest = i; + } + return lowest; +} + +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 stripSuffix (str, suffix) { + if (endsWith(str.toLowerCase(), suffix.toLowerCase())) str = str.substring(0, str.length - suffix.length); + return str; +} + +function layerName (layer) { + var result = stripSuffix(trim(layer.name), ".png").replace(/[:\/\\*\?\"\<\>\|]/g, ""); + // Get layer name without custom parameters string (customScale, customRotate, groupAsSlot). + var endIndex = result.indexOf(" #"); + if (endIndex != -1) { + result = result.substring(0, endIndex); + } + + return result +} + +function srcLayerName (layer) { + // Original version of layerName function + return stripSuffix(trim(layer.name), ".png").replace(/[:\/\\*\?\"\<\>\|]/g, ""); +} + +function isGroupAsSlot (layer) { + // Target is group with pattern, and parent is not as slot. + return (layer.name.indexOf(patternAsSlot) != -1 && layer.typename == "LayerSet" && isGroupAsSlot(layer.parent) == false) ? true : false; +} + +function resortSlotsOrder (slots, skins) { + // Fix slots' order while groupsAsSkins enabled. + // ^^^ Fix what wrong order?? This function output wrong order with default skin. All slots in default skin will on top all skins, but the original order doesn't get this problem. + +/* + var result = {}; + var resorted = []; + var exists = false; + + for (var slotName in slots) { + // first round. send 1 slot name to resorted array. + if (resorted.length == 0) { + resorted.push(slotName); + continue; + } + + // Find current slot in skins. + for (var skinName in skins) { + if (exists) break; + // Find current slot in current skin. + for (var i = 0; i < skins[skinName].length; i++) { + if (slotName == skins[skinName][i]) { + // Found slot's order in skins. + var insertIndex = 0; + + // Insert current slot after previous slot. + if (i > 0) { + // Get the slot before current slot in skins. + var previousSlotName = skins[skinName][i-1]; + for (var j = 0; j < resorted.length; j++) { + // Find the previous slot in resorted array, and get the next index. + if (resorted[j] == previousSlotName) { + insertIndex = j + 1; + break; + } + } + } + + // Insert current slot to resorted array. + resorted.splice(insertIndex, 0, slotName); + exists = true; + break; + } + } + } + exists = false; + } + + // Copy array to dictionary ** Why convert to dict?????? + for (var i in resorted) { + result[resorted[i]] = true; + } + + return result; +*/ + return slots; +} + +// Custom properties utility : + +function hasCustomParameters (layerName) { + var result = false; + if (layerName.search(patternRotate) != -1 || layerName.search(patternScale) != -1) { + result = true; + } + + return result; +} + +function getPropertyValue (layer, pattern) { + var layerName = layer.name; + var propertyValue; + // Return null while no valid property. + var result = null; + // Get last param in layer name. + var patternIndex = layerName.lastIndexOf(pattern); + + if (patternIndex != -1) { + // Parttern exists. + + var patternEndIndex = patternIndex + pattern.length; + var stringAfterPattern = layerName.substring( patternEndIndex, layerName.length ); + var propertyEndIndex = stringAfterPattern.indexOf(" "); + if (propertyEndIndex != -1) { + propertyValue = parseFloat(stringAfterPattern.substring(0, propertyEndIndex)); + } else { + propertyValue = parseFloat(stringAfterPattern); + } + result = propertyValue; + } + + // Return null while no vaild property. + // Return NaN while no vaild property value. + return result; +} + +function setPropertyValue (layer, pattern, value) { + var layerName = layer.name; + var newLayerName = ""; + // Get last param in layer name. + var patternIndex = layerName.lastIndexOf(pattern); + + if (patternIndex != -1) { + // Parttern exists. + var patternEndIndex = patternIndex + pattern.length; + var stringBeforeProperty = layerName.substring(0, patternIndex); + var stringAfterPattern = layerName.substring(patternEndIndex, layerName.length); + var propertyEndIndex = stringAfterPattern.indexOf(" "); + var stringAfterProperty = ""; + if (propertyEndIndex != -1) stringAfterProperty = stringAfterPattern.substring(propertyEndIndex, layerName.length); + + newLayerName = stringBeforeProperty + pattern + value + stringAfterProperty; + + layer.name = newLayerName; + } else if (forceOptRotation) { + newLayerName = layerName + pattern + value; + + layer.name = newLayerName; + } + + return newLayerName; +} + +function getScale (layer) { + var layerName = srcLayerName(layer); + var result = 1; + var value = getPropertyValue(layer, patternScale); + if (isNaN(value) == false) result = value; + + return result; +} + +function getRotate (layer) { + var layerName = srcLayerName(layer); + var result = 0; + var value = getPropertyValue(layer, patternRotate); + if (value) { + // Property is exists and has value, return the value. + result = value; + } else if (enableRotationOptimize) { + // Property is exists without value, generate value automatically. + if (isNaN(value) || forceOptRotation) result = autoOptimzeRotation(layer); + } + + return result; +} + +function autoOptimzeRotation (layer) { + if (getBoundsArea(layer) > rotationChkThreshold) { + var result = 0; +// var originalLayer = layer; + var tempRotaDocName = "OptimizingImageRotation"; + + // Duplicate layer to temp document to optimzing execution speed. + duplicateLayerToNewDocument(getLayerID(layer), tempRotaDocName); + var tempRotaDoc = app.documents.getByName(tempRotaDocName); + var tempLayer = tempRotaDoc.activeLayer; + + var boundsAreaList = new Array(); + + // If current layer is group, merge it first. + if (tempLayer.layers && tempLayer.layers.length > 0) tempLayer = tempLayer.merge(); + + // Save non-rotated and merged history state. + var nonRotatedAndMergedState = app.activeDocument.activeHistoryState; + + // Check layer's bounds area of each rotateStep. + if (rotateStep < 1) rotateStep = 1; + for (var i = 0; i < (rotateRange / rotateStep); i++) { + // Rotate layer. + if (i > 0) tempLayer.rotate(i * rotateStep, AnchorPosition.MIDDLECENTER); + + // Get bounds area and save to array. + var boundsArea = getBoundsArea(tempLayer); + boundsAreaList.push(boundsArea); + + // Restore non-rotated and merged history. + app.activeDocument.activeHistoryState = nonRotatedAndMergedState; + } + + tempRotaDoc.close(SaveOptions.DONOTSAVECHANGES); + + // Find minimum bounds area and get best degree. + var minIndex = indexOfSmallest(boundsAreaList); + var bestRotateValue = minIndex * rotateStep; + var boundsAreaDiff = Math.round(Math.sqrt(boundsAreaList[0] - boundsAreaList[minIndex])); + + if (boundsAreaDiff > rotationOptThreshold) { + // Save rotate value for layer renaming (UpdateToPSD) at end of script progress. + if (updateToPSD) updateRotaNameList[getLayerID(layer).toString()] = bestRotateValue; + result = bestRotateValue; + } + return result; + } +} + +function updateCustomRotaToLayerName () { + for (var id in updateRotaNameList) { + var layerID = parseInt(id); + selectLayerByID(layerID); + setPropertyValue(app.activeDocument.activeLayer, patternRotate, updateRotaNameList[id]); + } +} + +function getLayerColorByID( ID ) { + var ref = new ActionReference(); + ref.putProperty( cID("Prpr") ,sID('color')); + ref.putIdentifier(cID( "Lyr " ), ID ); + return typeIDToStringID(executeActionGet(ref).getEnumerationValue(sID('color'))); +} + +function selectLayerByID(layerID, add){ + var result = getLayerIndexByLayerID(layerID); + if(result){ + selectLayerByIndex(result, add); + }else{ + alert("Layer does not exist"); + } +}; +function selectLayerByIndex(index, add){ + add = undefined ? add = false:add + var ref = new ActionReference(); + ref.putIndex(cID("Lyr "), index); + var desc = new ActionDescriptor(); + desc.putReference(cID("null"), ref ); + if(add) desc.putEnumerated( sID( "selectionModifier" ), sID( "selectionModifierType" ), sID( "addToSelection" ) ); + desc.putBoolean( cID( "MkVs" ), false ); + try{ + executeAction(cID("slct"), desc, DialogModes.NO ); + }catch(e){ + alert(e.message); + } +}; +function getLayerIndexByLayerID(id) { + var ref = new ActionReference(); + ref.putProperty( cID("Prpr") , cID( "ItmI" )); + ref.putIdentifier( cID( "Lyr " ), id ); + try{ + return executeActionGet(ref).getInteger(cID( "ItmI" )); + }catch(e){ + return false; + } +}; + +// Bounds and Corp to canvas utility : + +function clampXToCanvas(num, axis) { + return Math.min( Math.max(num, 0), docWidth ); +} + +function clampYToCanvas(num, axis) { + return Math.min( Math.max(num, 0), docHeight ); +} + +function clampBoundsToCanvas(bounds) { + var x1 = clampXToCanvas(bounds[0]); + var y1 = clampYToCanvas(bounds[1]); + var x2 = clampXToCanvas(bounds[2]); + var y2 = clampYToCanvas(bounds[3]); + + return [x1, y1, x2, y2]; +} + +function getBoundsValue(layer) { + return [layer.bounds[0].value, layer.bounds[1].value, layer.bounds[2].value, layer.bounds[3].value]; +} + +function getBoundsSize(layer) { + var bounds = getBoundsValue(layer); + var width = bounds[2] - bounds[0]; + var height = bounds[3] - bounds[1]; + + return [width, height]; +} + +function getBoundsArea(layer) { + return (( layer.bounds[2] - layer.bounds[0] ) * ( layer.bounds[3] - layer.bounds[1] )); +} + +function getBoundsCenter(layer) { + var bounds = getBoundsValue(layer); + var size = getBoundsSize(layer); + var x = ( bounds[0] + size[0] / 2 ) * pngScale; + var y = ( bounds[1] + size[1] / 2 ) * pngScale; + + return [x, y]; +} + +function getCorpBoundsCenter(layer) { + var clampBounds = clampBoundsToCanvas(getBoundsValue(layer)); + var x1 = clampBounds[0]; + var y1 = clampBounds[1]; + var x2 = clampBounds[2]; + var y2 = clampBounds[3]; + var width = x2 - x1; + var height = y2 - y1; + var x = ( x1 + width / 2 ) * pngScale; + var y = ( y1 + height / 2 ) * pngScale; + + return [x, y]; +} + +function getOutOfCanvasDist(layer) { + var bounds = getBoundsValue(layer); + var x1 = bounds[0]; + var y1 = bounds[1]; + var x2 = activeDocument.width.as("px") - bounds[2]; + var y2 = activeDocument.height.as("px") - bounds[3]; + var dist = Math.min(x1, y1, x2, y2); + + return dist; +} + +function isOutOfCanvas(layer) { + if (getOutOfCanvasDist(layer) < 0) { + return true; + } else { + return false; + } +} + +function extendCanvas(layer) { + // Extend Canvas size for not corpping visible pixels + if (isOutOfCanvas(layer) == false) return; + var dist = Math.abs(getOutOfCanvasDist(layer)) * 2; + var w = activeDocument.width.as("px") + dist; + var h = activeDocument.height.as("px") + dist; + activeDocument.resizeCanvas(w, h, AnchorPosition.MIDDLECENTER); +} + +function shrinkCanvas(dist) { + // Extend Canvas size for not corpping visible pixels + var w = activeDocument.width.as("px") - dist; + var h = activeDocument.height.as("px") - dist; + activeDocument.resizeCanvas(w, h, AnchorPosition.MIDDLECENTER); +} + +/////////////////////////////////////////////////////////////////////////////// +// Function: applyLayerMask +// Usage: apply the vector mask on the current layer +// Input: Must have an open document +// Return: +/////////////////////////////////////////////////////////////////////////////// +function applyLayerMask() { + try{ + var id765 = cID( "Dlt " ); + var desc154 = new ActionDescriptor(); + var id766 = cID( "null" ); + var ref93 = new ActionReference(); + var id767 = cID( "Chnl" ); + var id768 = cID( "Ordn" ); + var id769 = cID( "Trgt" ); + ref93.putEnumerated( id767, id768, id769 ); + desc154.putReference( id766, ref93 ); + var id770 = cID( "Aply" ); + desc154.putBoolean( id770, true ); + executeAction( id765, desc154, DialogModes.NO ); + }catch(e) { + ; // do nothing + } +} + +function languageCodeToIndex() { + var currentLanguageIndex = 0; + for (var i = 0; i < languagesListValue.length; i++) { + if (currentLanguage == languagesListValue[i]) { + currentLanguageIndex = i; + } + } + return currentLanguageIndex; +} \ No newline at end of file diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG新版.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG新版.jsx new file mode 100644 index 0000000..21c5a65 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG新版.jsx @@ -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, ""); +} diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG旧版.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG旧版.jsx new file mode 100644 index 0000000..dd1ccb9 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/LayersToPNG旧版.jsx @@ -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(); +} diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/PhotoshopToSpine.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/PhotoshopToSpine.jsx new file mode 100644 index 0000000..615243e --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出Spine Json/PhotoshopToSpine.jsx @@ -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) : ""; + if (!cs2) { + jsonPathPreview.text = text; + jsonPathPreview.helpTip = text; + } else + jsonPathText.helpTip = text; + }; + imagesDirText.onChanging = function () { + var text = imagesDirText.text ? absolutePath(imagesDirText.text) : ""; + 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('"', '\\"') + '"'; +} diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出所有图层.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出所有图层.jsx new file mode 100644 index 0000000..e9bf788 --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/导出所有图层.jsx @@ -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); + } +}()); diff --git a/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/重命名图层.jsx b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/重命名图层.jsx new file mode 100644 index 0000000..141c6cb --- /dev/null +++ b/Mac工具/Yeomai-Toolbox-CEP/jsx/scripts/重命名图层.jsx @@ -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(); +} diff --git a/Mac工具/Yeomai-Toolbox-CEP插件.zip b/Mac工具/Yeomai-Toolbox-CEP插件.zip deleted file mode 100644 index 7b4ccee..0000000 Binary files a/Mac工具/Yeomai-Toolbox-CEP插件.zip and /dev/null differ