├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.sh ├── do_release.sh ├── doc_template ├── en.psd └── zh.psd ├── flatten_jsx.py ├── package-lock.json ├── package.json ├── pic.jpg ├── psd-to-labelplus-text.jsx ├── src ├── common.ts ├── copyleft_header.js ├── custom_options.ts ├── dialog_clear.ts ├── i18n.ts ├── importer.ts ├── jam │ ├── LICENSE.html │ ├── jamActions.jsxinc │ ├── jamBooks.jsxinc │ ├── jamColors.jsxinc │ ├── jamEngine.jsxinc │ ├── jamHelpers.jsxinc │ ├── jamJSON.jsxinc │ ├── jamLayers.jsxinc │ ├── jamShapes.jsxinc │ ├── jamStyles.jsxinc │ ├── jamText.jsxinc │ └── jamUtils.jsxinc ├── legacy.d.ts ├── main.ts ├── my_action.js ├── text_parser.ts ├── version.ts └── xtools │ ├── COPYRIGHT │ ├── ChangeLog │ ├── LICENSE │ ├── README │ ├── README.md │ └── xlib │ ├── GenericUI.jsx │ ├── LogWindow.js │ └── stdlib.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | ### Added 5 | ### Changed 6 | ### Fixed 7 | ### Removed 8 | 9 | 10 | ## [1.7.4] - 2024-09-21 11 | ### Changed 12 | - 兼容翻译文本编码格式UTF-8 with BOM或UTF-8,增强与其他兼容的工具导出的文本的兼容性,原先似乎与操作系统相关,win下为UTF-8 with BOM 13 | 14 | ## [1.7.3] - 2022-09-12 15 | ### Fixed 16 | - 修复载入字体配置功能失效的问题 17 | 18 | 19 | ## [1.7.2] - 2022-07-27 20 | 修复怨声载道的脚本崩溃问题... 21 | ### Fixed 22 | - 修复若不存在PS动作却保存了配置,脚本有可能崩溃的问题 23 | - 修复如果配置中存有当前系统不存在的字体,脚本崩溃的问题,问题很严重且高发,因为配置会自动储存、启动时装载,很多时候表现为启动时崩溃 24 | 25 | ## [1.7.1] - 2021-09-23 26 | ### Changed 27 | - 改进涂白功能填充颜色的精准度,之前取的点的颜色只采样单点,容易受文字附近的噪点干扰,已改为采样区域颜色 28 | ### Fixed 29 | - 修复涂白功能,Label位置正好在位于文字上时,会涂白整个页面的问题 30 | 31 | ## [1.7.0] - 2021-09-19 32 | 重写了涂白功能的逻辑,避免各种神奇问题。 33 | ### Added 34 | - 涂白功能,允许用户自定义容差 35 | - 打开翻译文本时,自动探测所在目录下是否存在“images”等文件夹,若存在则默认当作图源文件夹(配合萌翻的工程导出结构) 36 | ### Changed 37 | - 涂白功能,填充的颜色不再是白色,会自动取label所在的点的颜色来填充 38 | ### Fixed 39 | - 修复涂白功能,遇到粘连的框,会把两框相连部分的框涂白的问题 40 | - 修复涂白功能,对话框在4个角落时,整页都会被涂白的问题 41 | - 修复导出的PSD的Label图层分组,混合模式为“穿透”而不是“正常”的问题 42 | 43 | ## [1.6.1] - 2021-09-17 44 | ### Fixed 45 | - 修复涂白功能在高DPI图片下可能将对话框的边缘覆盖的问题 46 | 47 | ## [1.6.0] - 2021-03-14 48 | ### Added 49 | - 增强“预览匹配结果”按钮功能,显示名改为“检查图源匹配情况”,可以用它检查文件匹配(之前只在勾选“按顺序匹配图片”时可用),改用一个可滚动的文本框以显示大量内容 50 | - 执行导入前,先检查图源、模板文件是否全部存在,有缺失则不继续执行,避免在运行过程中才发现错误 51 | ### Changed 52 | - 启动时,将UI显示在屏幕中心 53 | - 改进“按顺序匹配图片文件”功能,会自动剔除非图片文件,之前匹配到到txt的情况不会再出现了 54 | - 发生未预料到的异常时,直接显示所有log方便排障 55 | ### Fixed 56 | - 修复“按顺序匹配图片文件”的“预览匹配结果”不停地弹出提示窗的问题 57 | - 修复当模板中的图层为相同名字时,不会被完全删除的问题(模板中原有的图层,在导入后应该被全部删除) 58 | 59 | ## [1.5.0] - 2020-05-19 60 | ### Added 61 | - 增加PS文档模板功能:使用模板可以方便地实现一些格式设置,以替代之前一部分需要录制动作的操作 62 | - PS文档功能UI选项:“自动”——根据当前系统语言自动选择模板;“不使用模板”——保持以前脚本的行为;“自定义模板” 63 | - PS文档功能支持为每个标签分组自定义文本图层的样式,只需在模板文档中添加与分组同名的图层 64 | - PS文档功能支持“dialog-overlay”图层,可自定义涂白覆盖层的样式 65 | - 导入时自动保存配置,下次打开脚本时自动加载上次的配置 66 | - 增加TIFF格式图片导入支持 67 | - 增加“输出格式”选项,脚本将自动转换导出的格式,此前脚本仅支持保存为PSD格式 68 | ### Changed 69 | - 此次更新配置项变更较大,不再兼容此前保存的ini配置文件,配置文件的格式改为JSON 70 | - 为配合模板功能,将“输出横排文字”选项改为“文字方向”,选项有“默认”、“横向”、“竖向” 71 | - UI,重新分类功能 72 | - UI,修改表述“忽略图片文件名”->“按顺序匹配图片文件 73 | - UI,“导出没有标号的文档”->“不输出未标号图片”,修改表述且默认输出全部图片,以免默认选项导致遗漏图片 74 | ### Fixed 75 | - 修复行距、间距组合等格式丢失的问题,通过引入PS文档模板功能解决 76 | - 修复PS动作不存在时报错的问题 77 | - 修复文件名中存在中括号时,lp文本文件解析不正常的问题(by Jason23347) 78 | - 修复路径中存在'%'时,脚本工作不正常的问题 79 | 80 | 81 | ## [1.4.1] - 2020-01-16 82 | ### Fixed 83 | - 修复“执行动作组”功能,保存配置项失效的问题 84 | 85 | ## [1.4.0] - 2020-01-12 86 | ### Changed 87 | - 发布时不再区分中、英文版本,由Photoshop中设置的语言自动切换 88 | - 改变设置行距功能的行为,从“字符的行距值”改为“段落自动行距的百分比值” 89 | - 默认启用设置行距功能(默认值120%) 90 | - UI优化:调整UI元素位置,简化文字描述 91 | 92 | ## [1.3.0] - 2020-01-05 93 | ### Added 94 | - 增加设置行距功能 95 | 96 | ### Changed 97 | - 修改表述"处理无标号文档"->“导出没有标号的文档” 98 | - “导出没有标号的文档”选项默认值为真,避免漏页 99 | 100 | ### Fixed 101 | - 修复UI上不显示“使文字方向为横向”选项的问题 102 | - 修复指定图源后缀名的JPEG没加“.”,导致文件名替换不正常的问题 103 | - 修复文本行距默认可能不为“自动”的问题 104 | 105 | ## [1.2.2] - 2017-12-27 106 | ### Fixed 107 | - 修复1.2.1脚本界面在mac下中文乱码的问题(发布时未转换成utf8) 108 | - 修复导出过程中遇到ps无法打开的文件时报错崩溃的问题, 将所有导出过程中捕获到的错误在完毕后一次性显示出来 109 | 110 | ## [1.2.1] - 2017-10-28 111 | ### Fixed 112 | - 修复1.2.0中修复的"CS2017下执行不存在的动作时提示'命令'播放'不可用'的问题"时造成动作全部不执行的问题 113 | 114 | ## [1.2.0] - 2017-06-18 115 | 开始使用语义化版本号 116 | ### Fixed 117 | - 修复PS CS2017下执行不存在的动作时提示"命令'播放'不可用"的问题 118 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LabelPlus PS-Script 2 | 3 | ![img](pic.jpg) 4 | 5 | ## 概述 6 | 7 | LabelPlus是一个用于图片翻译的工具包,本工程是其中的Photoshop文本导入工具,它读入翻译文本,并将文本逐条添加到PSD档中。 8 | 9 | 脚本用到的开源项目: 10 | * [xtools(BSD license)](http://ps-scripts.sourceforge.net/xtools.html)中部分工具函数及UI框架 11 | * [JSON Action Manager](http://www.tonton-pixel.com/json-photoshop-scripting/json-action-manager/index.html)中的JSON解析库 12 | 13 | 功能一览: 14 | 15 | * 解析LabelPlus文本 创建对应文本图层 16 | * 允许选择性导入部分文件、分组 17 | * 允许更换图源:可使用不同尺寸、可根据顺序自动匹配文件名(图片顺序、数量必须相同)、可替换图源后缀名 18 | * 自定义自动替换文本规则(如自动将`!?`替换为`!?`) 19 | * 格式设置:字体、字号、行距、文本方向 20 | * 可设置自定义动作:每导入一段文字后,执行动作;打开、关闭文档时执行动作 21 | * 根据标号位置自动涂白(实验功能) 22 | 23 | ## 开发方法 24 | 25 | ### requirement 26 | * typescript 27 | * python 28 | 29 | ``` 30 | $ sudo apt install python nodejs 31 | $ sudo npm install -g typescript yarn 32 | $ npm config set registry https://registry.npmmirror.com 33 | $ yarn config set registry https://registry.npmmirror.com 34 | ``` 35 | 36 | ### build 37 | 38 | ``` 39 | $ cd PS-Script 40 | $ yarn install 41 | $ ./build.sh 42 | ``` 43 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cd "${0%%/*}" 4 | 5 | TSC=./node_modules/typescript/bin/tsc 6 | TSC_OUTPUT=build/app.js 7 | PS_JSX_OUTPUT=build/LabelPlus_Ps_Script.jsx 8 | 9 | rm -rf ./build/* 10 | mkdir -p build/ 11 | $TSC --preserveConstEnums -p . --out $TSC_OUTPUT 12 | cat src/copyleft_header.js $TSC_OUTPUT > $PS_JSX_OUTPUT 13 | python ./flatten_jsx.py $PS_JSX_OUTPUT $PS_JSX_OUTPUT -I build/ src/ 14 | 15 | cp -r doc_template/ build/ps_script_res 16 | 17 | echo "Output File: $PS_JSX_OUTPUT" 18 | -------------------------------------------------------------------------------- /do_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | program_exists() { 4 | for program in $*; do 5 | command -v $program >/dev/null 2>&1 6 | [ $? -ne 0 ] && { 7 | echo "missing command $program" 8 | return 1 9 | } 10 | done 11 | 12 | return 0 13 | } 14 | 15 | show_usage() { 16 | echo "Usage: $0 " 17 | echo " version:\tlike 1.6.0" 18 | exit 1 19 | } 20 | 21 | if [ $# -lt 1 ]; then 22 | echo "error: Please input release version!" 23 | show_usage 24 | fi 25 | 26 | version=$1 27 | program_exists git 7z python 28 | [ $? -ne 0 ] && exit 1 29 | 30 | cd "${0%%/*}" 31 | 32 | # Determine if git working space clean 33 | if [ -n "$(git status --untracked-files=no --porcelain | grep -v CHANGELOG.md)" ]; then 34 | echo "git working space not clean" 35 | exit 1 36 | fi 37 | 38 | # remind 39 | echo "Check List:" 40 | read -p "* Is CHANGELOG.md ready to release? [y/N] " -n1 try; 41 | [ "${try##y}" != "" ] && exit 0 42 | 43 | printf "\nstart...\n" 44 | 45 | # update version 46 | sed -i "s/\".*\"/\"${version}\"/" ./src/version.ts 47 | 48 | # FIXME make sure dependencies in node_modules are installed 49 | # build 50 | ./build.sh 51 | 52 | # prepare to pack 53 | PACK_DIR=./build/pack 54 | 55 | # pack directories into ${PACK_DIR} 56 | pack() { 57 | for dir in $*; do 58 | cp -vr $dir ${PACK_DIR}/ 59 | done 60 | } 61 | 62 | mkdir -p ${PACK_DIR} 63 | rm -rf ./${PACK_DIR}/* 64 | 65 | # update changelog 66 | cp -v CHANGELOG.md ${PACK_DIR}/ 67 | date=$(date +%Y-%m-%d) 68 | sed -i "s/\[Unreleased\]/\[${version}\] - ${date}/" $PACK_DIR/CHANGELOG.md 69 | 70 | pack build/LabelPlus_Ps_Script.jsx \ 71 | build/ps_script_res \ 72 | LICENSE.txt \ 73 | README.md 74 | 75 | 7z a -t7z build/LabelPlus_PS-Script_${version}.7z ${PACK_DIR}/* -m0=BCJ -m1=LZMA:d=21 -ms -mmt 76 | 77 | # git commit, add tag 78 | cp ${PACK_DIR}/CHANGELOG.md ./ 79 | TEXT="\n## [Unreleased]\n### Added\n### Changed\n### Fixed\n### Removed\n" 80 | sed "1a\\${TEXT}" CHANGELOG.md -i 81 | 82 | git commit -am "release ${version}" 83 | git tag ${version} 84 | 85 | cat <=4.2.0" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "photoshop.d.ts": "^1.0.0" 4 | }, 5 | "devDependencies": { 6 | "typescript": "^4.9.5" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LabelPlus/PS-Script/e5897f4b390e3c397fa49af2c293bbcc93154e55/pic.jpg -------------------------------------------------------------------------------- /psd-to-labelplus-text.jsx: -------------------------------------------------------------------------------- 1 | #target photoshop 2 | 3 | 4 | 5 | /* 6 | 用于将PSD档中的文本图层导出为LabelPlus脚本 7 | 8 | 用法: 9 | 1. 在Photoshop中打开要导出的若干个psd档 10 | 2. 修改本脚本中的IMG_SUFFIX常量,为图片原本的后缀名 11 | 3. 将脚本拖到Photoshop中执行 12 | */ 13 | 14 | // 输出的图片后缀名 15 | const IMG_SUFFIX = ".png"; 16 | 17 | // 全局变量,计数Label序号 18 | var label_count = 0; 19 | // 全局变量,当前处理的文档的尺寸 20 | var g_docWidth = 0; 21 | var g_docHeight = 0; 22 | 23 | function main() { 24 | 25 | if (!documents.length) return; 26 | 27 | var f = File.saveDialog("保存为文本文件", "文本文件:*.txt"); 28 | 29 | if (f) { 30 | f.open("a"); 31 | 32 | // LabelPlus文本格式头部 33 | f.write("1,0\n-\n框内\n框外\n-\n备注备注备注\n"); 34 | 35 | // 遍历所有打开的文档 36 | for (var i = 0; i < app.documents.length; i++) { 37 | var doc = app.documents[i]; 38 | 39 | // 将文档设置激活才能正常访问到图层 40 | app.activeDocument = doc; 41 | 42 | g_docWidth = doc.width; 43 | g_docHeight = doc.height; 44 | 45 | // LabelPlus 图片文件名 46 | var fileName = doc.name.replace(/\.[^\.]+$/, IMG_SUFFIX); 47 | f.write("\n>>>>>>>>[" + fileName + "]<<<<<<<<" + "\n"); 48 | 49 | label_count = 0; 50 | f.write(scanLayerSets(doc)); 51 | } 52 | 53 | f.close(); 54 | 55 | alert("所有图层上的文本已保存到文件:" + f.fullName); 56 | 57 | } 58 | 59 | } 60 | 61 | function scanLayerSets(el) { 62 | 63 | var mystr = ""; 64 | 65 | //导出图层组 66 | for(var a=0; a 3 | 4 | namespace LabelPlus { 5 | 6 | export function assert(condition: any, msg?: string): asserts condition { 7 | if (!condition) { 8 | throw new Error("error: assert " + condition); 9 | } 10 | } 11 | 12 | // Operating System related 13 | export let dirSeparator = $.os.search(/windows/i) === -1 ? '/' : '\\'; 14 | 15 | export const TEMPLATE_LAYER = { 16 | TEXT: "text", 17 | IMAGE: "bg", 18 | DIALOG_OVERLAY: "dialog-overlay", 19 | }; 20 | 21 | export const image_suffix_list = [".psd", ".png", ".jpg", ".jpeg", ".tif", ".tiff"]; 22 | 23 | export function GetScriptPath(): string { 24 | return $.fileName; 25 | } 26 | 27 | export function GetScriptFolder(): string { 28 | return (new Folder(GetScriptPath())).path; 29 | } 30 | 31 | export function FileIsExists(path: string): boolean { 32 | return (new File(path)).exists; 33 | } 34 | 35 | export function FolderIsExists(path: string): boolean { 36 | return (new Folder(path)).exists; 37 | } 38 | 39 | export function Emit(func: Function): void { 40 | if (func !== undefined) 41 | func(); 42 | } 43 | 44 | export function StringEndsWith(str: string, suffix: string) { 45 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 46 | } 47 | 48 | export function getImageFilesListOfPath(path: string): string[] { 49 | let folder = new Folder(path); 50 | if (!folder.exists) { 51 | return new Array(); 52 | } 53 | 54 | let fileList = folder.getFiles(); 55 | let fileNameList = new Array(); 56 | 57 | for (let i = 0; i < fileList.length; i++) { 58 | let file = fileList[i]; 59 | if (file instanceof File) { 60 | let tmp = file.toString().split("/"); 61 | let short_name = tmp[tmp.length - 1]; 62 | for (let i = 0; i < image_suffix_list.length; i++) { 63 | if (StringEndsWith(short_name.toLowerCase(), image_suffix_list[i])) { 64 | fileNameList.push(short_name); 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | 71 | return fileNameList.sort(); 72 | } 73 | 74 | export function getFileSuffix(filename: string) { 75 | return filename.substring(filename.lastIndexOf("."), filename.length) 76 | } 77 | 78 | export function doAction(action: string, actionSet: string): boolean 79 | { 80 | if (Stdlib.hasAction(action, actionSet) === true) { 81 | app.doAction(action, actionSet); 82 | return true; 83 | } 84 | else { 85 | return false; 86 | } 87 | } 88 | 89 | export function min(a: number, b: number): number { 90 | return (a < b) ? a : b; 91 | } 92 | 93 | export function delArrayElement(arr: Array, element: T) { 94 | let idx = arr.indexOf(element); 95 | if (idx >= 0) { 96 | arr.splice(idx, 1); 97 | } 98 | } 99 | 100 | let dataPath = Folder.appData.fsName + dirSeparator + "labelplus_script"; 101 | let dataFolder = new Folder(dataPath); 102 | if (!dataFolder.exists) { 103 | if (!dataFolder.create()) { 104 | dataPath = Folder.temp.fsName; 105 | } 106 | } 107 | export const APP_DATA_FOLDER: string = dataPath; 108 | export const DEFAULT_LOG_PATH: string = APP_DATA_FOLDER + dirSeparator + "lp_ps_script.log"; 109 | export const DEFAULT_INI_PATH: string = APP_DATA_FOLDER + dirSeparator + "lp_ps_script.ini"; 110 | export const DEFAULT_DUMP_PATH: string = APP_DATA_FOLDER + dirSeparator + "lp_ps_script.dump"; 111 | export let alllog: string = ""; 112 | export let errlog: string = ""; 113 | 114 | Stdlib.log.setFile(DEFAULT_LOG_PATH); 115 | export function log(msg: any) { Stdlib.log(msg); alllog += msg + '\n'; } 116 | export function log_err(msg: any) { Stdlib.log(msg); errlog += msg + '\n'; alllog += msg + '\n'; } 117 | export function showdump(o: any) { alert(Stdlib.listProps(o)); } 118 | 119 | export function str_filename_pair(file_orign: string, file_matched: string): string { 120 | return file_orign + "(" + file_matched + ")"; 121 | } 122 | 123 | } // namespace LabelPlus 124 | -------------------------------------------------------------------------------- /src/copyleft_header.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * LabelPlus PS-Script 3 | * Home Page: http://noodlefighter.com/label_plus 4 | * Author: Noodlefighter 5 | * Released under GPL-2.0 License. 6 | */ 7 | -------------------------------------------------------------------------------- /src/custom_options.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace LabelPlus { 4 | 5 | export enum OptionTextDirection { Keep, Horizontal, Vertical }; 6 | export enum OptionDocTemplate { Auto, No, Custom }; // auto choose preset template/no use template/custom template 7 | export enum OptionOutputType { PSD, TIFF, PNG, JPG, _count }; 8 | 9 | export class ImageInfo { 10 | file: string = ""; 11 | matched_file:string = ""; 12 | index: number = 0; 13 | }; 14 | 15 | export class CustomOptions { 16 | 17 | // ------------------------------------ not saved options 18 | source: string = ""; // images source folder 19 | target: string = ""; // images target folder 20 | lpTextFilePath: string = ""; // path of labelplus text file 21 | imageSelected: ImageInfo[] = []; // selected images 22 | groupSelected: string[] = []; // selected label group 23 | 24 | // ------------------------------------ saved options 25 | docTemplate: OptionDocTemplate = OptionDocTemplate.Auto; // image document template option 26 | docTemplateCustomPath: string = ""; // custom image document template path 27 | 28 | outputType: OptionOutputType = OptionOutputType.PSD; // output image file type 29 | ignoreNoLabelImg: boolean = false; // ignore images with no label 30 | noLayerGroup: boolean = false; // do not create group in document for text layers 31 | notClose: boolean = false; // do not close image document 32 | 33 | font: string = ""; // set font if it is not empty 34 | fontSize: number = 0; // set font size if neq 0 35 | textLeading: number = 0; // set auto leading value if neq 0, unit is percent 36 | textReplace: string = ""; // run text replacing function, if the expression is not empty 37 | outputLabelIndex: boolean = false; // if true, output label index as text layer 38 | textDirection: OptionTextDirection = OptionTextDirection.Keep; // text direction option 39 | 40 | actionGroup: string = ""; // action group name 41 | dialogOverlayLabelGroups: string = ""; // the label groups need dialog overlay layer, split by "," 42 | dialogOverlayTolerance: number = 16; // dialog overlay tolerance 43 | }; 44 | 45 | } // namespace LabelPlus 46 | -------------------------------------------------------------------------------- /src/dialog_clear.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace LabelPlus { 4 | 5 | // note: too slow 6 | // function getColor(doc: Document, x: UnitValue, y: UnitValue): SolidColor 7 | // { 8 | // let sample = doc.colorSamplers.add([x, y]); 9 | // let color = sample.color; 10 | // sample.remove(); 11 | // return color; 12 | // } 13 | 14 | function getColor(doc: Document, x: UnitValue, y: UnitValue): SolidColor 15 | { 16 | let x_px = Math.floor(x.as("px")); 17 | let y_px = Math.floor(y.as("px")); 18 | 19 | var st = doc.activeHistoryState; 20 | 21 | Stdlib.selectBounds(doc, [x_px, y_px, x_px+1, y_px+1]); 22 | 23 | let color = getSelectionColor(doc); 24 | 25 | doc.activeHistoryState = st; 26 | 27 | return color; 28 | } 29 | 30 | function getSelectionColor(doc: Document): SolidColor 31 | { 32 | function findPV(h: number[]) { 33 | let max = 0; 34 | for (var i = 0; i <= 255; i++) { 35 | if (h[i] > h[max]) { 36 | max = i; 37 | } 38 | } 39 | return max; 40 | } 41 | 42 | let pColour = new SolidColor(); 43 | 44 | if (doc.mode == DocumentMode.RGB) { 45 | pColour.model = ColorModel.RGB; 46 | pColour.rgb.red = findPV(doc.channels[0].histogram); 47 | pColour.rgb.green = findPV(doc.channels[1].histogram); 48 | pColour.rgb.blue = findPV(doc.channels[2].histogram); 49 | } 50 | else if (doc.mode == DocumentMode.GRAYSCALE) { 51 | let gr = findPV(doc.channels.getByName("Gray").histogram); 52 | pColour.model = ColorModel.GRAYSCALE; 53 | pColour.gray.gray = 100 * (gr / 255); 54 | } 55 | else { 56 | log("getSelectionColor: Color Mode not supported: " + doc.mode); 57 | } 58 | return pColour; 59 | } 60 | 61 | function isSelectionValid() 62 | { 63 | try { 64 | let bounds = app.activeDocument.selection.bounds; // if no selection, raise error 65 | 66 | let bound_width = bounds[2].as('pt') - bounds[0].as('pt'); 67 | let bound_height = bounds[3].as('pt') - bounds[1].as('pt'); 68 | 69 | log("select bounds: " + bounds.toString() + "bound_width=" + bound_width + " bound_height=" + bound_height); 70 | if ((bound_width < 20) || (bound_height < 20)) { 71 | log("selection too small..."); 72 | return false; 73 | } 74 | 75 | return true; 76 | } 77 | catch (e) { 78 | log("no selection..."); 79 | return false; 80 | } 81 | } 82 | 83 | // clear dialog 84 | // bgLayer: the background layer 85 | // overLayer: the overlay layer 86 | // labels: label (x,y) coordinate 87 | // tolerance: magicwand's tolerance 88 | // contract: contract selected area, for protect the edge of dialog box 89 | export function dialogClear(doc: Document, bgLayer: ArtLayer, overLayer: ArtLayer, 90 | labels: Array<{ x: number, y: number }>, 91 | tolerance: number, contract: UnitValue): boolean 92 | { 93 | let width = doc.width.as("px"); 94 | let height = doc.height.as("px"); 95 | 96 | let tmp_color = new SolidColor; 97 | tmp_color.rgb.red = 255; 98 | tmp_color.rgb.green = 0; 99 | tmp_color.rgb.blue = 255; 100 | 101 | for (let i = 0; i < labels.length; i++) { 102 | let x = labels[i].x * width; 103 | let y = labels[i].y * height; 104 | 105 | app.activeDocument.activeLayer = bgLayer; 106 | doc.selection.deselect(); 107 | MyAction.magicWand(x, y, tolerance, false, true, 'addTo'); 108 | 109 | let fill_color = getSelectionColor(doc); 110 | log("point " + i + "(" + x + "," + y + ") color=" + fill_color.rgb.hexValue.toString()); 111 | 112 | let tmp_layer = doc.artLayers.add(); 113 | app.activeDocument.activeLayer = tmp_layer; 114 | doc.selection.fill(tmp_color, ColorBlendMode.NORMAL, 100, false); 115 | doc.selection.deselect(); 116 | let corners : { x: number, y: number }[]= [ 117 | { x: 0, y: 0 }, 118 | { x: width - 1, y: 0 }, 119 | { x: 0, y: height -1 }, 120 | { x: width - 1, y: height -1 }, 121 | ]; 122 | for (let j = 0; j < corners.length; j++) { 123 | let x = corners[j].x; 124 | let y = corners[j].y; 125 | let color = getColor(doc, UnitValue(x, 'px'), UnitValue(y, 'px')); 126 | if (color.rgb.hexValue == tmp_color.rgb.hexValue) { 127 | log("detect corner (" + x + "," + y + ") is tmp_color"); 128 | continue; 129 | } 130 | MyAction.magicWand(x, y, 0, false, true, 'addTo'); 131 | } 132 | 133 | tmp_layer.remove(); 134 | 135 | doc.selection.invert(); 136 | if (isSelectionValid()) { 137 | doc.selection.contract(contract); 138 | 139 | app.activeDocument.activeLayer = overLayer; 140 | doc.selection.fill(fill_color, ColorBlendMode.NORMAL, 100, false); 141 | } 142 | else { 143 | log("selection is not valid, ignore..."); 144 | } 145 | } 146 | return true; 147 | } 148 | 149 | } // namespace LabelPlus 150 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | namespace I18n { 2 | export var APP_NAME: string = "LabelPlus Script"; 3 | 4 | export var BUTTON_RUN: string = "导入"; 5 | export var BUTTON_CANCEL: string = "关闭"; 6 | export var BUTTON_LOAD: string = "加载配置"; 7 | export var BUTTON_SAVE: string = "保存配置"; 8 | export var BUTTON_RESET: string = "还原配置"; 9 | 10 | export var PANEL_INPUT: string = "输入"; 11 | export var PANEL_OUTPUT: string = "输出"; 12 | export var PANEL_STYLE: string = "格式"; 13 | export var PANEL_AUTOMATION: string = "自动化"; 14 | 15 | export var PANEL_TEMPLATE_SETTING: string = "文档模板设置"; 16 | export var RB_TEMPLATE_AUTO: string = "自动"; 17 | export var RB_TEMPLATE_NO: string = "不使用模板(直接新建文件)"; 18 | export var RB_TEMPLATE_CUSTOM: string = "自定义模板"; 19 | 20 | export var LABEL_TEXT_FILE: string = "LabelPlus文本:"; 21 | export var LABEL_SOURCE: string = "图源文件夹:"; 22 | export var LABEL_TARGET: string = "输出文件夹:"; 23 | export var LABEL_SETTING: string = "存取配置"; 24 | export var LABEL_SELECT_IMG: string = "导入图片选择"; 25 | export var LABEL_SELECT_GROUP: string = "导入分组选择"; 26 | export var LABEL_SELECT_TIP: string = "提示:列表框中,按住Ctrl键选中/取消单个项目,按住Shift键批量选择项目。"; 27 | 28 | 29 | export var CHECKBOX_OUTPUT_LABEL_INDEX: string = "导出标号"; 30 | export var CHECKBOX_TEXT_REPLACE: string = "文本替换(格式:\"A->B|C->D\")"; 31 | export var CHECKBOX_IGNORE_NO_LABEL_IMG: string = "不输出未标号图片"; 32 | export var CHECKBOX_MATCH_IMG_BY_ORDER: string = "按顺序匹配图片文件"; 33 | export var BUTTON_SOURCE_CHECK_MATCH: string = "检查图源匹配情况"; 34 | export var LABEL_OUTPUT_FILE_TYPE: string = "输出文件类型:"; 35 | export var CHECKBOX_REPLACE_IMG_SUFFIX: string = "替换图片后缀名"; 36 | export var CHECKBOX_RUN_ACTION: string = "执行自动化动作"; 37 | export var CHECKBOX_NOT_CLOSE: string = "导入后不关闭文档"; 38 | export var CHECKBOX_SET_FONT: string = "字体"; 39 | export var CHECKBOX_SET_LEADING: string = "行距"; 40 | export var LABEL_TEXT_DIRECTION: string = "文字方向:"; 41 | export var LIST_TEXT_DIT_ITEMS: string[] = [ "默认", "横向", "纵向" ]; 42 | export var CHECKBOX_NO_LAYER_GROUP: string = "不对图层进行分组"; 43 | 44 | export var CHECKBOX_DIALOG_OVERLAY: string = "启用对话框自动涂白"; 45 | export var LABEL_DIALOG_OVERLAY_GROUP: string = "指定需要涂白的分组(例如: 框内,心理):"; 46 | export var LABEL_DIALOG_OVERLAY_TOLERANCE: string = "容差:"; 47 | 48 | export var COMPLETE: string = "导出完毕!"; 49 | export var COMPLETE_WITH_ERROR: string = "导出完毕,但遇到些错误..."; 50 | export var COMPLETE_FAILED: string = "导出失败!"; 51 | 52 | export var ERROR_UNEXPECTED: string = "未预料到的错误,请与作者联系!"; 53 | export var ERROR_FILE_OPEN_FAIL: string = "文件打开失败,请检查PS是否能正确打开该文件!"; 54 | export var ERROR_FILE_SAVE_FAIL: string = "文件保存失败,请检查是否有磁盘操作权限、磁盘空间是否充足。"; 55 | export var ERROR_NOT_FOUND_SOURCE: string = "未找到图源文件夹"; 56 | export var ERROR_NOT_FOUND_TARGET: string = "未找到目标文件夹"; 57 | export var ERROR_NOT_FOUND_LPTEXT: string = "未找到LabelPlus文本文件"; 58 | export var ERROR_NOT_FOUND_TEMPLATE: string = "未找到Photoshop模板文件"; 59 | export var ERROR_CREATE_NEW_FOLDER: string = "无法创建新文件夹"; 60 | export var ERROR_PARSER_LPTEXT_FAIL: string = "解析LabelPlus文本失败"; 61 | export var ERROR_NO_IMG_CHOOSED: string = "未选择输出图片"; 62 | export var ERROR_NO_LABEL_GROUP_CHOOSED: string = "未选择导入分组"; 63 | export var ERROR_NO_MATCH_IMG: string = "找不到对应的图片文件!!"; 64 | export var ERROR_HAVE_NO_MATCH_IMG: string = "存在无法匹配的图源文件,请重新检查!"; 65 | export var ERROR_PRESET_TEMPLATE_NOT_FOUND: string = "无法自动匹配模板文件,请确认脚本所在目录是否存在ps_script_res目录"; 66 | export var ERROR_TEXT_REPLACE_EXPRESSION: string = "文本替换表达式解析错误,请检查!"; 67 | export var ERROR_OPT_FONT_NOT_FOUND: string = "找不到配置中保存的字体"; 68 | 69 | declare var app: any; 70 | // if (true) { 71 | if (!(app.locale in {"zh_CN":1, "zh_TW":1, "zh_HK":1})) { 72 | BUTTON_RUN = "Run"; 73 | BUTTON_CANCEL = "Cancel"; 74 | BUTTON_LOAD = "Load"; 75 | BUTTON_SAVE = "Save"; 76 | BUTTON_RESET = "Reset"; 77 | PANEL_INPUT = "Input"; 78 | PANEL_OUTPUT = "Output"; 79 | PANEL_STYLE = "Style"; 80 | PANEL_AUTOMATION = "Automation"; 81 | PANEL_TEMPLATE_SETTING = "Document Template Setting"; 82 | RB_TEMPLATE_AUTO = "Auto"; 83 | RB_TEMPLATE_NO = "No Template"; 84 | RB_TEMPLATE_CUSTOM = "Custom Template"; 85 | LABEL_TEXT_FILE = "LabelPlus Text:"; 86 | LABEL_SOURCE = "Image Source:"; 87 | LABEL_TARGET = "Output Folder:"; 88 | LABEL_SETTING = "Setting"; 89 | LABEL_SELECT_IMG = "Select Image"; 90 | LABEL_SELECT_GROUP = "Select Group"; 91 | LABEL_SELECT_TIP = "Tip: Push [Ctrl] key to select/cancel one item, push [Shift] key to select multiple items."; 92 | CHECKBOX_OUTPUT_LABEL_INDEX = "Output Label Number"; 93 | CHECKBOX_TEXT_REPLACE = "Text Replace(e.g. \"A->B|C->D\")"; 94 | CHECKBOX_IGNORE_NO_LABEL_IMG = "Ignore Images With No Label"; 95 | CHECKBOX_MATCH_IMG_BY_ORDER = "Match Image Source By Order"; 96 | BUTTON_SOURCE_CHECK_MATCH = "Check Match Result"; 97 | LABEL_OUTPUT_FILE_TYPE = "Output File Type:"; 98 | CHECKBOX_REPLACE_IMG_SUFFIX = "Replace Image Suffix"; 99 | CHECKBOX_RUN_ACTION = "Execute Action:"; 100 | CHECKBOX_NOT_CLOSE = "Do Not Close File"; 101 | CHECKBOX_SET_FONT = "Font"; 102 | CHECKBOX_SET_LEADING = "Leading"; 103 | LABEL_TEXT_DIRECTION = "Text Direction:"; 104 | LIST_TEXT_DIT_ITEMS = [ "Default", "Horizontal", "Vertical" ]; 105 | CHECKBOX_NO_LAYER_GROUP = "Layer Not Grouping"; 106 | CHECKBOX_DIALOG_OVERLAY = "Execute \"Dialog Overlay\""; 107 | LABEL_DIALOG_OVERLAY_GROUP = "Specified Groups(like: group1,group2):"; 108 | LABEL_DIALOG_OVERLAY_TOLERANCE = "Tolerance:"; 109 | COMPLETE = "Export completed!"; 110 | COMPLETE_WITH_ERROR = "Export Completed, but some error occured..." 111 | COMPLETE_FAILED = "Exported failed..." 112 | ERROR_UNEXPECTED = "Unexpected error, please contact with maintenance..."; 113 | ERROR_FILE_OPEN_FAIL = "open file failed, please confirm whether Photoshop can open the file."; 114 | ERROR_FILE_SAVE_FAIL = "File saving failed, please check whether you have disk operation permission and whether the disk space is sufficient."; 115 | ERROR_NOT_FOUND_SOURCE = "Image Source Folder Not Found!"; 116 | ERROR_NOT_FOUND_TARGET = "Output PSD Folder Not Found!"; 117 | ERROR_NOT_FOUND_LPTEXT = "LabelPlus Text File Not Found!"; 118 | ERROR_NOT_FOUND_TEMPLATE = "Photoshop template file not found!"; 119 | ERROR_CREATE_NEW_FOLDER = "Could not build new folder"; 120 | ERROR_PARSER_LPTEXT_FAIL = "Fail To Load LabelPlus Text File"; 121 | ERROR_NO_IMG_CHOOSED = "Please select more than one image"; 122 | ERROR_NO_LABEL_GROUP_CHOOSED = "Please select more than one group"; 123 | ERROR_NO_MATCH_IMG = "No matched image file!!!!"; 124 | ERROR_HAVE_NO_MATCH_IMG = "Some image files did not match, please check again." 125 | ERROR_PRESET_TEMPLATE_NOT_FOUND = "Cannot match template file, please make sure \"ps_script_res\" folder exsit."; 126 | ERROR_TEXT_REPLACE_EXPRESSION = "Expression of text replacing is wrong, please check again."; 127 | ERROR_OPT_FONT_NOT_FOUND = "Cannot found the font"; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/importer.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | 7 | namespace LabelPlus { 8 | 9 | // global var 10 | let opts: CustomOptions | null = null; 11 | let textReplace: TextReplaceInfo = []; 12 | 13 | interface Group { 14 | layerSet?: LayerSet; 15 | template?: ArtLayer; 16 | }; 17 | type GroupDict = { [key: string]: Group }; 18 | 19 | interface LabelInfo { 20 | index: number; 21 | x: number; 22 | y: number; 23 | group: string; 24 | contents: string; 25 | }; 26 | 27 | interface ImageWorkspace { 28 | doc: Document; 29 | 30 | bgLayer: ArtLayer; 31 | textTemplateLayer: ArtLayer; 32 | dialogOverlayLayer: ArtLayer; 33 | 34 | pendingDelLayerList: ArtLayer[]; 35 | groups: GroupDict; 36 | }; 37 | 38 | interface ImageInfo { 39 | ws: ImageWorkspace; 40 | name: string; 41 | name_pair: string; 42 | labels: LpLabel[]; 43 | }; 44 | 45 | function importLabel(img: ImageInfo, label: LabelInfo): boolean 46 | { 47 | assert(opts !== null); 48 | 49 | // import the index of the Label 50 | if (opts.outputLabelIndex) { 51 | let o: TextInputOptions = { 52 | template: img.ws.textTemplateLayer, 53 | direction: Direction.HORIZONTAL, 54 | font: "Arial", 55 | size: (opts.fontSize !== 0) ? UnitValue(opts.fontSize, "pt") : undefined, 56 | lgroup: img.ws.groups["_Label"].layerSet, 57 | }; 58 | newTextLayer(img.ws.doc, String(label.index), label.x, label.y, o); 59 | } 60 | 61 | // 替换文本 62 | if (opts.textReplace) { 63 | for (let k = 0; k < textReplace.length; k++) { 64 | while (label.contents.indexOf(textReplace[k].from) != -1) 65 | label.contents = label.contents.replace(textReplace[k].from, textReplace[k].to); 66 | } 67 | } 68 | 69 | // 确定文字方向 70 | let textDir: Direction | undefined; 71 | switch (opts.textDirection) { 72 | case OptionTextDirection.Keep: textDir = undefined; break; 73 | case OptionTextDirection.Horizontal: textDir = Direction.HORIZONTAL; break; 74 | case OptionTextDirection.Vertical: textDir = Direction.VERTICAL; break; 75 | } 76 | 77 | // 导出文本,设置的优先级大于模板,无模板时做部分额外处理 78 | let textLayer: ArtLayer; 79 | let o: TextInputOptions = { 80 | template: img.ws.groups[label.group].template, 81 | font: (opts.font != "") ? opts.font : undefined, 82 | direction: textDir, 83 | lgroup: img.ws.groups[label.group].layerSet, 84 | lending: opts.textLeading ? opts.textLeading : undefined, 85 | }; 86 | 87 | // 使用模板时,用户不设置字体大小,不做更改;不使用模板时,如果用户不设置大小,自动调整到合适的大小 88 | if (opts.docTemplate === OptionDocTemplate.No) { 89 | let proper_size = UnitValue(min(img.ws.doc.height.as("pt"), img.ws.doc.height.as("pt")) / 90.0, "pt"); 90 | o.size = (opts.fontSize !== 0) ? UnitValue(opts.fontSize, "pt") : proper_size; 91 | } else { 92 | o.size = (opts.fontSize !== 0) ? UnitValue(opts.fontSize, "pt") : undefined; 93 | } 94 | textLayer = newTextLayer(img.ws.doc, label.contents, label.x, label.y, o); 95 | 96 | // 执行动作,名称为分组名 97 | if (opts.actionGroup) { 98 | img.ws.doc.activeLayer = textLayer; 99 | let result = doAction(label.group, opts.actionGroup); 100 | log("run action " + label.group + "[" + opts.actionGroup + "]..." + result ? "done" : "fail"); 101 | } 102 | return true; 103 | } 104 | 105 | function importImage(img: ImageInfo): boolean 106 | { 107 | assert(opts !== null); 108 | 109 | // run action _start 110 | if (opts.actionGroup) { 111 | img.ws.doc.activeLayer = img.ws.doc.layers[img.ws.doc.layers.length - 1]; 112 | let result = doAction("_start", opts.actionGroup); 113 | log("run action _start[" + opts.actionGroup + "]..." + result ? "done" : "fail"); 114 | } 115 | 116 | // 找出需要涂白的标签,记录他们的坐标,执行涂白 117 | if (opts.dialogOverlayLabelGroups) { 118 | let points = new Array(); 119 | let groups = opts.dialogOverlayLabelGroups.split(","); 120 | for (let j = 0; j < img.labels.length; j++) { 121 | let l = img.labels[j]; 122 | if (groups.indexOf(l.group) >= 0) { 123 | points.push({ x: l.x, y: l.y }); 124 | } 125 | } 126 | 127 | let contract = UnitValue(2, 'pt'); 128 | let tolerance = opts.dialogOverlayTolerance; 129 | log("dialogClear() ,contract_px=" + contract + ",tolerance=" + tolerance); 130 | dialogClear(img.ws.doc, img.ws.bgLayer, img.ws.dialogOverlayLayer, points, tolerance, contract); 131 | delArrayElement(img.ws.pendingDelLayerList, img.ws.dialogOverlayLayer); // do not delete dialog-overlay-layer 132 | } 133 | 134 | // 遍历LabelData 135 | for (let j = 0; j < img.labels.length; j++) { 136 | let l = img.labels[j]; 137 | if (opts.groupSelected.indexOf(l.group) == -1) // the group did not select by user, return directly 138 | continue; 139 | 140 | let label_info: LabelInfo = { 141 | index: j + 1, 142 | x: l.x, 143 | y: l.y, 144 | group: l.group, 145 | contents: l.contents, 146 | }; 147 | log("import label " + label_info.index + "..."); 148 | importLabel(img, label_info); 149 | } 150 | 151 | // adjust layer order 152 | if (img.ws.bgLayer && (opts.dialogOverlayLabelGroups !== "")) { 153 | log('move "dialog-overlay" before "bg"'); 154 | img.ws.dialogOverlayLayer.move(img.ws.bgLayer, ElementPlacement.PLACEBEFORE); 155 | } 156 | 157 | // remove unnecessary Layer/LayerSet 158 | log('remove unnecessary Layer/LayerSet...'); 159 | for (var layer of img.ws.pendingDelLayerList) { // Layer 160 | layer.remove(); 161 | } 162 | for (let k in img.ws.groups) { // LayerSet 163 | if (img.ws.groups[k].layerSet !== undefined) { 164 | if (img.ws.groups[k].layerSet?.artLayers.length === 0) { 165 | img.ws.groups[k].layerSet?.remove(); 166 | } 167 | } 168 | } 169 | 170 | // run action _end 171 | if (opts.actionGroup) { 172 | img.ws.doc.activeLayer = img.ws.doc.layers[img.ws.doc.layers.length - 1]; 173 | let result = doAction("_end", opts.actionGroup); 174 | log("run action _end[" + opts.actionGroup + "]..." + result ? "done" : "fail"); 175 | } 176 | return true; 177 | } 178 | 179 | function openImageWorkspace(img_filename: string, template_path: string): ImageWorkspace | null 180 | { 181 | assert(opts !== null); 182 | 183 | // open background image 184 | let bgDoc: Document; 185 | try { 186 | let bgFile = new File(opts.source + dirSeparator + img_filename); 187 | bgDoc = app.open(bgFile); 188 | } catch { 189 | return null; //note: do not exit if image not exist 190 | } 191 | 192 | // if template is enabled, open template; or create a new file 193 | let wsDoc: Document; // workspace document 194 | if (opts.docTemplate == OptionDocTemplate.No) { 195 | wsDoc = app.documents.add(bgDoc.width, bgDoc.height, bgDoc.resolution, bgDoc.name, NewDocumentMode.RGB, DocumentFill.TRANSPARENT); 196 | wsDoc.activeLayer.name = TEMPLATE_LAYER.IMAGE; 197 | } else { 198 | let docFile = new File(template_path); //note: if template must do not exist, crash 199 | wsDoc = app.open(docFile); 200 | wsDoc.resizeImage(undefined, undefined, bgDoc.resolution); 201 | wsDoc.resizeCanvas(bgDoc.width, bgDoc.height); 202 | } 203 | 204 | // wsDoc is clean, check template elements, if a element not exist 205 | let bgLayer: ArtLayer; 206 | let textTemplateLayer: ArtLayer; 207 | let dialogOverlayLayer: ArtLayer; 208 | let pendingDelLayerList: ArtLayer[] = new Array(); 209 | { 210 | // add all artlayers to the pending delete list 211 | for (let i = 0; i < wsDoc.artLayers.length; i++) { 212 | let layer: ArtLayer = wsDoc.artLayers[i]; 213 | pendingDelLayerList.push(layer); 214 | } 215 | 216 | // bg layer template 217 | try { bgLayer = wsDoc.artLayers.getByName(TEMPLATE_LAYER.IMAGE); } 218 | catch { 219 | bgLayer = wsDoc.artLayers.add(); 220 | bgLayer.name = TEMPLATE_LAYER.DIALOG_OVERLAY; 221 | } 222 | // text layer template 223 | try { textTemplateLayer = wsDoc.artLayers.getByName(TEMPLATE_LAYER.TEXT); } 224 | catch { 225 | textTemplateLayer = wsDoc.artLayers.add(); 226 | textTemplateLayer.name = TEMPLATE_LAYER.TEXT; 227 | pendingDelLayerList.push(textTemplateLayer); // pending delete 228 | } 229 | // dialog overlay layer template 230 | try { dialogOverlayLayer = wsDoc.artLayers.getByName(TEMPLATE_LAYER.DIALOG_OVERLAY); } 231 | catch { 232 | dialogOverlayLayer = wsDoc.artLayers.add(); 233 | dialogOverlayLayer.name = TEMPLATE_LAYER.DIALOG_OVERLAY; 234 | } 235 | } 236 | 237 | // import bgDoc to wsDoc: 238 | // if bgDoc has only a layer, select all and copy to bg layer, for applying bg layer template 239 | // if bgDoc has multiple layers, move all layers after bg layer (bg layer template is invalid) 240 | if ((bgDoc.artLayers.length == 1) && (bgDoc.layerSets.length == 0)) { 241 | app.activeDocument = bgDoc; 242 | bgDoc.selection.selectAll(); 243 | bgDoc.selection.copy(); 244 | app.activeDocument = wsDoc; 245 | wsDoc.activeLayer = bgLayer; 246 | wsDoc.paste(); 247 | delArrayElement(pendingDelLayerList, bgLayer); // keep bg layer 248 | } else { 249 | app.activeDocument = bgDoc; 250 | let item = bgLayer; 251 | for (let i = 0; i < bgDoc.layers.length; i++) { 252 | item = bgDoc.layers[i].duplicate(item, ElementPlacement.PLACEAFTER); 253 | } 254 | } 255 | bgDoc.close(SaveOptions.DONOTSAVECHANGES); 256 | 257 | // 若文档类型为索引色模式 更改为RGB模式 258 | if (wsDoc.mode == DocumentMode.INDEXEDCOLOR) { 259 | log("wsDoc.mode is INDEXEDCOLOR, set RGB"); 260 | wsDoc.changeMode(ChangeMode.RGB); 261 | } 262 | 263 | // 分组 264 | let groups: GroupDict = {}; 265 | for (let i = 0; i < opts.groupSelected.length; i++) { 266 | let name = opts.groupSelected[i]; 267 | let tmp: Group = {}; 268 | 269 | // 创建PS中图层分组 270 | if (!opts.noLayerGroup) { 271 | tmp.layerSet = wsDoc.layerSets.add(); 272 | tmp.layerSet.name = name; 273 | tmp.layerSet.blendMode = BlendMode.NORMAL; 274 | } 275 | // 尝试寻找分组模板,找不到则使用默认文本模板 276 | if (opts.docTemplate !== OptionDocTemplate.No) { 277 | let l: ArtLayer | undefined; 278 | try { 279 | l = wsDoc.artLayers.getByName(name); 280 | } catch { }; 281 | tmp.template = (l !== undefined) ? l : textTemplateLayer; 282 | } 283 | groups[name] = tmp; // add 284 | } 285 | if (opts.outputLabelIndex) { 286 | let tmp: Group = {}; 287 | tmp.layerSet = wsDoc.layerSets.add(); 288 | tmp.layerSet.name = "Label"; 289 | groups["_Label"] = tmp; 290 | } 291 | 292 | let ws: ImageWorkspace = { 293 | doc: wsDoc, 294 | bgLayer: bgLayer, 295 | textTemplateLayer: textTemplateLayer, 296 | dialogOverlayLayer: dialogOverlayLayer, 297 | pendingDelLayerList: pendingDelLayerList, 298 | groups: groups, 299 | }; 300 | return ws; 301 | } 302 | 303 | function closeImage(img: ImageInfo, saveType: OptionOutputType = OptionOutputType.PSD): boolean 304 | { 305 | assert(opts !== null); 306 | 307 | // 保存文件 308 | let fileOut = new File(opts.target + dirSeparator + img.name); 309 | let asCopy = false; 310 | let options: any; 311 | switch (saveType) { 312 | case OptionOutputType.PSD: 313 | options = PhotoshopSaveOptions; 314 | break; 315 | case OptionOutputType.TIFF: 316 | options = TiffSaveOptions; 317 | break; 318 | case OptionOutputType.PNG: 319 | options = PNGSaveOptions; 320 | asCopy = true; 321 | break; 322 | case OptionOutputType.JPG: 323 | options = new JPEGSaveOptions(); 324 | options.quality = 10; 325 | asCopy = true; 326 | break; 327 | default: 328 | log_err(img.name_pair + ": unkown save type " + saveType); 329 | return false 330 | } 331 | 332 | let extensionType = Extension.LOWERCASE; 333 | img.ws.doc.saveAs(fileOut, options, asCopy, extensionType); 334 | 335 | // 关闭文件 336 | if (!opts.notClose) 337 | img.ws.doc.close(SaveOptions.DONOTSAVECHANGES); 338 | 339 | return true; 340 | } 341 | 342 | export function importFiles(custom_opts: CustomOptions): boolean 343 | { 344 | opts = custom_opts; 345 | 346 | log("Start import process!!!"); 347 | log("Properties start ------------------"); 348 | log(Stdlib.listProps(opts)); 349 | log("Properties end ------------------"); 350 | 351 | //解析LabelPlus文本 352 | let lpFile = lpTextParser(opts.lpTextFilePath); 353 | if (lpFile == null) { 354 | log_err("error: " + I18n.ERROR_PARSER_LPTEXT_FAIL); 355 | return false; 356 | } 357 | log("parse lptext done..."); 358 | 359 | // 替换文本解析 360 | if (opts.textReplace) { 361 | let tmp = textReplaceReader(opts.textReplace); 362 | if (tmp === null) { 363 | log_err("error: " + I18n.ERROR_TEXT_REPLACE_EXPRESSION); 364 | return false; 365 | } 366 | textReplace = tmp; 367 | } 368 | log("parse textreplace done..."); 369 | 370 | // 确定doc模板文件 371 | let template_path: string = ""; 372 | switch (opts.docTemplate) { 373 | case OptionDocTemplate.Custom: 374 | template_path = opts.docTemplateCustomPath; 375 | if (!FileIsExists(template_path)) { 376 | log_err("error: " + I18n.ERROR_NOT_FOUND_TEMPLATE + " " + template_path); 377 | return false; 378 | } 379 | break; 380 | case OptionDocTemplate.Auto: 381 | let tempdir = GetScriptFolder() + dirSeparator + "ps_script_res" + dirSeparator; 382 | let tempname = app.locale.split("_")[0].toLocaleLowerCase() + ".psd"; // such as "zh_CN" -> zh.psd 383 | 384 | let try_list: string[] = [ 385 | tempdir + tempname, 386 | tempdir + "en.psd" 387 | ]; 388 | for (let i = 0; i < try_list.length; i++) { 389 | if (FileIsExists(try_list[i])) { 390 | template_path = try_list[i]; 391 | break; 392 | } 393 | } 394 | if (template_path === "") { 395 | log_err("error: " + I18n.ERROR_PRESET_TEMPLATE_NOT_FOUND); 396 | return false; 397 | } 398 | log("auto match template: " + template_path); 399 | break; 400 | case OptionDocTemplate.No: 401 | default: 402 | log("template not used"); 403 | break; 404 | } 405 | 406 | // 遍历所选图片 407 | for (let i = 0; i < opts.imageSelected.length; i++) { 408 | let orgin_name :string = opts.imageSelected[i].file; // 翻译文件中的图片文件名 409 | let matched_name: string = opts.imageSelected[i].matched_file; 410 | let name_pair = LabelPlus.str_filename_pair(orgin_name, matched_name); 411 | 412 | log(name_pair + 'in processing...' ); 413 | if (opts.ignoreNoLabelImg && lpFile?.images[orgin_name].length == 0) { // ignore img with no label 414 | log('no label, ignored...'); 415 | continue; 416 | } 417 | let ws = openImageWorkspace(matched_name, template_path); 418 | if (ws == null) { 419 | log_err(name_pair + ": " + I18n.ERROR_FILE_OPEN_FAIL); 420 | continue; 421 | } 422 | 423 | let img_info: ImageInfo = { 424 | ws: ws, 425 | name: matched_name, 426 | name_pair: name_pair, 427 | labels: lpFile.images[orgin_name], 428 | }; 429 | if (!importImage(img_info)) { 430 | log_err(name_pair + ": import label failed"); 431 | } 432 | if (!closeImage(img_info, opts.outputType)) { 433 | log_err(name_pair + ": " + I18n.ERROR_FILE_SAVE_FAIL); 434 | } 435 | log(name_pair + ": done"); 436 | } 437 | log("All Done!"); 438 | return true; 439 | }; 440 | 441 | 442 | // 文本导入选项,参数为undefined时表示不设置该项 443 | interface TextInputOptions { 444 | template?: ArtLayer; // 文本图层模板 445 | font?: string; 446 | size?: UnitValue; 447 | direction?: Direction; 448 | lgroup?: LayerSet; 449 | lending?: number; // 自动行距 450 | }; 451 | 452 | // 创建文本图层 453 | function newTextLayer(doc: Document, text: string, x: number, y: number, topts: TextInputOptions = {}): ArtLayer 454 | { 455 | let artLayerRef: ArtLayer; 456 | let textItemRef: TextItem; 457 | 458 | // 从模板创建,可以保证图层的所有格式与模板一致 459 | if (topts.template) { 460 | /// @ts-ignore ts声明文件有误,duplicate()返回ArtLayer对象,而不是void 461 | artLayerRef = topts.template.duplicate(); 462 | textItemRef = artLayerRef.textItem; 463 | } 464 | else { 465 | artLayerRef = doc.artLayers.add(); 466 | artLayerRef.kind = LayerKind.TEXT; 467 | textItemRef = artLayerRef.textItem; 468 | } 469 | 470 | if (topts.size) 471 | textItemRef.size = topts.size; 472 | 473 | if (topts.font) 474 | textItemRef.font = topts.font; 475 | 476 | if (topts.direction) 477 | textItemRef.direction = topts.direction; 478 | 479 | textItemRef.position = Array(UnitValue(doc.width.as("px") * x, "px"), UnitValue(doc.height.as("px") * y, "px")); 480 | 481 | if (topts.lgroup) 482 | artLayerRef.move(topts.lgroup, ElementPlacement.PLACEATBEGINNING); 483 | 484 | if ((topts.lending) && (topts.lending != 0)) { 485 | textItemRef.useAutoLeading = true; 486 | textItemRef.autoLeadingAmount = topts.lending; 487 | } 488 | 489 | artLayerRef.name = text; 490 | textItemRef.contents = text; 491 | 492 | return artLayerRef; 493 | } 494 | 495 | type TextReplaceInfo = { from: string; to: string; }[]; 496 | 497 | // 文本替换表达式解析 498 | function textReplaceReader(str: string): TextReplaceInfo | null 499 | { 500 | let arr: TextReplaceInfo = []; 501 | 502 | let strs = str.split('|'); 503 | if (!strs) 504 | return null; //解析失败 505 | 506 | for (let i = 0; i < strs.length; i++) { 507 | if (strs[i] === "") 508 | continue; 509 | 510 | let strss = strs[i].split("->"); 511 | if ((strss.length != 2) || (strss[0] == "")) 512 | return null; //解析失败 513 | 514 | arr.push({ from: strss[0], to: strss[1] }); 515 | } 516 | return arr; 517 | } 518 | 519 | } // namespace LabelPlus 520 | -------------------------------------------------------------------------------- /src/jam/jamBooks.jsxinc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: jamBooks.jsxinc 3 | // Version: 4.5.1 4 | // Release Date: 2016-10-13 5 | // Copyright: © 2016 Michel MARIANI 6 | // Licence: GPL 7 | //------------------------------------------------------------------------------ 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see . 20 | //------------------------------------------------------------------------------ 21 | // Version History: 22 | // 4.5.1: 23 | // - Added book description and simplified name of color components fields in 24 | // function jamBooks.getColorBookFileColors (). 25 | // 4.5: 26 | // - Initial release. 27 | //------------------------------------------------------------------------------ 28 | 29 | /** 30 | * @fileOverview 31 | * @name jamBooks.jsxinc 32 | * @author Michel MARIANI 33 | */ 34 | 35 | //------------------------------------------------------------------------------ 36 | 37 | if (typeof jamBooks !== 'object') 38 | { 39 | /** 40 | * Global object (used to simulate a namespace in JavaScript) containing 41 | * a set of color books-related functions for scripts written with the 42 | * JSON Action Manager engine.
43 | * Uses information found in the documents 44 | * Adobe Color Book File Format Specification and 45 | * Adobe Photoshop File Formats Specification. 46 | * @author Michel MARIANI 47 | * @version 4.5.1 48 | * @namespace 49 | */ 50 | var jamBooks = { }; 51 | // 52 | (function () 53 | { 54 | /** 55 | * @description Test if a given file is a color book file (*.acb). 56 | * @param {Object} file File object 57 | * @returns {Boolean} true if color book file 58 | * @example 59 | * function colorBookFileFilter (f) 60 | * { 61 | * return (f instanceof Folder) || jamBooks.isColorBookFile (f); 62 | * } 63 | * var select = (File.fs === "Macintosh") ? colorBookFileFilter : "Color Book Files:*.acb,All Files:*"; 64 | * var colorBookFile = File.openDialog ("Select a color book file:", select); 65 | * if (colorBookFile !== null) 66 | * { 67 | * alert ("OK!"); 68 | * } 69 | */ 70 | jamBooks.isColorBookFile = function (file) 71 | { 72 | return (file.type === '8BCB') || file.name.match (/\.acb$/i); 73 | }; 74 | // 75 | /** 76 | * @description Parse a color book file (*.acb) into a data structure in JSON object format. 77 | * @param {String|Object} colorBookFile Color book file path string or File object 78 | * @param {Boolean} [actualComponents] List actual color components instead of raw color components 79 | * @returns {Object|String} Parsed color book file data structure in JSON object format, or error message string 80 | * @see jamBooks.dataToColorBookFile 81 | * @example 82 | * var acbFilter = 83 | * (File.fs === "Macintosh") ? 84 | * function (f) { return (f instanceof Folder) || jamBooks.isColorBookFile (f) } : 85 | * "Color Book Files:*.acb,All Files:*.*"; 86 | * var colorBookFile = File.openDialog ("Open color book file:", acbFilter); 87 | * if (colorBookFile) 88 | * { 89 | * var colorBookData = jamBooks.dataFromColorBookFile (colorBookFile); 90 | * if (typeof colorBookData === 'string') 91 | * { 92 | * alert ("Error: " + colorBookData); 93 | * } 94 | * else 95 | * { 96 | * alert ("Color space: " + colorBookData["colorSpace"]); 97 | * alert ("Number of colors: " + colorBookData["colors"].length); 98 | * } 99 | * } 100 | */ 101 | jamBooks.dataFromColorBookFile = function (colorBookFile, actualComponents) 102 | { 103 | var file; 104 | if (typeof colorBookFile === 'string') 105 | { 106 | file = new File (colorBookFile); 107 | } 108 | else if (colorBookFile instanceof File) 109 | { 110 | file = colorBookFile; 111 | } 112 | // 113 | var colorBookData; 114 | if (file.open ("r")) 115 | { 116 | try 117 | { 118 | file.encoding = 'BINARY'; 119 | var magicNumber = file.read (4); 120 | if (magicNumber === '8BCB') 121 | { 122 | function readBEInt (file, byteCount) 123 | { 124 | var bytes = file.read (byteCount); 125 | var intValue = 0; 126 | for (var index = 0; index < byteCount; index++) 127 | { 128 | intValue = (intValue << 8) + bytes.charCodeAt (index); 129 | } 130 | return intValue; 131 | } 132 | function readUnicodeString (file) 133 | { 134 | var unicodeString = ""; 135 | var unicodeLength = readBEInt (file, 4); // Includes terminating null 136 | for (var index = 0; index < unicodeLength; index++) 137 | { 138 | var unicodeChar = readBEInt (file, 2); 139 | if (unicodeChar !== 0) 140 | { 141 | unicodeString += String.fromCharCode (unicodeChar); 142 | } 143 | } 144 | return unicodeString; 145 | } 146 | var formatVersion = readBEInt (file, 2); 147 | if (formatVersion === 1) 148 | { 149 | var colorBook = { }; 150 | colorBook["bookID"] = readBEInt (file, 2); 151 | colorBook["bookName"] = readUnicodeString (file); 152 | colorBook["colorNamePrefix"] = readUnicodeString (file); 153 | colorBook["colorNameSuffix"] = readUnicodeString (file); 154 | colorBook["bookDescription"] = readUnicodeString (file); 155 | var colorCount = readBEInt (file, 2); 156 | colorBook["colorsPerPage"] = readBEInt (file, 2); 157 | colorBook["keyColorIndex"] = readBEInt (file, 2); 158 | var colorSpace = readBEInt (file, 2); 159 | var colorSpaces = [ "RGB", null, "CMYK", null, null, null, null, "Lab" ]; 160 | if ((colorSpace < 0) || (colorSpace > colorSpaces.length)) 161 | { 162 | throw new Error ("[jamBooks.dataFromColorBookFile] Invalid color space: " + colorSpace); 163 | } 164 | else 165 | { 166 | colorBook["colorSpace"] = colorSpaces[colorSpace]; 167 | } 168 | var colors = [ ]; 169 | for (var colorIndex = 0; colorIndex < colorCount; colorIndex++) 170 | { 171 | var color = { }; 172 | color["colorName"] = readUnicodeString (file); 173 | color["colorKey"] = file.read (6); 174 | var components = file.read ((colorSpace === 2) ? 4 : 3); 175 | var componentArr = [ ]; 176 | for (componentIndex = 0; componentIndex < components.length; componentIndex++) 177 | { 178 | var componentValue = components.charCodeAt (componentIndex); 179 | if (actualComponents) 180 | { 181 | switch (colorSpace) 182 | { 183 | case 2: // CMYK 184 | componentValue = (255 - componentValue) / 255 * 100; // 0% to 100% 185 | break; 186 | case 7: // Lab 187 | if (componentIndex > 0) // a or b 188 | { 189 | componentValue -= 128; // -128 to 127 190 | } 191 | else // L (luminance) 192 | { 193 | componentValue = componentValue / 255 * 100; // 0% to 100% 194 | } 195 | break; 196 | } 197 | } 198 | componentArr.push (componentValue); 199 | } 200 | color[(actualComponents) ? "actualComponents" : "rawComponents"] = componentArr; 201 | colors.push (color); 202 | } 203 | colorBook["colors"] = colors; 204 | if (!file.eof) 205 | { 206 | colorBook["spotProcess"] = file.read (8); // Either 'spflspot' or 'spflproc' 207 | } 208 | colorBookData = colorBook; 209 | } 210 | else 211 | { 212 | throw new Error ("[jamBooks.dataFromColorBookFile] Unrecognized format version: " + formatVersion); 213 | } 214 | } 215 | else 216 | { 217 | throw new Error ("[jamBooks.dataFromColorBookFile] Unrecognized magic number: " + magicNumber); 218 | } 219 | } 220 | catch (e) 221 | { 222 | colorBookData = e.message; 223 | } 224 | finally 225 | { 226 | file.close (); 227 | } 228 | } 229 | else 230 | { 231 | colorBookData = "[jamBooks.dataFromColorBookFile] Cannot open file"; 232 | } 233 | return colorBookData; 234 | }; 235 | // 236 | /** 237 | * @description Generate a color book file (*.acb) from a data structure in JSON object format. 238 | * @param {String|Object} colorBookFile Color book file path string or File object 239 | * @param {Object} colorBookData Color book file data structure in JSON object format 240 | * @returns {String} Error message string (empty if no error) 241 | * @see jamBooks.dataFromColorBookFile 242 | * @example 243 | * var jsonFilter = 244 | * (File.fs === "Macintosh") ? 245 | * function (f) { return (f instanceof Folder) || f.name.match (/\.json$/i) } : 246 | * "JSON Text Files:*.json,All Files:*.*"; 247 | * var jsonFile = File.openDialog ("Open JSON text file:", jsonFilter); 248 | * if (jsonFile) 249 | * { 250 | * var colorBookData = jamUtils.readJsonFile (jsonFile); 251 | * if (colorBookData) 252 | * { 253 | * var result = jamBooks.dataToColorBookFile ("~/Desktop/color-book.acb", colorBookData); 254 | * if (result) 255 | * { 256 | * alert ("Error: " + result); 257 | * } 258 | * else 259 | * { 260 | * alert ("Color book file generated on Desktop."); 261 | * } 262 | * } 263 | * else 264 | * { 265 | * alert ("Invalid JSON text file!"); 266 | * } 267 | * } 268 | */ 269 | jamBooks.dataToColorBookFile = function (colorBookFile, colorBookData) 270 | { 271 | var file; 272 | if (typeof colorBookFile === 'string') 273 | { 274 | file = new File (colorBookFile); 275 | } 276 | else if (colorBookFile instanceof File) 277 | { 278 | file = colorBookFile; 279 | } 280 | // 281 | var result = ""; 282 | if (file.open ('w', '8BCB', '8BIM')) 283 | { 284 | try 285 | { 286 | function writeBEInt (file, byteCount, intValue) 287 | { 288 | var bytes = ""; 289 | for (var index = 0; index < byteCount; index++) 290 | { 291 | bytes = String.fromCharCode (intValue & 0xFF) + bytes; 292 | intValue >>= 8; 293 | } 294 | file.write (bytes); 295 | } 296 | function writeUnicodeString (file, unicodeString) 297 | { 298 | var unicodeLength = unicodeString.length; 299 | writeBEInt (file, 4, unicodeLength); // Doesn't include terminating null! 300 | for (var index = 0; index < unicodeLength; index++) 301 | { 302 | writeBEInt (file, 2, unicodeString.charCodeAt (index)); 303 | } 304 | } 305 | file.encoding = "BINARY"; 306 | file.write ('8BCB'); 307 | writeBEInt (file, 2, 1); 308 | writeBEInt (file, 2, colorBookData["bookID"]); 309 | writeUnicodeString (file, colorBookData["bookName"]); 310 | writeUnicodeString (file, colorBookData["colorNamePrefix"]); 311 | writeUnicodeString (file, colorBookData["colorNameSuffix"]); 312 | writeUnicodeString (file, colorBookData["bookDescription"]); 313 | var colors = colorBookData["colors"]; 314 | var colorCount = colors.length; 315 | writeBEInt (file, 2, colorCount); 316 | writeBEInt (file, 2, colorBookData["colorsPerPage"]); 317 | if ("keyColorIndex" in colorBookData) 318 | { 319 | writeBEInt (file, 2, colorBookData["keyColorIndex"]); 320 | } 321 | else if ("keyColorPage" in colorBookData) // Legacy... 322 | { 323 | writeBEInt (file, 2, colorBookData["keyColorPage"]); 324 | } 325 | var colorSpace = colorBookData["colorSpace"]; 326 | var colorSpaces = { "RGB": 0, "CMYK": 2, "Lab": 7 }; 327 | if (colorSpace in colorSpaces) 328 | { 329 | writeBEInt (file, 2, colorSpaces[colorSpace]); 330 | } 331 | else 332 | { 333 | throw new Error ("[jamBooks.dataToColorBookFile] Invalid color space: " + jamJSON.stringify (colorSpace)); 334 | } 335 | for (var colorIndex = 0; colorIndex < colorCount; colorIndex++) 336 | { 337 | var color = colors[colorIndex]; 338 | writeUnicodeString (file, color["colorName"]); 339 | var colorKey = color["colorKey"]; 340 | if (colorKey.length === 6) 341 | { 342 | file.write (colorKey); 343 | } 344 | else 345 | { 346 | throw new Error ("[jamBooks.dataToColorBookFile] Invalid color key: " + jamJSON.stringify (colorKey)); 347 | } 348 | if ("actualComponents" in color) 349 | { 350 | var components = color["actualComponents"]; 351 | for (componentIndex = 0; componentIndex < components.length; componentIndex++) 352 | { 353 | var componentValue = components[componentIndex]; 354 | switch (colorSpace) 355 | { 356 | case "CMYK": 357 | componentValue = 255 - Math.round (componentValue * 255 / 100); 358 | break; 359 | case "Lab": 360 | if (componentIndex > 0) // a or b 361 | { 362 | componentValue = Math.round (componentValue) + 128; 363 | } 364 | else // L (luminance) 365 | { 366 | componentValue = Math.round (componentValue * 255 / 100); 367 | } 368 | break; 369 | case "RGB": 370 | componentValue = Math.round (componentValue); 371 | break; 372 | } 373 | file.write (String.fromCharCode (componentValue)); 374 | } 375 | } 376 | else if ("rawComponents" in color) 377 | { 378 | var components = color["rawComponents"]; 379 | for (componentIndex = 0; componentIndex < components.length; componentIndex++) 380 | { 381 | file.write (String.fromCharCode (components[componentIndex])); 382 | } 383 | } 384 | else if ("components" in color) // Legacy... 385 | { 386 | var components = color["components"]; 387 | for (componentIndex = 0; componentIndex < components.length; componentIndex++) 388 | { 389 | file.write (String.fromCharCode (components[componentIndex])); 390 | } 391 | } 392 | } 393 | if ("spotProcess" in colorBookData) 394 | { 395 | file.write (colorBookData["spotProcess"]); // Either 'spflspot' or 'spflproc' 396 | } 397 | } 398 | catch (e) 399 | { 400 | result = e.message; 401 | } 402 | finally 403 | { 404 | file.close (); 405 | } 406 | } 407 | else 408 | { 409 | result = "[jamBooks.dataToColorBookFile] Cannot open file"; 410 | } 411 | return result; 412 | }; 413 | // 414 | /** 415 | * @description Get the list of colors from a color book file (*.acb). 416 | * @param {String|Object} colorBookFile Color book file path string or File object 417 | * @param {Boolean} [roundComponents] Round color components 418 | * @returns {Object|Null} List of colors from a color book file (*.acb), or null if error 419 | * @example 420 | * var acbFilter = 421 | * (File.fs === "Macintosh") ? 422 | * function (f) { return (f instanceof Folder) || jamBooks.isColorBookFile (f) } : 423 | * "Color Book Files:*.acb,All Files:*.*"; 424 | * var colorBookFile = File.openDialog ("Open color book file:", acbFilter); 425 | * if (colorBookFile ) 426 | * { 427 | * var colorBookColors = jamBooks.getColorBookFileColors (colorBookFile, true); 428 | * if (colorBookColors) 429 | * { 430 | * alert ("First color:\r"+ jamJSON.stringify (colorBookColors["bookColors"][0], '\t')); 431 | * } 432 | * } 433 | */ 434 | jamBooks.getColorBookFileColors = function (colorBookFile, roundComponents) 435 | { 436 | var colorBookColors = null; 437 | var colorBookData = this.dataFromColorBookFile (colorBookFile, true); 438 | if (colorBookData !== 'string') 439 | { 440 | colorBookColors = { }; 441 | colorBookColors["bookFile"] = File.decode (colorBookFile.name); 442 | colorBookColors["bookName"] = localize (colorBookData["bookName"]); 443 | colorBookColors["bookDescription"] = localize (colorBookData["bookDescription"]); 444 | var colorNamePrefix = localize (colorBookData["colorNamePrefix"]); 445 | var colorNameSuffix = localize (colorBookData["colorNameSuffix"]); 446 | var colorSpace = colorBookData["colorSpace"]; 447 | var colors = colorBookData["colors"]; 448 | bookColors = [ ]; 449 | for (var i = 0; i < colors.length; i++) 450 | { 451 | var bookColor = { }; 452 | var colorName = localize (colors[i]["colorName"]); 453 | if (colorName) 454 | { 455 | bookColor["name"] = colorNamePrefix + colorName + colorNameSuffix; 456 | var components = colors[i]["actualComponents"]; 457 | var color = { }; 458 | switch (colorSpace) 459 | { 460 | case "CMYK": 461 | color["C"] = (roundComponents) ? Math.round (components[0]) : components[0]; 462 | color["M"] = (roundComponents) ? Math.round (components[1]) : components[1]; 463 | color["Y"] = (roundComponents) ? Math.round (components[2]) : components[2]; 464 | color["K"] = (roundComponents) ? Math.round (components[3]) : components[3]; 465 | break; 466 | case "Lab": 467 | color["L"] = (roundComponents) ? Math.round (components[0]) : components[0]; 468 | color["a"] = components[1]; 469 | color["b"] = components[2]; 470 | break; 471 | case "RGB": 472 | color["R"] = components[0]; 473 | color["G"] = components[1]; 474 | color["B"] = components[2]; 475 | break; 476 | } 477 | bookColor["color"] = color; 478 | bookColors.push (bookColor); 479 | } 480 | } 481 | colorBookColors["bookColors"] = bookColors; 482 | } 483 | return colorBookColors; 484 | }; 485 | } ()); 486 | } 487 | 488 | //------------------------------------------------------------------------------ 489 | 490 | -------------------------------------------------------------------------------- /src/jam/jamJSON.jsxinc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: jamJSON.jsxinc 3 | // Version: 4.5 4 | // Release Date: 2016-09-29 5 | // Copyright: © 2011-2016 Michel MARIANI 6 | // Licence: GPL 7 | //------------------------------------------------------------------------------ 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see . 20 | //------------------------------------------------------------------------------ 21 | // Version History: 22 | // 4.5: 23 | // - Incremented version number to keep in sync with other modules. 24 | // 4.4: 25 | // - Normalized error messages. 26 | // 4.0: 27 | // - Removed reference to 'this' for main global object. 28 | // 3.6: 29 | // - Incremented version number to keep in sync with other modules. 30 | // 3.5: 31 | // - Incremented version number to keep in sync with other modules. 32 | // 3.4: 33 | // - Incremented version number to keep in sync with other modules. 34 | // 3.3: 35 | // - Incremented version number to keep in sync with other modules. 36 | // 3.2: 37 | // - Incremented version number to keep in sync with other modules. 38 | // 3.1: 39 | // - Incremented version number to keep in sync with other modules. 40 | // 3.0: 41 | // - Incremented version number to keep in sync with other modules. 42 | // 2.0: 43 | // - Renamed jamJSON.js to jamJSON.jsxinc. 44 | // 1.0: 45 | // - Initial release. 46 | //------------------------------------------------------------------------------ 47 | 48 | /** 49 | * @fileOverview 50 | * @name jamJSON.jsxinc 51 | * @author Michel MARIANI 52 | */ 53 | 54 | if (typeof jamJSON !== 'object') 55 | { 56 | /** 57 | * @description Global object (used to simulate a namespace in JavaScript) containing customized JSON methods 58 | * for translating a JavaScript data structure to and from a JSON text string; can be used in scripts written with 59 | * the JSON Action Manager engine. 60 | *
    61 | *
  • 62 | * Adapted from: json_parse_state.js 63 | * and 64 | * json2.js 65 | * by Douglas CrockFord. 66 | *
  • 67 | *
  • 68 | * Spotted and fixed a few problems in the Photoshop implementation of the JavaScript interpreter: 69 | *
      70 | *
    • 71 | * In regular expressions, hexadecimal escape sequences (both \x and \u) must be in capital letters (A-F). 72 | *
    • 73 | *
    • 74 | * The precedence of nested ternary operators ( ? : ) is handled differently, extra parentheses must be used. 75 | *
    • 76 | *
    • 77 | * The test (state instanceof SyntaxError) returns true even if state is a ReferenceError; in fact, it seems that all error 78 | * types: Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, are just synonyms for each other; 79 | * therefore, one must check beforehand if a function is missing in the action table before calling it, so that no 80 | * ReferenceError gets ever thrown... 81 | *
    • 82 | *
    83 | *
  • 84 | *
85 | * @author Michel MARIANI 86 | * @version 4.5 87 | * @namespace 88 | */ 89 | var jamJSON = { }; 90 | // 91 | (function () 92 | { 93 | // The state of the parser, one of 94 | // 'go' The starting state 95 | // 'ok' The final, accepting state 96 | // 'firstokey' Ready for the first key of the object or the closing of an empty object 97 | // 'okey' Ready for the next key of the object 98 | // 'colon' Ready for the colon 99 | // 'ovalue' Ready for the value half of a key/value pair 100 | // 'ocomma' Ready for a comma or closing } 101 | // 'firstavalue' Ready for the first value of an array or an empty array 102 | // 'avalue' Ready for the next value of an array 103 | // 'acomma' Ready for a comma or closing ] 104 | var state; 105 | var stack; // The stack, for controlling nesting. 106 | var container; // The current container object or array 107 | var key; // The current key 108 | var value; // The current value 109 | // Escapement translation table 110 | var escapes = 111 | { 112 | '\\': '\\', 113 | '"': '"', 114 | '/': '/', 115 | 't': '\t', 116 | 'n': '\n', 117 | 'r': '\r', 118 | 'f': '\f', 119 | 'b': '\b' 120 | }; 121 | // The action table describes the behavior of the machine. It contains an object for each token. 122 | // Each object contains a method that is called when a token is matched in a state. 123 | // An object will lack a method for illegal states. 124 | var action = 125 | { 126 | '{': 127 | { 128 | go: function () 129 | { 130 | stack.push ({ state: 'ok' }); 131 | container = { }; 132 | state = 'firstokey'; 133 | }, 134 | ovalue: function () 135 | { 136 | stack.push ({ container: container, state: 'ocomma', key: key }); 137 | container = { }; 138 | state = 'firstokey'; 139 | }, 140 | firstavalue: function () 141 | { 142 | stack.push ({ container: container, state: 'acomma' }); 143 | container = { }; 144 | state = 'firstokey'; 145 | }, 146 | avalue: function () 147 | { 148 | stack.push ({ container: container, state: 'acomma' }); 149 | container = { }; 150 | state = 'firstokey'; 151 | } 152 | }, 153 | '}': 154 | { 155 | firstokey: function () 156 | { 157 | var pop = stack.pop (); 158 | value = container; 159 | container = pop.container; 160 | key = pop.key; 161 | state = pop.state; 162 | }, 163 | ocomma: function () 164 | { 165 | var pop = stack.pop (); 166 | container[key] = value; 167 | value = container; 168 | container = pop.container; 169 | key = pop.key; 170 | state = pop.state; 171 | } 172 | }, 173 | '[': 174 | { 175 | go: function () 176 | { 177 | stack.push ({ state: 'ok' }); 178 | container = [ ]; 179 | state = 'firstavalue'; 180 | }, 181 | ovalue: function () 182 | { 183 | stack.push ({ container: container, state: 'ocomma', key: key }); 184 | container = [ ]; 185 | state = 'firstavalue'; 186 | }, 187 | firstavalue: function () 188 | { 189 | stack.push ({ container: container, state: 'acomma' }); 190 | container = [ ]; 191 | state = 'firstavalue'; 192 | }, 193 | avalue: function () 194 | { 195 | stack.push ({ container: container, state: 'acomma' }); 196 | container = [ ]; 197 | state = 'firstavalue'; 198 | } 199 | }, 200 | ']': 201 | { 202 | firstavalue: function () 203 | { 204 | var pop = stack.pop (); 205 | value = container; 206 | container = pop.container; 207 | key = pop.key; 208 | state = pop.state; 209 | }, 210 | acomma: function () 211 | { 212 | var pop = stack.pop (); 213 | container.push (value); 214 | value = container; 215 | container = pop.container; 216 | key = pop.key; 217 | state = pop.state; 218 | } 219 | }, 220 | ':': 221 | { 222 | colon: function () 223 | { 224 | if (container.hasOwnProperty (key)) 225 | { 226 | throw new SyntaxError ("[jamJSON.parse] Duplicate key: “" + key + "”"); 227 | } 228 | state = 'ovalue'; 229 | } 230 | }, 231 | ',': 232 | { 233 | ocomma: function () 234 | { 235 | container[key] = value; 236 | state = 'okey'; 237 | }, 238 | acomma: function () 239 | { 240 | container.push (value); 241 | state = 'avalue'; 242 | } 243 | }, 244 | 'true': 245 | { 246 | go: function () 247 | { 248 | value = true; 249 | state = 'ok'; 250 | }, 251 | ovalue: function () 252 | { 253 | value = true; 254 | state = 'ocomma'; 255 | }, 256 | firstavalue: function () 257 | { 258 | value = true; 259 | state = 'acomma'; 260 | }, 261 | avalue: function () 262 | { 263 | value = true; 264 | state = 'acomma'; 265 | } 266 | }, 267 | 'false': 268 | { 269 | go: function () 270 | { 271 | value = false; 272 | state = 'ok'; 273 | }, 274 | ovalue: function () 275 | { 276 | value = false; 277 | state = 'ocomma'; 278 | }, 279 | firstavalue: function () 280 | { 281 | value = false; 282 | state = 'acomma'; 283 | }, 284 | avalue: function () 285 | { 286 | value = false; 287 | state = 'acomma'; 288 | } 289 | }, 290 | 'null': 291 | { 292 | go: function () 293 | { 294 | value = null; 295 | state = 'ok'; 296 | }, 297 | ovalue: function () 298 | { 299 | value = null; 300 | state = 'ocomma'; 301 | }, 302 | firstavalue: function () 303 | { 304 | value = null; 305 | state = 'acomma'; 306 | }, 307 | avalue: function () 308 | { 309 | value = null; 310 | state = 'acomma'; 311 | } 312 | } 313 | }; 314 | // The actions for number tokens 315 | var number = 316 | { 317 | go: function () 318 | { 319 | state = 'ok'; 320 | }, 321 | ovalue: function () 322 | { 323 | state = 'ocomma'; 324 | }, 325 | firstavalue: function () 326 | { 327 | state = 'acomma'; 328 | }, 329 | avalue: function () 330 | { 331 | state = 'acomma'; 332 | } 333 | }; 334 | // The actions for string tokens 335 | var string = 336 | { 337 | go: function () 338 | { 339 | state = 'ok'; 340 | }, 341 | firstokey: function () 342 | { 343 | key = value; 344 | state = 'colon'; 345 | }, 346 | okey: function () 347 | { 348 | key = value; 349 | state = 'colon'; 350 | }, 351 | ovalue: function () 352 | { 353 | state = 'ocomma'; 354 | }, 355 | firstavalue: function () 356 | { 357 | state = 'acomma'; 358 | }, 359 | avalue: function () 360 | { 361 | state = 'acomma'; 362 | } 363 | }; 364 | // 365 | var commentFunc = function () { }; // No state change 366 | // 367 | function debackslashify (text) 368 | { 369 | // Remove and replace any backslash escapement. 370 | return text.replace (/\\(?:u(.{4})|([^u]))/g, function (a, b, c) { return (b) ? String.fromCharCode (parseInt (b, 16)) : escapes[c]; }); 371 | } 372 | // 373 | /** 374 | * @description Convert a JSON text string into a JavaScript data structure.
375 | *
    376 | *
  • 377 | * Adapted from json_parse_state.js by Douglas Crockford: 378 | *
      379 | *
    • 380 | * Removed the reviver parameter. 381 | *
    • 382 | *
    • 383 | * Added an extra validate parameter; if false (or undefined), a simple eval is performed. 384 | *
    • 385 | *
    • 386 | * Added an extra allowComments parameter; if false (or undefined), validation does not allow comments (strict JSON). 387 | *
    • 388 | *
    • 389 | * Corrected the regular expression for numbers to conform to the JSON grammar; 390 | * cf. Introducing JSON and RFC 4627. 391 | *
    • 392 | *
    393 | *
  • 394 | *
395 | * @param {String} text JSON text string 396 | * @param {Boolean} [validate] validate JSON syntax while parsing 397 | * @param {Boolean} [allowComments] validate comments too 398 | * @returns {Object|Array|String|Number|Boolean|Null} JavaScript data structure (usually an object or array) 399 | * @see jamJSON.stringify 400 | * @example 401 | * var jsonText = '{ "Last Name": "Einstein", "First Name": "Albert" }'; 402 | * try 403 | * { 404 | * var jsObj = jamJSON.parse (jsonText, true); 405 | * alert (jsObj["First Name"] + " " + jsObj["Last Name"]); // -> Albert Einstein 406 | * } 407 | * catch (e) 408 | * { 409 | * alert ("E≠mc2!"); 410 | * } 411 | */ 412 | jamJSON.parse = function (text, validate, allowComments) 413 | { 414 | if (validate) 415 | { 416 | // Use a state machine rather than the dangerous eval function to parse a JSON text. 417 | // A regular expression is used to extract tokens from the JSON text. 418 | var tx = /^[\x20\t\n\r]*(?:([,:\[\]{}]|true|false|null)|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?)|"((?:[^\r\n\t\\\"]|\\(?:["\\\/trnfb]|u[0-9a-fA-F]{4}))*)")/; 419 | var txc = /^[\x20\t\n\r]*(?:(\/(?:\/.*|\*(?:.|[\r\n])*?\*\/))|([,:\[\]{}]|true|false|null)|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?)|"((?:[^\r\n\t\\\"]|\\(?:["\\\/trnfb]|u[0-9a-fA-F]{4}))*)")/; 420 | // The extraction process is cautious. 421 | var r; // The result of the exec method. 422 | var i; // The index shift in result array 423 | var actionFunc; // The current action function 424 | // Set the starting state. 425 | state = 'go'; 426 | // The stack records the container, key, and state for each object or array 427 | // that contains another object or array while processing nested structures. 428 | stack = [ ]; 429 | // If any error occurs, we will catch it and ultimately throw a syntax error. 430 | try 431 | { 432 | // For each token... 433 | while (true) 434 | { 435 | i = (allowComments) ? 1 : 0; 436 | r = (allowComments) ? txc.exec (text) : tx.exec (text); 437 | if (!r) 438 | { 439 | break; 440 | } 441 | // r is the result array from matching the tokenizing regular expression. 442 | // r[0] contains everything that matched, including any initial whitespace. 443 | // r[1] contains any punctuation that was matched, or true, false, or null. 444 | // r[2] contains a matched number, still in string form. 445 | // r[3] contains a matched string, without quotes but with escapement. 446 | if (allowComments && r[1]) 447 | { 448 | // Comment: just do nothing... 449 | actionFunc = commentFunc; 450 | } 451 | else if (r[i + 1]) 452 | { 453 | // Token: execute the action for this state and token. 454 | actionFunc = action[r[i + 1]][state]; 455 | } 456 | else if (r[i + 2]) 457 | { 458 | // Number token: convert the number string into a number value and execute 459 | // the action for this state and number. 460 | value = +r[i + 2]; 461 | actionFunc = number[state]; 462 | } 463 | else // Do not test r[i + 3] explicitely since a string can be empty 464 | { 465 | // String token: replace the escapement sequences and execute the action for 466 | // this state and string. 467 | value = debackslashify (r[i + 3]); 468 | actionFunc = string[state]; 469 | } 470 | // 471 | if (actionFunc) 472 | { 473 | actionFunc (); 474 | // Remove the token from the string. The loop will continue as long as there 475 | // are tokens. This is a slow process, but it allows the use of ^ matching, 476 | // which assures that no illegal tokens slip through. 477 | text = text.slice (r[0].length); 478 | } 479 | else 480 | { 481 | break; 482 | } 483 | } 484 | } 485 | catch (e) 486 | { 487 | // If we find a state/token combination that is illegal, then the action will 488 | // cause an error. We handle the error by simply changing the state. 489 | state = e; 490 | } 491 | // The parsing is finished. If we are not in the final 'ok' state, or if the 492 | // remaining source text contains anything except whitespace, then we did not have 493 | // a well-formed JSON text. 494 | if (state !== 'ok' || /[^\x20\t\n\r]/.test (text)) 495 | { 496 | throw state instanceof SyntaxError ? state : new SyntaxError ("[jamJSON.parse] Invalid JSON"); 497 | } 498 | return value; 499 | } 500 | else 501 | { 502 | // Let's live dangerously (but so fast)! ;-) 503 | return eval ('(' + text + ')'); 504 | } 505 | }; 506 | // 507 | var escapable = /[\\\"\x00-\x1F\x7F-\x9F\u00AD\u0600-\u0604\u070F\u17B4\u17B5\u200C-\u200F\u2028-\u202F\u2060-\u206F\uFEFF\uFFF0-\uFFFF]/g; 508 | var meta = // table of character substitutions 509 | { 510 | '\b': '\\b', 511 | '\t': '\\t', 512 | '\n': '\\n', 513 | '\f': '\\f', 514 | '\r': '\\r', 515 | '"' : '\\"', 516 | '\\': '\\\\' 517 | }; 518 | var gap; 519 | var indent; 520 | var prefixIndent; 521 | // 522 | function quote (string) 523 | { 524 | // If the string contains no control characters, no quote characters, and no 525 | // backslash characters, then we can safely slap some quotes around it. 526 | // Otherwise we must also replace the offending characters with safe escape 527 | // sequences. 528 | escapable.lastIndex = 0; 529 | return escapable.test (string) ? 530 | '"' + string.replace (escapable, function (a) { 531 | var c = meta[a]; 532 | return (typeof c === 'string') ? c : '\\u' + ('0000' + a.charCodeAt (0).toString (16).toUpperCase ()).slice (-4); 533 | }) + '"' : '"' + string + '"'; 534 | } 535 | // 536 | function str (value) // Produce a string from value. 537 | { 538 | var i; // The loop counter. 539 | var k; // The member key. 540 | var v; // The member value. 541 | var mind = gap; 542 | var partial; 543 | // What happens next depends on the value's type. 544 | switch (typeof value) 545 | { 546 | case 'string': 547 | return quote (value); 548 | case 'number': 549 | // JSON numbers must be finite. Encode non-finite numbers as null. 550 | return isFinite (value) ? String (value) : 'null'; 551 | case 'boolean': 552 | case 'null': 553 | // If the value is a boolean or null, convert it to a string. Note: 554 | // typeof null does not produce 'null'. The case is included here in 555 | // the remote chance that this gets fixed someday. 556 | return String (value); 557 | case 'object': // If the type is 'object', we might be dealing with an object or an array or null. 558 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 559 | // so watch out for that case. 560 | if (!value) 561 | { 562 | return 'null'; 563 | } 564 | // Make an array to hold the partial results of stringifying this object value. 565 | gap += indent; 566 | partial = [ ]; 567 | // Is the value an array? 568 | if (value.constructor === Array) 569 | { 570 | // The value is an array. Stringify every element. 571 | for (i = 0; i < value.length; i++) 572 | { 573 | partial[i] = str (value[i]); 574 | } 575 | // Join all of the elements together, separated with commas, and wrap them in brackets. 576 | v = (partial.length === 0) ? 577 | (gap ? '[\n' + prefixIndent + mind + ']' : '[ ]') : 578 | (gap ? '[\n' + prefixIndent + gap + partial.join (',\n' + prefixIndent + gap) + '\n' + prefixIndent + mind + ']' : '[ ' + partial.join (', ') + ' ]'); 579 | gap = mind; 580 | return v; 581 | } 582 | else 583 | { 584 | // Iterate through all of the keys in the object. 585 | for (k in value) 586 | { 587 | if (value.hasOwnProperty (k)) 588 | { 589 | v = str (value[k]); 590 | if (v) // Useless ? 591 | { 592 | partial.push (quote (k) + (gap && ((v.charAt (0) === '{') || (v.charAt (0) === '[')) ? ':\n' + prefixIndent + gap : ': ') + v); 593 | } 594 | } 595 | } 596 | // Join all of the member texts together, separated with commas, and wrap them in braces. 597 | v = (partial.length === 0) ? 598 | (gap ? '{\n' + prefixIndent + mind + '}' : '{ }') : 599 | (gap ? '{\n' + prefixIndent + gap + partial.join (',\n' + prefixIndent + gap) + '\n' + prefixIndent + mind + '}' : '{ ' + partial.join (', ') + ' }'); 600 | gap = mind; 601 | return v; 602 | } 603 | default: 604 | throw new SyntaxError ("[jamJSON.stringify] Invalid JSON"); 605 | } 606 | } 607 | // 608 | /** 609 | * @description Convert a JavaScript data structure into a JSON text string.
610 | *
    611 | *
  • 612 | * Adapted from json2.js by Douglas Crockford: 613 | *
      614 | *
    • 615 | * Removed the replacer parameter. 616 | *
    • 617 | *
    • 618 | * No handling of toJSON methods whatsoever. 619 | *
    • 620 | *
    • 621 | * Added an extra prefix parameter to allow the insertion of the resulting text into already-indented code. 622 | *
    • 623 | *
    • 624 | * Improved indenting so that pairs of brackets { } and [ ] are always aligned on the same vertical position. 625 | *
    • 626 | *
    • 627 | * Single spaces are systematically inserted for better readability when indenting is off. 628 | *
    • 629 | *
    • 630 | * A syntax error is thrown for any invalid JSON element (undefined, function, etc.). 631 | *
    • 632 | *
    633 | *
  • 634 | *
635 | * @param {Object|Array|String|Number|Boolean|Null} value JavaScript data structure (usually an object or array) 636 | * @param {String|Number} [space] Indent space string (e.g. "\t") or number of spaces 637 | * @param {String|Number} [prefix] Prefix space string (e.g. "\t") or number of spaces 638 | * @returns {String} JSON text string 639 | * @see jamJSON.parse 640 | * @example 641 | * var dummy = null; 642 | * var jsArr = 643 | * [ 644 | * 3.14E0, 645 | * 'Hello ' + 'JSON!', 646 | * { on: (0 === 0) }, 647 | * [ 1 + 1, dummy ] 648 | * ]; 649 | * alert (jamJSON.stringify (jsArr)); // -> [ 3.14, "Hello JSON!", { "on": true }, [ 2, null ] ] 650 | */ 651 | jamJSON.stringify = function (value, space, prefix) 652 | { 653 | // The stringify method takes a value, two optional parameters: space and prefix, and returns a JSON text. 654 | // Use of the space parameter can produce text that is more easily readable. 655 | // Use of the prefix parameter allows the insertion of the resulting text into some existing code already indented. 656 | var i; 657 | gap = ''; 658 | indent = ''; 659 | prefixIndent = ''; 660 | // 661 | // If the space parameter is a number, make an indent string containing that many spaces. 662 | if (typeof space === 'number') 663 | { 664 | for (i = 0; i < space; i++) 665 | { 666 | indent += ' '; 667 | } 668 | } 669 | else if (typeof space === 'string') // If the space parameter is a string, it will be used as the indent string. 670 | { 671 | indent = space; 672 | } 673 | // If the prefix parameter is a number, make a prefix indent string containing that many spaces. 674 | if (typeof prefix === 'number') 675 | { 676 | for (i = 0; i < prefix; i++) 677 | { 678 | prefixIndent += ' '; 679 | } 680 | } 681 | else if (typeof prefix === 'string') // If the prefix parameter is a string, it will be used as the prefix indent string. 682 | { 683 | prefixIndent = prefix; 684 | } 685 | // Return the result of stringifying the value. 686 | return prefixIndent + str (value); 687 | }; 688 | } ()); 689 | } 690 | 691 | //------------------------------------------------------------------------------ 692 | 693 | -------------------------------------------------------------------------------- /src/jam/jamShapes.jsxinc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: jamShapes.jsxinc 3 | // Version: 4.5 4 | // Release Date: 2016-09-29 5 | // Copyright: © 2011-2016 Michel MARIANI 6 | // Licence: GPL 7 | //------------------------------------------------------------------------------ 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see . 20 | //------------------------------------------------------------------------------ 21 | // Version History: 22 | // 4.5: 23 | // - Incremented version number to keep in sync with other modules. 24 | // 4.4: 25 | // - Normalized error messages. 26 | // 4.1: 27 | // - Simplified test in jamShapes.isCustomShapesPrefsFile (). 28 | // 4.0: 29 | // - Removed reference to 'this' for main global object. 30 | // 3.6: 31 | // - Incremented version number to keep in sync with other modules. 32 | // 3.5: 33 | // - Renamed field "id" to "ID" in data returned by dataFromCustomShapesFile (). 34 | // 3.4.3: 35 | // - Added parameter shapeIndex to dataFromCustomShapesFile (). 36 | // - Renamed field "uuid" to "id" in data returned by dataFromCustomShapesFile (). 37 | // 3.4.2: 38 | // - Added global option: jamShapes.debugMode. 39 | // 3.4.1: 40 | // - Cleaned up some code. 41 | // 3.4: 42 | // - Initial release. 43 | //------------------------------------------------------------------------------ 44 | 45 | /** 46 | * @fileOverview 47 | * @name jamShapes.jsxinc 48 | * @author Michel MARIANI 49 | */ 50 | 51 | //------------------------------------------------------------------------------ 52 | 53 | if (typeof jamShapes !== 'object') 54 | { 55 | /** 56 | * Global object (used to simulate a namespace in JavaScript) containing 57 | * a set of functions related to decoding custom shapes files into a format usable by scripts written with the 58 | * JSON Action Manager engine.
59 | * Uses information found in the document 60 | * Photoshop Custom Shapes File Format. 61 | * @author Michel MARIANI 62 | * @version 4.5 63 | * @namespace 64 | */ 65 | var jamShapes = { }; 66 | // 67 | (function () 68 | { 69 | /** 70 | * @description Test if a given file is a custom shapes file (*.csh). 71 | * @param {Object} file File object 72 | * @returns {Boolean} true if custom shapes file 73 | * @example 74 | * function customShapesFileFilter (f) 75 | * { 76 | * return (f instanceof Folder) || jamShapes.isCustomShapesFile (f); 77 | * } 78 | * var select = (File.fs === "Macintosh") ? customShapesFileFilter : "Custom Shapes Files:*.csh,All Files:*"; 79 | * var customShapesFile = File.openDialog ("Select a custom shapes file:", select); 80 | * if (customShapesFile !== null) 81 | * { 82 | * alert ("OK!"); 83 | * } 84 | */ 85 | jamShapes.isCustomShapesFile = function (file) 86 | { 87 | return (file.type === '8BCS') || file.name.match (/\.csh$/i); 88 | }; 89 | // 90 | /** 91 | * @description Test if a given file is a custom shapes preferences file (CustomShapes.psp). 92 | * @param {Object} file File object 93 | * @returns {Boolean} true if custom shapes preferences file 94 | * @example 95 | * function customShapesPrefsFileFilter (f) 96 | * { 97 | * return (f instanceof Folder) || jamShapes.isCustomShapesPrefsFile (f); 98 | * } 99 | * var select = (File.fs === "Macintosh") ? 100 | * customShapesPrefsFileFilter : 101 | * "Custom Shapes Preferences File:CustomShapes.psp,All Files:*.*"; 102 | * var customShapesPrefsFile = File.openDialog ("Select a custom shapes preferences file:", select); 103 | * if (customShapesPrefsFile !== null) 104 | * { 105 | * alert ("OK!"); 106 | * } 107 | */ 108 | jamShapes.isCustomShapesPrefsFile = function (file) 109 | { 110 | return file.name.match (/^CustomShapes.psp$/i); 111 | }; 112 | // 113 | /** 114 | * @description Convert a custom shapes file (*.csh or CustomShapes.psp) into a data structure in JSON format. 115 | * @param {String|Object} shapesFile Custom shapes file path string or File object 116 | * @param {Number} [shapeIndex] If defined, the returned information for each custom shape is limited to its name and ID 117 | * (no bounds, no path records) except for the shape located at this index; passing -1 will limit the information for 118 | * all custom shapes 119 | * @returns {Object|String} Converted custom shapes file data structure in JSON format, or error message string 120 | *

121 | * The custom shapes file data structure is defined as a JSON object { } with two members:
122 | * { "fileVersion": fileVersion, "customShapes": customShapes } 123 | *

124 | *

125 | * fileVersion: number
126 | * customShapes: JSON array [ ] of customShape 127 | *

128 | *

129 | * customShape: JSON object { } with four members:
130 | * { "name": name, "ID": ID, "bounds": bounds, 131 | * "pathRecords": pathRecords } 132 | *

133 | *

134 | * name: string
135 | * ID: string
136 | * bounds: JSON array [ ] with four items: 137 | * [ top, left, bottom, right ] 138 | *

139 | *

140 | * top: number
141 | * left: number
142 | * bottom: number
143 | * right: number
144 | *

145 | *

146 | * pathRecords: JSON array [ ] of pathRecord 147 | *

148 | *

149 | * pathRecord: JSON array [ ] with two items: [ selector, data } 150 | *

151 | * 152 | * 153 | * 154 | * 155 | * 156 | * 157 | * 158 | * 159 | * 160 | * 161 | * 162 | * 163 | * 164 | * 165 | * 166 | * 167 | * 168 | * 169 | * 170 | * 171 | * 172 | * 173 | * 174 | * 175 | * 176 | * 177 | * 178 | * 179 | * 180 | * 181 | * 182 | * 183 | * 184 | * 185 | * 186 | * 187 | * 188 | *
selectordata
"pathFill"null
"initialFill"number (0 or 1)
"closedLength"number
"closedLinked"JSON array [ ] with three items: [ backward, anchor, forward ]
"closedUnlinked"JSON array [ ] with three items: [ backward, anchor, forward ]
"openLength"number
"openLinked"JSON array [ ] with three items: [ backward, anchor, forward ]
"openUnlinked"JSON array [ ] with three items: [ backward, anchor, forward ]
189 | *

190 | * backward: JSON array [ ] with two items: [ vertical, horizontal ]
191 | * anchor: JSON array [ ] with two items: [ vertical, horizontal ]
192 | * forward: JSON array [ ] with two items: [ vertical, horizontal ]
193 | *

194 | *

195 | * vertical: number
196 | * horizontal: number
197 | *

198 | * @example 199 | * function customShapesFileFilter (f) 200 | * { 201 | * return (f instanceof Folder) || jamShapes.isCustomShapesFile (f); 202 | * } 203 | * var select = (File.fs === "Macintosh") ? customShapesFileFilter : "Custom Shapes Files:*.csh,All Files:*"; 204 | * var customShapesFile = File.openDialog ("Select a custom shapes file:", select); 205 | * if (customShapesFile !== null) 206 | * { 207 | * var fileData = jamShapes.dataFromCustomShapesFile (customShapesFile, -1); 208 | * if (typeof fileData === 'string') 209 | * { 210 | * alert (fileData + "\n" + "Custom shapes file: “" + File.decode (customShapesFile.name) + "”"); 211 | * } 212 | * else 213 | * { 214 | * alert ("Number of custom shapes: " + fileData["customShapes"].length); 215 | * } 216 | * } 217 | */ 218 | jamShapes.dataFromCustomShapesFile = function (shapesFile, shapeIndex) 219 | { 220 | function skipBytes (file, byteCount) 221 | { 222 | file.seek (byteCount, 1); 223 | } 224 | // 225 | function readBEInt (file, byteCount) 226 | { 227 | var bytes = file.read (byteCount); 228 | var intValue = 0; 229 | for (var index = 0; index < byteCount; index++) 230 | { 231 | intValue = (intValue << 8) + bytes.charCodeAt (index); 232 | } 233 | return intValue; 234 | } 235 | // 236 | function readBytes (file, byteCount) 237 | { 238 | return file.read (byteCount); 239 | } 240 | // 241 | function readPascalString (file) 242 | { 243 | var stringLength = readBEInt (file, 1); 244 | return readBytes (file, stringLength); 245 | } 246 | // 247 | function readUnicodeStringWithPadding (file) 248 | { 249 | var unicodeString = ""; 250 | var unicodeLength = readBEInt (file, 4); // Includes terminating null 251 | for (var index = 0; index < unicodeLength; index++) 252 | { 253 | var unicodeChar = readBEInt (file, 2); 254 | if (unicodeChar !== 0) 255 | { 256 | unicodeString += String.fromCharCode (unicodeChar); 257 | } 258 | } 259 | if ((unicodeLength % 2) !== 0) 260 | { 261 | skipBytes (file, 2); 262 | } 263 | return unicodeString; 264 | } 265 | // 266 | function readSignedBEInt32 (file) 267 | { 268 | var intValue = readBEInt (file, 4); 269 | return (intValue < 0x80000000) ? intValue : (intValue - 0x100000000); 270 | } 271 | // 272 | function readSignedBEFixed32 (file) 273 | { 274 | return readSignedBEInt32 (file) / 0x1000000; 275 | } 276 | // 277 | var file; 278 | if (typeof shapesFile === 'string') 279 | { 280 | file = new File (shapesFile); 281 | } 282 | else if (shapesFile instanceof File) 283 | { 284 | file = shapesFile; 285 | } 286 | else 287 | { 288 | throw new Error ('[jamShapes.dataFromCustomShapesFile] Invalid argument'); 289 | } 290 | // 291 | var selectorStrings = 292 | [ 293 | "closedLength", 294 | "closedLinked", 295 | "closedUnlinked", 296 | "openLength", 297 | "openLinked", 298 | "openUnlinked", 299 | "pathFill", 300 | "clipboard", 301 | "initialFill" 302 | ]; 303 | // 304 | var fileData; 305 | if (file.open ("r")) 306 | { 307 | try 308 | { 309 | file.encoding = 'BINARY'; 310 | var magicNumber = file.read (4); 311 | if (magicNumber === 'cush') 312 | { 313 | var fileVersion = readBEInt (file, 4); 314 | if (fileVersion === 2) 315 | { 316 | fileData = { }; 317 | fileData["fileVersion"] = fileVersion; 318 | var customShapes = [ ]; 319 | var customShapeCount = readBEInt (file, 4); 320 | for (var customShapeIndex = 0; customShapeIndex < customShapeCount; customShapeIndex++) 321 | { 322 | var customShape = { }; 323 | customShape["name"] = localize (readUnicodeStringWithPadding (file)); 324 | var unknown = jamUtils.dataToHexaString (readBytes (file, 4)); 325 | var dataLength = readBEInt (file, 4); 326 | var dataStart = file.tell (); 327 | customShape["ID"] = readPascalString (file); 328 | if ((typeof shapeIndex === 'undefined') || (shapeIndex === customShapeIndex)) 329 | { 330 | var top = readSignedBEInt32 (file); 331 | var left = readSignedBEInt32 (file); 332 | var bottom = readSignedBEInt32 (file); 333 | var right = readSignedBEInt32 (file); 334 | customShape["bounds"] = [ top, left, bottom, right ]; 335 | var pathRecords = [ ]; 336 | var pathRecordCount = Math.floor ((dataStart + dataLength - file.tell ()) / (2 + 8 + 8 + 8)); 337 | for (var pathRecordIndex = 0; pathRecordIndex < pathRecordCount; pathRecordIndex++) 338 | { 339 | var pathRecord = [ ]; 340 | var selector = readBEInt (file, 2); 341 | if ((selector >= 0) && (selector < selectorStrings.length)) 342 | { 343 | pathRecord.push (selectorStrings[selector]); 344 | } 345 | else 346 | { 347 | throw new Error ("[jamShapes.dataFromCustomShapesFile] Unknown selector: " + selector); 348 | } 349 | switch (selector) 350 | { 351 | case 6: 352 | pathRecord.push (null); 353 | skipBytes (file, 24); 354 | break; 355 | case 8: 356 | pathRecord.push (readBEInt (file, 2)); 357 | skipBytes (file, 24 - 2); 358 | break; 359 | case 0: 360 | case 3: 361 | pathRecord.push (readBEInt (file, 2)); 362 | skipBytes (file, 24 - 2); 363 | break; 364 | case 1: 365 | case 2: 366 | case 4: 367 | case 5: 368 | pathRecord.push 369 | ( 370 | [ 371 | [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ], 372 | [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ], 373 | [ readSignedBEFixed32 (file), readSignedBEFixed32 (file) ] 374 | ] 375 | ); 376 | break; 377 | default: 378 | pathRecord.push (null); 379 | skipBytes (file, 24); 380 | break; 381 | } 382 | pathRecords.push (pathRecord); 383 | } 384 | customShape["pathRecords"] = pathRecords; 385 | } 386 | file.seek (dataStart + dataLength, 0); 387 | customShapes.push (customShape); 388 | } 389 | fileData["customShapes"] = customShapes; 390 | } 391 | else 392 | { 393 | fileData = "Unrecognized custom shapes file version: " + fileVersion; 394 | } 395 | } 396 | else 397 | { 398 | fileData = "Unrecognized custom shapes file magic number: '" + magicNumber + "'"; 399 | } 400 | } 401 | catch (e) 402 | { 403 | fileData = e.message; 404 | } 405 | finally 406 | { 407 | file.close (); 408 | } 409 | } 410 | else 411 | { 412 | fileData = "Cannot open file"; 413 | } 414 | return fileData; 415 | }; 416 | // 417 | /** 418 | * @description Global option: if true, jamShapes.pathComponentsFromCustomShape () returns the path components of 419 | * the shape's bounding box instead of the shape itself. 420 | * @type Boolean 421 | * @default false 422 | * @see jamShapes.pathComponentsFromCustomShape 423 | * @example 424 | * var fileData = jamShapes.dataFromCustomShapesFile ("~/JSON Action Manager/tests/resources/Logo-X-Aqua.csh"); 425 | * if (typeof fileData === 'string') 426 | * { 427 | * alert (fileData); 428 | * } 429 | * else 430 | * { 431 | * var customShape = fileData["customShapes"][0]; 432 | * var bounds = [ [ 10, 10, 90, 90 ], "percentUnit" ]; 433 | * jamShapes.debugMode = true; 434 | * pathComponents = jamShapes.pathComponentsFromCustomShape (customShape, "add", bounds, true); 435 | * jamEngine.jsonPlay 436 | * ( 437 | * "set", 438 | * { 439 | * "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] }, 440 | * "to": jamHelpers.toPathComponentList (pathComponents) 441 | * } 442 | * ); 443 | * jamEngine.jsonPlay 444 | * ( 445 | * "fill", 446 | * { 447 | * "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] }, 448 | * "wholePath": { "<boolean>": true }, 449 | * "using": { "<enumerated>": { "fillContents": "color" } }, 450 | * "color": jamHelpers.nameToColorObject ("W3C", "Red"), 451 | * "opacity": { "<unitDouble>": { "percentUnit": 50 } }, 452 | * "mode": { "<enumerated>": { "blendMode": "normal" } }, 453 | * "radius": { "<unitDouble>": { "pixelsUnit": 0.0 } }, 454 | * "antiAlias": { "<boolean>": true } 455 | * } 456 | * ); 457 | * } 458 | */ 459 | jamShapes.debugMode = false; 460 | // 461 | /** 462 | * @description Get a JSON array of data (simplified path component values) and unit ID string for coordinates 463 | * from a custom shape data structure obtained from a converted custom shapes file (*.csh). 464 | * @param {Object} customShape Custom shape data structure in JSON format, obtained from a converted custom shapes file (*.csh) 465 | * @param {String} shapeOperation Shape operation:
466 | *
    467 | *
  • "add"
  • 468 | *
  • "subtract"
  • 469 | *
  • "intersect"
  • 470 | *
  • "xor"
  • 471 | *
472 | * @param {Array} bounds Path bounds rectangle with optional unit (either "distanceUnit" or "percentUnit" or "pixelsUnit"):
473 | * [ [ left, top, right, bottom ], unit ] 474 | * @param {Boolean} [constrainProportions] Constrain proportions using the custom shape aspect ratio (false by default) 475 | * @returns {Array} JSON array of data (simplified path component values) and unit ID string for coordinates 476 | * (cf. Path Component List Simplified Format); 477 | * if jamShapes.debugMode is true, returns the path components of the shape's bounding box instead of the shape itself 478 | * @see jamShapes.dataFromCustomShapesFile 479 | * @see jamShapes.debugMode 480 | * @example 481 | * var fileData = jamShapes.dataFromCustomShapesFile ("~/JSON Action Manager/tests/resources/Logo-X-Aqua.csh"); 482 | * if (typeof fileData === 'string') 483 | * { 484 | * alert (fileData); 485 | * } 486 | * else 487 | * { 488 | * var customShape = fileData["customShapes"][0]; 489 | * var bounds = [ [ 10, 10, 90, 90 ], "percentUnit" ]; 490 | * var pathComponents = jamShapes.pathComponentsFromCustomShape (customShape, "add", bounds, true); 491 | * jamEngine.jsonPlay 492 | * ( 493 | * "set", 494 | * { 495 | * "target": { "<reference>": [ { "path": { "<property>": "workPath" } } ] }, 496 | * "to": jamHelpers.toPathComponentList (pathComponents) 497 | * } 498 | * ); 499 | * } 500 | */ 501 | jamShapes.pathComponentsFromCustomShape = function (customShape, shapeOperation, bounds, constrainProportions) 502 | { 503 | var rectangle = bounds[0]; 504 | var unit = bounds[1]; // Optional, may be undefined 505 | var left = rectangle[0]; 506 | var top = rectangle[1]; 507 | var right = rectangle[2]; 508 | var bottom = rectangle[3]; 509 | var width = right - left; 510 | var height = bottom - top; 511 | if (constrainProportions) 512 | { 513 | var adjustmentFactor = 1; 514 | if ((typeof unit !== 'undefined') && (jamEngine.uniIdStrToId (unit) === jamEngine.uniIdStrToId ("percentUnit"))) 515 | { 516 | var saveMeaningfulIds = jamEngine.meaningfulIds; 517 | var saveParseFriendly = jamEngine.parseFriendly; 518 | jamEngine.meaningfulIds = true; 519 | jamEngine.parseFriendly = true; 520 | var resultDescObj = jamEngine.jsonGet ([ { "document": [ "", [ "ordinal", "first" ] ] } ]); 521 | jamEngine.meaningfulIds = saveMeaningfulIds; 522 | jamEngine.parseFriendly = saveParseFriendly; 523 | adjustmentFactor = resultDescObj["width"][1][1] / resultDescObj["height"][1][1]; 524 | } 525 | var boundsRatio = (width / height) * adjustmentFactor; 526 | var shapeWidth = customShape["bounds"][3] - customShape["bounds"][1]; 527 | var shapeHeight = customShape["bounds"][2] - customShape["bounds"][0]; 528 | var shapeRatio = shapeWidth / shapeHeight; 529 | if (shapeRatio > boundsRatio) 530 | { 531 | shapeHeight = (width / shapeRatio) * adjustmentFactor; 532 | top += (height - shapeHeight) / 2; 533 | height = shapeHeight; 534 | } 535 | else 536 | { 537 | shapeWidth = (height * shapeRatio) / adjustmentFactor; 538 | left += (width - shapeWidth) / 2; 539 | width = shapeWidth; 540 | } 541 | } 542 | var subpaths = [ ]; 543 | if (this.debugMode) 544 | { 545 | var subpath = 546 | [ 547 | [ [ left, top ] ], 548 | [ [ left + width, top ] ], 549 | [ [ left + width, top + height ] ], 550 | [ [ left, top + height ] ] 551 | ]; 552 | subpaths.push ([ subpath, true ]); 553 | } 554 | else 555 | { 556 | var pathRecords = customShape["pathRecords"]; 557 | var subLength = 0; 558 | for (var pathRecordIndex = 0; pathRecordIndex < pathRecords.length; pathRecordIndex++) 559 | { 560 | var pathRecord = pathRecords[pathRecordIndex]; 561 | var selector = pathRecord[0]; 562 | var data = pathRecord[1]; 563 | switch (selector) 564 | { 565 | case "closedLength": 566 | case "openLength": 567 | subLength = data; 568 | var closedSubpath = (selector === "closedLength"); 569 | var subpath = [ ]; 570 | break; 571 | case "closedLinked": 572 | case "closedUnlinked": 573 | case "openLinked": 574 | case "openUnlinked": 575 | var backward = 576 | [ 577 | left + (data[0][1] * width), 578 | top + (data[0][0] * height) 579 | ]; 580 | var anchor = 581 | [ 582 | left + (data[1][1] * width), 583 | top + (data[1][0] * height) 584 | ]; 585 | var forward = 586 | [ 587 | left + (data[2][1] * width), 588 | top + (data[2][0] * height) 589 | ]; 590 | var smooth = (selector === "closedLinked") || (selector === "openLinked"); 591 | subpath.push ([ anchor, forward, backward, smooth ]); 592 | if (--subLength === 0) 593 | { 594 | subpaths.push ([ subpath, closedSubpath ]); 595 | } 596 | break; 597 | } 598 | } 599 | } 600 | var pathComponentsArr = [ [ [ shapeOperation, subpaths ] ] ]; 601 | if (unit) 602 | { 603 | pathComponentsArr.push (unit); 604 | } 605 | return pathComponentsArr; 606 | }; 607 | } ()); 608 | } 609 | 610 | //------------------------------------------------------------------------------ 611 | 612 | -------------------------------------------------------------------------------- /src/jam/jamUtils.jsxinc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // File: jamUtils.jsxinc 3 | // Version: 4.5 4 | // Release Date: 2016-09-29 5 | // Copyright: © 2011-2016 Michel MARIANI 6 | // Licence: GPL 7 | //------------------------------------------------------------------------------ 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | // 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see . 20 | //------------------------------------------------------------------------------ 21 | // Version History: 22 | // 4.5: 23 | // - Incremented version number to keep in sync with other modules. 24 | // 4.4: 25 | // - Normalized error messages. 26 | // 4.0: 27 | // - Removed reference to 'this' for main global object. 28 | // 3.6: 29 | // - Incremented version number to keep in sync with other modules. 30 | // 3.5: 31 | // - Added jamUtils.mergeData (). 32 | // 3.4: 33 | // - Added jamUtils.readTextFile () and jamUtils.writeTextFile (), now used 34 | // by jamUtils.readJsonFile () and jamUtils.writeJsonFile () respectively. 35 | // - Added optional lowercase parameter to jamUtils.dataToHexaString (). 36 | // 3.3.1: 37 | // - Added 'TEXT' Mac OS type to newly created JSON file in 38 | // jamUtils.writeJsonFile (). 39 | // 3.3: 40 | // - Added jamUtils.loadActionSet (). 41 | // - Added extra file.writeln () when needed in jamUtils.writeJsonFile (). 42 | // - Added proper handling of null values in jamUtils.cloneData () and 43 | // jamUtils.getCustomOptions (). 44 | // 3.2: 45 | // - Incremented version number to keep in sync with other modules. 46 | // 3.1: 47 | // - Moved jamEngine.dataToHexaString () and jamEngine.hexaToDataString () 48 | // to jamUtils.dataToHexaString () and jamUtils.hexaToDataString (). 49 | // 3.0: 50 | // - Applied the redefined JSON AM Reference format. 51 | // 2.0: 52 | // - Initial release. 53 | //------------------------------------------------------------------------------ 54 | 55 | /** 56 | * @fileOverview 57 | * @name jamUtils.jsxinc 58 | * @author Michel MARIANI 59 | */ 60 | 61 | //------------------------------------------------------------------------------ 62 | 63 | if (typeof jamUtils !== 'object') 64 | { 65 | /** 66 | * Global object (used to simulate a namespace in JavaScript) containing 67 | * a set of utility functions for scripts written with the 68 | * JSON Action Manager engine. 69 | * @author Michel MARIANI 70 | * @version 4.5 71 | * @namespace 72 | */ 73 | var jamUtils = { }; 74 | // 75 | (function () 76 | { 77 | /** 78 | * @description Convert a distance in pixels to "distanceUnit" units. 79 | * @param {Number} amount Distance in pixels 80 | * @param {Number} amountBasePerInch Resolution of document in pixels per inch 81 | * @returns {Number} Distance in "distanceUnit" units (special pixels units at an absolute 72 dpi) 82 | * @see jamUtils.fromDistanceUnit 83 | * @example 84 | * jamEngine.jsonPlay 85 | * ( 86 | * "make", 87 | * { 88 | * "new": 89 | * { 90 | * "<object>": 91 | * { 92 | * "document": 93 | * { 94 | * "name": { "<string>": "Polaroid 4x5 Inch (Print)" }, 95 | * "mode": { "<class>": "RGBColorMode" }, 96 | * "width": 97 | * { 98 | * "<unitDouble>": 99 | * { 100 | * "distanceUnit": jamUtils.toDistanceUnit (4 * 300, 300) 101 | * } 102 | * }, 103 | * "height": 104 | * { 105 | * "<unitDouble>": 106 | * { 107 | * "distanceUnit": jamUtils.toDistanceUnit (5 * 300, 300) 108 | * } 109 | * }, 110 | * "resolution": { "<unitDouble>": { "densityUnit": 300 } }, 111 | * "depth": { "<integer>": 16 }, 112 | * "fill": { "<enumerated>": { "fill": "white" } }, 113 | * "profile": { "<string>": "Adobe RGB (1998)" } 114 | * } 115 | * } 116 | * } 117 | * } 118 | * ); 119 | */ 120 | jamUtils.toDistanceUnit = function (amount, amountBasePerInch) 121 | { 122 | return (amount / amountBasePerInch) * 72.0; 123 | // return (amount * 72.0) / amountBasePerInch; 124 | }; 125 | // 126 | /** 127 | * @description Convert a distance from "distanceUnit" units to pixels. 128 | * @param {Number} amount Distance in "distanceUnit" units (special pixels units at an absolute 72 dpi) 129 | * @param {Number} amountBasePerInch Resolution of document in pixels per inch 130 | * @returns {Number} Distance in pixels 131 | * @see jamUtils.toDistanceUnit 132 | * @example 133 | * jamEngine.meaningfulIds = true; 134 | * var resultDescObj = jamEngine.jsonGet ([ { "document": { "<enumerated>": { "ordinal": "first" } } } ]); 135 | * var width = resultDescObj["width"]["<unitDouble>"]["distanceUnit"]; 136 | * var height = resultDescObj["height"]["<unitDouble>"]["distanceUnit"]; 137 | * var resolution = resultDescObj["resolution"]["<unitDouble>"]["densityUnit"]; 138 | * var pixelsWidth = jamUtils.fromDistanceUnit (width, resolution); 139 | * var pixelsHeight = jamUtils.fromDistanceUnit (height, resolution); 140 | */ 141 | jamUtils.fromDistanceUnit = function (amount, amountBasePerInch) 142 | { 143 | return (amount / 72.0) * amountBasePerInch; 144 | // return (amount * amountBasePerInch) / 72.0; 145 | }; 146 | // 147 | /** 148 | * @description Test if a required font is available. 149 | * @param {String} fontPostScriptName Font PostScript name string 150 | * @returns {Boolean} Font found boolean 151 | * @example 152 | * if (jamUtils.fontExists ("Apple-Chancery")) 153 | * { 154 | * // Use the fancy Apple Chancery font... 155 | * } 156 | */ 157 | jamUtils.fontExists = function (fontPostScriptName) 158 | { 159 | var useDOM = true; 160 | // 161 | var found = false; 162 | if (useDOM) // Much faster !! 163 | { 164 | for (var i = 0; i < app.fonts.length; i++) 165 | { 166 | if (app.fonts[i].postScriptName === fontPostScriptName) 167 | { 168 | found = true; 169 | break; 170 | } 171 | } 172 | } 173 | else 174 | { 175 | var saveMeaningfulIds = jamEngine.meaningfulIds; 176 | var saveParseFriendly = jamEngine.parseFriendly; 177 | jamEngine.meaningfulIds = true; 178 | jamEngine.parseFriendly = true; 179 | // 180 | var resultDescriptorObj = jamEngine.jsonGet 181 | ( 182 | [ 183 | [ "property", [ "", "fontList" ] ], 184 | [ "application", [ "", [ "ordinal", "targetEnum" ] ] ] 185 | ] 186 | ); 187 | var fontPostScriptNameArr = resultDescriptorObj["fontList"][1][1]["fontPostScriptName"][1]; 188 | for (var i = 0; i < fontPostScriptNameArr.length; i++) 189 | { 190 | if (fontPostScriptNameArr[i][1] === fontPostScriptName) 191 | { 192 | found = true; 193 | break; 194 | } 195 | } 196 | // 197 | jamEngine.meaningfulIds = saveMeaningfulIds; 198 | jamEngine.parseFriendly = saveParseFriendly; 199 | } 200 | return found; 201 | }; 202 | // 203 | /** 204 | * @description Load an action if not currently available. 205 | * @param {String} action Action name string 206 | * @param {String} actionSet Action set name string 207 | * @param {String} actionsFilePath Actions file path string 208 | * @example 209 | * Folder.current = new Folder ("~/JSON Action Manager/tests/resources/"); 210 | * jamUtils.loadAction ("Cross Process 2", "Cross Processing", "Cross Processing.atn"); 211 | */ 212 | jamUtils.loadAction = function (action, actionSet, actionsFilePath) 213 | { 214 | try 215 | { 216 | jamEngine.jsonGet ([ [ "action", [ "", action ] ], [ "actionSet", [ "", actionSet ] ] ]); 217 | var found = true; 218 | } 219 | catch (e) 220 | { 221 | var found = false; 222 | } 223 | if (!found) 224 | { 225 | jamEngine.jsonPlay ("open", { "target": [ "", actionsFilePath ] }); 226 | } 227 | }; 228 | // 229 | /** 230 | * @description Load an action set if not currently available. 231 | * @param {String} actionSet Action set name string 232 | * @param {String} actionsFilePath Actions file path string 233 | * @example 234 | * Folder.current = new Folder ("~/JSON Action Manager/tests/resources/"); 235 | * jamUtils.loadActionSet ("Cross Processing", "Cross Processing.atn"); 236 | */ 237 | jamUtils.loadActionSet = function (actionSet, actionsFilePath) 238 | { 239 | try 240 | { 241 | jamEngine.jsonGet ([ [ "actionSet", [ "", actionSet ] ] ]); 242 | var found = true; 243 | } 244 | catch (e) 245 | { 246 | var found = false; 247 | } 248 | if (!found) 249 | { 250 | jamEngine.jsonPlay ("open", { "target": [ "", actionsFilePath ] }); 251 | } 252 | }; 253 | // 254 | /** 255 | * @description Load a preset if not currently available. 256 | * @param {String} presetProperty Preset property string:
257 | *
    258 | *
  • "brush"
  • 259 | *
  • "colors"
  • 260 | *
  • "gradientClassEvent"
  • 261 | *
  • "style"
  • 262 | *
  • "pattern"
  • 263 | *
  • "shapingCurve"
  • 264 | *
  • "customShape"
  • 265 | *
  • "toolPreset"
  • 266 | *
267 | * @param {String} presetName Preset name string 268 | * @param {String} presetFilePath Preset file path string 269 | * @example 270 | * Folder.current = new Folder ("~/JSON Action Manager/tests/resources/"); 271 | * jamUtils.loadPreset ("style", "Logo X-Aqua in Blue Glass (Button)", "Logo-X-Aqua.asl"); 272 | * jamUtils.loadPreset ("toolPreset", "Brush Tool Soft Round 35 Red", "Brush-35-Red.tpl"); 273 | */ 274 | jamUtils.loadPreset = function (presetProperty, presetName, presetFilePath) 275 | { 276 | var useDOM = false; 277 | var useOpen = true; 278 | // 279 | var classes = 280 | { 281 | // (property): (class) 282 | "brush": "brush", 283 | "colors": "color", 284 | "gradientClassEvent": "gradientClassEvent", 285 | "style": "styleClass", 286 | "pattern": "'PttR'", 287 | "shapingCurve": "shapingCurve", 288 | "customShape": "customShape", 289 | "toolPreset": "toolPreset" 290 | }; 291 | var presetClass = classes[presetProperty]; 292 | // 293 | var saveMeaningfulIds = jamEngine.meaningfulIds; 294 | var saveParseFriendly = jamEngine.parseFriendly; 295 | jamEngine.meaningfulIds = true; 296 | jamEngine.parseFriendly = true; 297 | // 298 | var found = false; 299 | var resultDescriptorObj = jamEngine.jsonGet 300 | ( 301 | [ 302 | [ "property", [ "", "presetManager" ] ], 303 | [ "application", [ "", [ "ordinal", "targetEnum" ] ] ] 304 | ] 305 | ); 306 | var presetManagerArr = resultDescriptorObj["presetManager"][1]; 307 | for (var i = 0; i < presetManagerArr.length; i++) 308 | { 309 | var presets = presetManagerArr[i][1]; 310 | if (presets[0] === presetClass) 311 | { 312 | var presetsArr = presets[1]["name"][1]; 313 | for (var j = 0; j < presetsArr.length; j++) 314 | { 315 | if (presetsArr[j][1] === presetName) 316 | { 317 | found = true; 318 | break; 319 | } 320 | } 321 | break; 322 | } 323 | } 324 | if (!found) 325 | { 326 | if (useDOM) 327 | { 328 | app.load (new File (presetFilePath)); 329 | } 330 | else if (useOpen) 331 | { 332 | jamEngine.jsonPlay ("open", { "target": [ "", presetFilePath ] }); 333 | } 334 | else 335 | { 336 | jamEngine.jsonPlay 337 | ( 338 | "set", 339 | { 340 | "target": 341 | [ 342 | "", 343 | [ 344 | [ "property", [ "", presetProperty ] ], 345 | [ "application", [ "", [ "ordinal", "targetEnum" ] ] ] 346 | ] 347 | ], 348 | "to": [ "", presetFilePath ], 349 | "append": [ "", true ] 350 | } 351 | ); 352 | } 353 | } 354 | // 355 | jamEngine.meaningfulIds = saveMeaningfulIds; 356 | jamEngine.parseFriendly = saveParseFriendly; 357 | }; 358 | // 359 | function getFileObject (file) 360 | { 361 | var fileObject; 362 | if (file instanceof File) 363 | { 364 | fileObject = file; 365 | } 366 | else if (typeof file === 'string') 367 | { 368 | fileObject = new File (file); 369 | } 370 | else 371 | { 372 | throw new Error ('[jamUtils getFileObject] Invalid argument'); 373 | } 374 | return fileObject; 375 | } 376 | // 377 | /** 378 | * @description Read a text file in UTF-8 encoding. 379 | * @param {Object|String} textFile Text file File object (or path string) 380 | * @returns {String|Null} Text string, or null if error 381 | * @see jamUtils.writeTextFile 382 | * @example 383 | * var text = jamUtils.readTextFile ("~/Desktop/test.txt"); 384 | */ 385 | jamUtils.readTextFile = function (textFile) 386 | { 387 | var text = null; 388 | var file = getFileObject (textFile); 389 | if (file.open ("r")) 390 | { 391 | text = file.read (); 392 | file.close (); 393 | } 394 | return text; 395 | }; 396 | // 397 | /** 398 | * @description Convert a JSON text file into a JavaScript data structure. 399 | * @param {Object|String} jsonFile JSON text file File object (or path string) 400 | * @returns {Object|Array|String|Number|Boolean|Null} JavaScript data structure (usually an object or array) 401 | * @see jamUtils.writeJsonFile 402 | * @example 403 | * var data = jamUtils.readJsonFile ("~/Desktop/test-data.json"); 404 | * alert ("color: " + jamJSON.stringify (data["color"])); // -> color: [ "RGBColor", [ 255, 0, 255 ] ] 405 | */ 406 | jamUtils.readJsonFile = function (jsonFile) 407 | { 408 | return jamJSON.parse (this.readTextFile (jsonFile), true); 409 | }; 410 | // 411 | /** 412 | * @description Write a text file in UTF-8 encoding. 413 | * @param {Object|String} textFile Text file File object (or path string) 414 | * @param {String} text Text string 415 | * @see jamUtils.readTextFile 416 | * @example 417 | * jamUtils.writeTextFile ("~/Desktop/test.txt", "test"); 418 | */ 419 | jamUtils.writeTextFile = function (textFile, text) 420 | { 421 | var file = getFileObject (textFile); 422 | if (file.open ('w', 'TEXT')) 423 | { 424 | file.encoding = 'UTF-8'; 425 | file.lineFeed = 'unix'; 426 | file.write ('\uFEFF'); // Byte Order Mark 427 | file.write (text); 428 | file.close (); 429 | } 430 | }; 431 | // 432 | /** 433 | * @description Convert a JavaScript data structure into a JSON text file. 434 | * @param {Object|String} jsonFile JSON text file File object (or path string) 435 | * @param {Object|Array|String|Number|Boolean|Null} data JavaScript data structure (usually an object or array) 436 | * @param {String|Number} [space] Indent space string (e.g. "\t") or number of spaces 437 | * @see jamUtils.readJsonFile 438 | * @example 439 | * var magenta = [ "RGBColor", [ 255, 0, 255 ] ]; 440 | * var data = 441 | * { 442 | * "width": 640, 443 | * "height": 480, 444 | * "color": magenta, 445 | * "title": "紅紫色" + "/" + "マゼンタ" 446 | * }; 447 | * jamUtils.writeJsonFile ("~/Desktop/test-data.json", data); 448 | */ 449 | jamUtils.writeJsonFile = function (jsonFile, data, space) 450 | { 451 | this.writeTextFile (jsonFile, jamJSON.stringify (data, space)); 452 | }; 453 | // 454 | /** 455 | * @description Get a clone (deep copy) of a Javascript data structure. 456 | * @param {Object|Array|String|Number|Boolean|Null} data Javascript data structure 457 | * @returns {Object|Array|String|Number|Boolean|Null} Clone (deep copy) of a Javascript data structure 458 | * @example 459 | * var customOptions; 460 | * var defaultOptions = 461 | * { 462 | * dialog: 463 | * { 464 | * x: 0, 465 | * y: 0, 466 | * width: 1024, 467 | * height: 768 468 | * } 469 | * }; 470 | * // 471 | * customOptions = jamUtils.cloneData (defaultOptions); 472 | * customOptions.dialog.width = 512; 473 | * alert (defaultOptions.dialog.width); // -> 1024 474 | * // 475 | * customOptions = defaultOptions; 476 | * customOptions.dialog.width = 512; 477 | * alert (defaultOptions.dialog.width); // -> 512 478 | */ 479 | jamUtils.cloneData = function (data) 480 | { 481 | var clone; 482 | if (data === null) // No constructor for null 483 | { 484 | clone = data; 485 | } 486 | else if (data.constructor === Object) 487 | { 488 | clone = { }; 489 | for (var k in data) 490 | { 491 | if (data.hasOwnProperty (k)) 492 | { 493 | clone[k] = this.cloneData (data[k]); 494 | } 495 | } 496 | } 497 | else if (data.constructor === Array) 498 | { 499 | clone = [ ]; 500 | for (var i = 0; i < data.length; i++) 501 | { 502 | clone.push (this.cloneData (data[i])); 503 | } 504 | } 505 | else 506 | { 507 | clone = data; 508 | } 509 | return clone; 510 | }; 511 | // 512 | /** 513 | * @description Merge two Javascript literal object data structures, using the second one as default base. 514 | * @param {Object} data Javascript literal object data structure 515 | * @param {Object} defaultData Javascript literal object data structure 516 | * @returns {Object} Merged Javascript literal object data structure 517 | * @example 518 | * var customOptions = 519 | * { 520 | * name: "foobar", 521 | * active: true, 522 | * dialog: 523 | * { 524 | * width: 512 525 | * } 526 | * }; 527 | * var defaultOptions = 528 | * { 529 | * name: "untitled", 530 | * enabled: false, 531 | * active: false, 532 | * dialog: 533 | * { 534 | * x: 0, 535 | * y: 0, 536 | * width: 1024, 537 | * height: 768 538 | * } 539 | * }; 540 | * // 541 | * customOptions = jamUtils.mergeData (customOptions, defaultOptions); 542 | * alert (jamJSON.stringify (customOptions, '\t')); 543 | * // -> 544 | * // { 545 | * // "name": "foobar", 546 | * // "active": true, 547 | * // "dialog": 548 | * // { 549 | * // "width": 512, 550 | * // "x": 0, 551 | * // "y": 0, 552 | * // "height": 768 553 | * // }, 554 | * // "enabled": false 555 | * // } 556 | */ 557 | jamUtils.mergeData = function (data, defaultData) 558 | { 559 | for (var k in defaultData) 560 | { 561 | if (defaultData.hasOwnProperty (k)) 562 | { 563 | if (k in data) 564 | { 565 | if ((defaultData[k] !== null) && (defaultData[k].constructor === Object)) 566 | { 567 | data[k] = this.mergeData (data[k], defaultData[k]); 568 | } 569 | } 570 | else 571 | { 572 | data[k] = this.cloneData (defaultData[k]); 573 | } 574 | } 575 | } 576 | return data; 577 | }; 578 | // 579 | var jsonCustomOptionsStringKey = "jsonCustomOptions"; 580 | // 581 | /** 582 | * @description Retrieve custom options associated with a unique signature [available in CS3 or later]. 583 | * @param {String} signature Unique signature string 584 | * @param {Object} defaultOptions Default options as a JavaScript literal object 585 | * @returns {Object} Custom options as a JavaScript literal object 586 | * @see jamUtils.eraseCustomOptions 587 | * @see jamUtils.putCustomOptions 588 | * @example 589 | * var signature = "unique-signature-custom-options"; 590 | * jamUtils.eraseCustomOptions (signature); 591 | * var customOptions = { name: "Rectangle" }; 592 | * jamUtils.putCustomOptions (signature, customOptions, true); 593 | * var defaultOptions = { width: 1024, height: 768, name: "Untitled" }; 594 | * customOptions = jamUtils.getCustomOptions (signature, defaultOptions); 595 | * if (customOptions.name !== "Untitled") 596 | * { 597 | * alert (jamJSON.stringify (customOptions.name + "-" + customOptions.width + "x" + customOptions.height)); 598 | * // -> "Rectangle-1024x768" 599 | * } 600 | */ 601 | jamUtils.getCustomOptions = function (signature, defaultOptions) 602 | { 603 | var saveMeaningfulIds = jamEngine.meaningfulIds; 604 | var saveParseFriendly = jamEngine.parseFriendly; 605 | jamEngine.meaningfulIds = true; 606 | jamEngine.parseFriendly = false; 607 | try 608 | { 609 | var resultObj = jamEngine.classIdAndActionDescriptorToJson (jamEngine.uniIdStrToId (signature), app.getCustomOptions (signature)); 610 | var customOptions = jamJSON.parse (resultObj[""][jsonCustomOptionsStringKey][""], true) 611 | } 612 | catch (e) 613 | { 614 | var customOptions = { }; 615 | } 616 | jamEngine.meaningfulIds = saveMeaningfulIds; 617 | jamEngine.parseFriendly = saveParseFriendly; 618 | return this.mergeData (customOptions, defaultOptions); 619 | }; 620 | // 621 | /** 622 | * @description Save custom options associated with a unique signature [available in CS3 or later]. 623 | * @param {String} signature Unique signature string 624 | * @param {Object} customOptions Custom options as a JavaScript literal object 625 | * @param {Boolean} [persistent] Whether the options should persist once the script has finished 626 | * @see jamUtils.eraseCustomOptions 627 | * @see jamUtils.getCustomOptions 628 | * @example 629 | * var signature = "unique-signature-custom-options"; 630 | * var customOptions = { width: 512, height: 512, name: "Square" }; 631 | * jamUtils.putCustomOptions (signature, customOptions, true); 632 | */ 633 | jamUtils.putCustomOptions = function (signature, customOptions, persistent) 634 | { 635 | var descriptorObj = { }; 636 | descriptorObj[jsonCustomOptionsStringKey] = [ "", jamJSON.stringify (customOptions) ]; 637 | app.putCustomOptions (signature, jamEngine.jsonToActionDescriptor (descriptorObj), persistent); 638 | }; 639 | // 640 | /** 641 | * @description Erase custom options associated with a unique signature [available in CS3 or later]. 642 | * @param {String} signature Unique signature string 643 | * @see jamUtils.getCustomOptions 644 | * @see jamUtils.putCustomOptions 645 | * @example 646 | * var signature = "unique-signature-custom-options"; 647 | * jamUtils.eraseCustomOptions (signature); 648 | */ 649 | jamUtils.eraseCustomOptions = function (signature) 650 | { 651 | app.eraseCustomOptions (signature); 652 | }; 653 | // 654 | /** 655 | * @description Convert a raw byte data string into a hexadecimal string. 656 | * @param {String} dataString Raw byte data string 657 | * @param {Boolean} [lowercase] Use lowercase hexadecimal digits (false by default) 658 | * @returns {String} Hexadecimal string 659 | * @see jamUtils.hexaToDataString 660 | * @example 661 | * var hexaString = jamUtils.dataToHexaString ("«Íï"); // -> "ABCDEF" 662 | */ 663 | jamUtils.dataToHexaString = function (dataString, lowercase) 664 | { 665 | var hexaDigits = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ]; 666 | var hexaString = ""; 667 | var length = dataString.length; 668 | for (var index = 0; index < length; index++) 669 | { 670 | var dataByte = dataString.charCodeAt (index); 671 | if ((dataByte >= 0x00) && (dataByte <= 0xFF)) 672 | { 673 | hexaString += hexaDigits[(dataByte & 0xF0) >> 4] + hexaDigits[dataByte & 0x0F]; 674 | } 675 | else 676 | { 677 | throw new Error ("[jamUtils.dataToHexaString] Invalid data string"); 678 | } 679 | } 680 | if (lowercase) 681 | { 682 | hexaString = hexaString.toLowerCase (); 683 | } 684 | return hexaString; 685 | }; 686 | // 687 | /** 688 | * @description Convert a hexadecimal string into a raw byte data string. 689 | * @param {String} hexaString Hexadecimal string 690 | * @returns {String} Raw byte data string 691 | * @see jamUtils.dataToHexaString 692 | * @example 693 | * var dataString = jamUtils.hexaToDataString ("00011E1F20217E7F80819E9FA0A1FEFF"); 694 | * // -> "\u0000\u0001\u001E\u001F !~\u007F\u0080\u0081\u009E\u009F ¡þÿ" 695 | */ 696 | jamUtils.hexaToDataString = function (hexaString) 697 | { 698 | var dataString = ""; 699 | var length = hexaString.length; 700 | if (((length % 2) === 0) && (/^[0-9A-Fa-f]*$/.test (hexaString))) 701 | { 702 | for (var index = 0; index < length; index += 2) 703 | { 704 | var byteString = hexaString.slice (index, index + 2); 705 | dataString += String.fromCharCode (parseInt (byteString, 16)); 706 | } 707 | } 708 | else 709 | { 710 | throw new Error ("[jamUtils.hexaToDataString] Invalid hexa string"); 711 | } 712 | return dataString; 713 | }; 714 | } ()); 715 | } 716 | 717 | //------------------------------------------------------------------------------ 718 | 719 | -------------------------------------------------------------------------------- /src/legacy.d.ts: -------------------------------------------------------------------------------- 1 | declare var Stdlib: any; 2 | declare function isMac(): any; 3 | declare function toBoolean(n: any): boolean; 4 | declare var GenericUI: any; 5 | declare var LogWindow: any; 6 | declare var MyAction: any; 7 | 8 | declare class jamJSON { 9 | static parse(text: string, validate?: boolean, allowComments?: boolean): any; 10 | static stringify(value: any, space?: string | number, prefix?: string | number): string; 11 | } 12 | -------------------------------------------------------------------------------- /src/my_action.js: -------------------------------------------------------------------------------- 1 | // 2 | // LabelPlus_Ps_Script.jsx 3 | // This is a Input Text Tool for LabelPlus Text File. 4 | // 5 | // Copyright 2015, Noodlefighter 6 | // Released under GPL License. 7 | // 8 | // License: http://noodlefighter.com/label_plus/license 9 | // 10 | //-include "stdlib.js" 11 | 12 | // 13 | // 动作库 14 | // 15 | MyAction = function() { } 16 | 17 | // 取消选择 18 | MyAction.selectNone = function() { 19 | var desc1 = new ActionDescriptor(); 20 | var ref1 = new ActionReference(); 21 | try{ 22 | ref1.putProperty(cTID('Chnl'), sTID("selection")); 23 | desc1.putReference(cTID('null'), ref1); 24 | desc1.putEnumerated(cTID('T '), cTID('Ordn'), cTID('None')); 25 | executeAction(sTID('set'), desc1, DialogModes.NO); 26 | } 27 | catch(e){} 28 | }; 29 | 30 | // 反向选择 31 | MyAction.selectInverse = function() { 32 | try{ 33 | executeAction(cTID('Invs'), undefined, DialogModes.NO); 34 | } 35 | catch(e){} 36 | }; 37 | 38 | // 选区收缩(像素) 39 | MyAction.selectContract = function(pxl) { 40 | var desc1 = new ActionDescriptor(); 41 | try{ 42 | desc1.putUnitDouble(cTID('By '), cTID('#Pxl'), pxl); 43 | executeAction(cTID('Cntc'), desc1, DialogModes.NO); 44 | } 45 | catch(e){} 46 | }; 47 | 48 | // 选区扩展(像素) 49 | MyAction.selectExpand = function(pxl) { 50 | var desc1 = new ActionDescriptor(); 51 | try{ 52 | desc1.putUnitDouble(cTID('By '), cTID('#Pxl'), pxl); 53 | executeAction(cTID('Expn'), desc1, DialogModes.NO); 54 | } 55 | catch(e){} 56 | }; 57 | 58 | // 魔棒(x, y, 容差, 采样所有图层, 抗锯齿, 新选区域方式字符串) 59 | // 新选区域方式字符串 可以为: 60 | // 'setd' 新建区域 61 | // 'addTo' 添加 62 | // 'subtractFrom' 移出 63 | // 'interfaceWhite' 交集 64 | MyAction.magicWand = function(x, y, tolerance, merged, antiAlias, newAreaModeStr) { 65 | try{ 66 | if(x == undefined || y == undefined){ 67 | x = 0; 68 | y = 0; 69 | } 70 | if(tolerance == undefined) 71 | tolerance = 32; 72 | if(merged == undefined) 73 | merged = false; 74 | if(antiAlias == undefined) 75 | antiAlias = true; 76 | if(newAreaModeStr == undefined || newAreaModeStr == '') 77 | newAreaModeStr = 'setd'; 78 | 79 | var desc1 = new ActionDescriptor(); 80 | var ref1 = new ActionReference(); 81 | ref1.putProperty(cTID('Chnl'), sTID("selection")); 82 | desc1.putReference(cTID('null'), ref1); 83 | var desc2 = new ActionDescriptor(); 84 | desc2.putUnitDouble(cTID('Hrzn'), cTID('#Pxl'), x); 85 | desc2.putUnitDouble(cTID('Vrtc'), cTID('#Pxl'), y); 86 | desc1.putObject(cTID('T '), cTID('Pnt '), desc2); 87 | desc1.putInteger(cTID('Tlrn'), tolerance); 88 | desc1.putBoolean(cTID('Mrgd'), merged); 89 | desc1.putBoolean(cTID('AntA'), antiAlias); 90 | executeAction(sTID(newAreaModeStr), desc1, DialogModes.NO); 91 | } 92 | catch(e){} 93 | 94 | }; 95 | 96 | // 新建图层 97 | MyAction.newLyr = function() { 98 | try{ 99 | var desc1 = new ActionDescriptor(); 100 | var ref1 = new ActionReference(); 101 | ref1.putClass(cTID('Lyr ')); 102 | desc1.putReference(cTID('null'), ref1); 103 | executeAction(cTID('Mk '), desc1, DialogModes.NO); 104 | } 105 | catch(e){} 106 | } 107 | // 删除当前图层 108 | MyAction.delLyr = function() { 109 | try{ 110 | var desc1 = new ActionDescriptor(); 111 | var ref1 = new ActionReference(); 112 | ref1.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt')); 113 | desc1.putReference(cTID('null'), ref1); 114 | executeAction(cTID('Dlt '), desc1, DialogModes.NO); 115 | } 116 | catch(e){} 117 | } 118 | 119 | // 填充(使用什么填充, 透明度) 120 | // use可以是: 121 | // 'FrgC' 前景色 122 | // 'BckC' 背景色 123 | // 'Blck' 黑色 124 | // 'Gry ' 灰色 125 | // 'Wht ' 白色 126 | MyAction.fill = function(use, opct) { 127 | try{ 128 | var desc1 = new ActionDescriptor(); 129 | desc1.putEnumerated(cTID('Usng'), cTID('FlCn'), cTID(use)); 130 | desc1.putUnitDouble(cTID('Opct'), cTID('#Prc'), opct); 131 | desc1.putEnumerated(cTID('Md '), cTID('BlnM'), cTID('Nrml')); 132 | executeAction(cTID('Fl '), desc1, DialogModes.NO); 133 | } 134 | catch(e){} 135 | }; 136 | 137 | "my_action.js"; 138 | -------------------------------------------------------------------------------- /src/text_parser.ts: -------------------------------------------------------------------------------- 1 | // LabelPlus专用格式TextReader 2 | /// 3 | 4 | namespace LabelPlus { 5 | 6 | export interface LpLabel { 7 | x: number; 8 | y: number; 9 | contents: string; 10 | group: string; 11 | }; 12 | 13 | export type LpLabelDict = { 14 | [key: string]: LpLabel[] 15 | }; 16 | 17 | export interface LpFile { 18 | path: string; 19 | groups: string[]; 20 | images: LpLabelDict; 21 | }; 22 | 23 | export function lpTextParser(path: string): LpFile | null 24 | { 25 | var f = new File(path); 26 | if (!f || !f.exists) { 27 | log_err("LabelPlusTextReader: file " + path + " not exists"); 28 | return null; 29 | } 30 | 31 | // 打开 32 | f.open("r"); 33 | f.encoding = 'UTF-8'; 34 | 35 | // json格式读取 36 | if (path.substring(path.lastIndexOf("."), path.length) == '.json') { 37 | f.open("r", "TEXT", "????"); 38 | f.lineFeed = "unix"; 39 | f.encoding = 'UTF-8'; 40 | var json = f.read(); 41 | var data = (new Function('return ' + json))(); 42 | f.close(); 43 | return data; 44 | } 45 | 46 | // 分行读取 47 | var state = 'start'; //'start','filehead','context' 48 | var notDealStr; 49 | var notDealLabelheadMsg; 50 | var nowFilename; 51 | var labelData = new Array(); 52 | var filenameList = new Array(); 53 | var groupData; 54 | var lineMsg; 55 | 56 | for (var i = 0; !f.eof; i++) { 57 | var lineStr = f.readln(); 58 | lineMsg = judgeLineType(lineStr); 59 | switch (lineMsg.Type) { 60 | case 'filehead': 61 | if (state == 'start') { 62 | //处理start blocks 63 | var result = readStartBlocks(notDealStr); 64 | if (!result) { 65 | log_err("readStartBlocks fail"); 66 | return null; 67 | } 68 | groupData = result.Groups; 69 | } 70 | else if (state == 'filehead') { 71 | } 72 | else if (state == 'context') { 73 | //保存label 74 | labelData[nowFilename].push( 75 | { 76 | LabelheadValue: notDealLabelheadMsg.Values, 77 | LabelString: notDealStr.trim() 78 | } 79 | ); 80 | } 81 | 82 | //新建文件项 83 | labelData[lineMsg.Title] = new Array(); 84 | filenameList.push(lineMsg.Title); 85 | nowFilename = lineMsg.Title; 86 | notDealStr = ""; 87 | state = 'filehead'; 88 | break; 89 | 90 | case 'labelhead': 91 | if (state == 'start') { 92 | log_err("start-filehead not found..."); 93 | return null; 94 | } 95 | else if (state == 'filehead') { 96 | } 97 | else if (state == 'context') { 98 | labelData[nowFilename].push( 99 | { 100 | LabelheadValue: notDealLabelheadMsg.Values, 101 | LabelString: notDealStr.trim() 102 | } 103 | ); 104 | } 105 | 106 | notDealStr = ""; 107 | notDealLabelheadMsg = lineMsg; 108 | state = 'context'; 109 | break; 110 | 111 | case 'unknown': 112 | notDealStr += "\r" + lineStr; 113 | break; 114 | } 115 | } 116 | 117 | if (state == 'context' && lineMsg.Type == 'unknown') { 118 | labelData[nowFilename].push( 119 | { 120 | LabelheadValue: notDealLabelheadMsg.Values, 121 | LabelString: notDealStr.trim() 122 | } 123 | ); 124 | } 125 | 126 | // output 127 | let label_dict: LpLabelDict = {}; 128 | for (let i = 0; i < filenameList.length; i++) { 129 | let img_name = filenameList[i]; 130 | let labels_of_image: LpLabel[] = new Array(); 131 | for (let j = 0; j < labelData[img_name].length; j++) { 132 | let data = labelData[img_name][j]; 133 | let l: LpLabel = { 134 | x: data.LabelheadValue[0], 135 | y: data.LabelheadValue[1], 136 | group: groupData[data.LabelheadValue[2] - 1], 137 | contents: data.LabelString, 138 | }; 139 | labels_of_image.push(l); 140 | } 141 | label_dict[img_name] = labels_of_image; 142 | } 143 | let dat: LpFile = { 144 | path: path, 145 | groups: groupData, 146 | images: label_dict, 147 | }; 148 | return dat; 149 | }; 150 | 151 | // 152 | // 判断字符串行类型 'filehead','labelhead','unknown' 153 | // filehead: >>>>>>[filename]<<<<<< 154 | // labelhead: ------[num]------[value, list] 155 | // 156 | function judgeLineType(str: string) { 157 | let index = 0; 158 | var result = { 159 | Type: 'unknown', 160 | Title: '', 161 | Values: [''], 162 | }; 163 | 164 | // FIXME handle invalid string format error 165 | str = str.trim(); 166 | if (str.substr(0, 6) == '>>>>>>') { // assumed to be a file name 167 | str = str.slice(2 + str.indexOf(">[")); 168 | if ((index = str.search(/\]<{6,}$/)) < 0) 169 | return result; 170 | result.Title = str.substring(0, index); 171 | result.Type = 'filehead'; 172 | } else if (str.substr(0, 6) == '------') { // assumed to be a label 173 | str = str.slice(2 + str.indexOf("-[")); 174 | if ((index = str.search(/\]-{6,}\[/)) < 0) 175 | return result; 176 | result.Title = str.substring(0, index); 177 | str = str.slice(2 + str.indexOf("-[")) 178 | if ((index = str.search(/\]$/)) < 0) 179 | return result; 180 | str = str.substring(0, index); 181 | result.Values = str.split(','); 182 | result.Type = 'labelhead'; 183 | } 184 | 185 | return result; 186 | }; 187 | 188 | function readStartBlocks(str: string) { 189 | var blocks = str.split("-"); 190 | if (blocks.length < 3) { 191 | log_err("Start blocks format error!"); 192 | return null; 193 | } 194 | 195 | //block1 文件头 196 | var filehead = blocks[0].split(","); 197 | if (filehead.length < 2) { 198 | log_err("filehead format error!"); 199 | return null; 200 | } 201 | var first_version = parseInt(filehead[0]); 202 | var last_version = parseInt(filehead[1]); 203 | 204 | //block2 分组信息 205 | var groups = blocks[1].trim().split("\r"); 206 | for (var i = 0; i < groups.length; i++) 207 | groups[i] = groups[i].trim(); 208 | 209 | //block末 210 | var comment = blocks[blocks.length - 1]; 211 | 212 | return { 213 | FirstVer: first_version, 214 | LastVer: last_version, 215 | Groups: groups, 216 | Comment: comment, 217 | }; 218 | }; 219 | 220 | } // namespace LabelPlus 221 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | namespace LabelPlus { 2 | export const VERSION: string = "1.7.4"; 3 | } 4 | -------------------------------------------------------------------------------- /src/xtools/COPYRIGHT: -------------------------------------------------------------------------------- 1 | $Id: COPYRIGHT,v 1.9 2014/05/01 19:09:13 anonymous Exp $ 2 | 3 | XTools is (generally) covered under a BSD-style licenese. See the file 4 | LICENESE in this folder. Use it as you wish. If you redistribute in 5 | original or modified form, include this copyright information and 6 | provide attribution as appropropriate. 7 | 8 | Other terms can be negotiated, if necessary. 9 | 10 | 11 | All files not explicitly listed later are covered by: 12 | Copyright: (c)2014, xbytor 13 | License: http://www.opensource.org/licenses/bsd-license.php 14 | Contact: xbytor@gmail.com 15 | 16 | xmlw3cdom.js, xmlsax.js, and xmlxpath.js are covered by: 17 | Copyright (C) 2000 - 2002, 2003 Michael Houghton (mike@idle.org), 18 | Raymond Irving and David Joham (djoham@yahoo.com) 19 | and by the LPGL2.1 license. 20 | More information on these scripts can be found at http://xmljs.sourceforge.net 21 | These files are used only by atn2xml.jsx. They can be removed if you use 22 | action2xml.jsx instead. 23 | 24 | 25 | The ieee754 code in xlib/ieee754 is covered by 26 | Copyright (c) 2003, City University of New York 27 | Details are in xlib/ieee754/IEEE-754.html 28 | 29 | perl -pi -e 's|http://creativecommons.org/licenses/LGPL/2.1|http://www.opensource.org/licenses/bsd-license.php|' 30 | 31 | -------------------------------------------------------------------------------- /src/xtools/ChangeLog: -------------------------------------------------------------------------------- 1 | ChangeLog for xtools 2 | $Id: ChangeLog,v 1.22 2015/12/03 22:51:08 anonymous Exp $ 3 | 4 | v2.3 5 | Added support for sidecar xmp files in XMPTools. 6 | 7 | Added getDocumentTable to get the color table of an image in 8 | ColorTable.jsx. 9 | 10 | Added new error codes to PSError.jsx and localized strings. 11 | 12 | Added isCC2015(). 13 | 14 | Miscellaneous fixes for CC2015 including a problem with 15 | Stdlib.wrapLCLayer. 16 | 17 | Added a large number of ZStrings to psx.jsx. 18 | 19 | I18N work on FileSavePanel. 20 | 21 | Fixed "ICCProfile" in XMPNameSpaces.jsx. 22 | 23 | SLCFix - now handles multi-line strings and checks to see 24 | if a match to /id/ matches a previously mapped variable name. 25 | 26 | Changed ActionXML Largeinteger to LargeInteger. 27 | 28 | v2.2 29 | Fixed typo in Install.jsx. 30 | 31 | Extended Stdlib.getXMPValue() to support File objects with cavaets. 32 | 33 | Added speedups in Stream when writing files, esp Action files. 34 | 35 | Added fix for layer.resize bug. 36 | 37 | Added new function Stdlib.resizeLayer(). 38 | 39 | Added support for DescValueType.LARGEINTEGERTYPE. This affects 40 | Stream and Action related library, and xapps/xapps scripts. 41 | 42 | Tweaked GetterDemo because of ScriptUI bug in CS6+. Will save to 43 | file if Application is selected. 44 | 45 | SLCFix code now replaces """ with ". This also affects LastLogEntry. 46 | There may be some edge cases where this breaks code, but it unbreaks 47 | far more SL generated code. 48 | 49 | Added isCC2014() and changed isCC(). 50 | 51 | Minor tweaks to xapps/apps scripts to improve usability/UI. 52 | 53 | Unfixed Stdlib._selectFile for CS6+. The underlying bug has been 54 | taken care of in subsequent PS revs. 55 | 56 | Validated compatiblity back to CS4 for most xapps/apps scripts. 57 | 58 | v2.1 59 | CC upgrade. 60 | 61 | Fixed font size bug. 62 | 63 | Added PSCCFontSizeBugFix.jsx. 64 | 65 | Fixed miscellaneous minor bugs. 66 | 67 | v2.0 68 | Fixed bugs, added new stuff, added support for CS6. 69 | 70 | Fixed Stdlib._selectFile to work around openDlg and saveDlg mask bugs. 71 | 72 | Upgraded for DropletDecompiler for CS5+. 73 | 74 | v1.5 75 | Added GenericUI.createProgressWindow(). 76 | 77 | Extensive reworking of FileSaveOptions panel - GenericUI.jsx 78 | 79 | Fixed problem with ActionList output - atn2js.jsx 80 | 81 | Added ActionsPaletteFile.write, loadRuntime routines - atn2bin.jsx 82 | 83 | Added dumpAscii - Stream.js 84 | 85 | Added support for Alias for Mac paths - ActionStream.js 86 | 87 | Fixed Stdlib.hasAction to handle case where more than one action set 88 | has the same name. 89 | 90 | Fixed bug in Getter.jsx for the case when there is no Background layer. 91 | 92 | EOF 93 | -------------------------------------------------------------------------------- /src/xtools/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, xbytor@gmail.com 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | · Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following 10 | disclaimer. 11 | 12 | · Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer 14 | in the documentation and/or other materials provided with 15 | the distribution. 16 | 17 | · Neither the name of the xbytor nor the names of its 18 | contributors may be used to endorse or promote products 19 | derived from this software without specific prior written 20 | permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | 35 | 36 | LINK: http://www.opensource.org/licenses/bsd-license.php -------------------------------------------------------------------------------- /src/xtools/README: -------------------------------------------------------------------------------- 1 | README for xtools 2 | $Id: README,v 1.8 2015/02/09 22:45:39 anonymous Exp $ 3 | 4 | This is the README file for XToolkit. I (xbytor, that is) am the author and 5 | maintainer of the package. There are a couple of things that I bundled in 6 | from other open source packages, and there are several ideas or code that 7 | came from the good people at adobeforums.com on the Photoshop Script forums 8 | as well as others on ps-scripts.com. 9 | 10 | COPYRIGHT 11 | This file contains copyright, copyleft, and license information for the files 12 | in this package. There is nothing scary in there. Basically, you can use this 13 | stuff however you want as long as you give credit where credit is due. 14 | 15 | INSTALLATION 16 | The instructions for installing this toolkit are here. It's pretty simple, 17 | really. Just extract the zip file to a temp directory and run 18 | xtools/xapps/Install.js from inside Photoshop and you'll be all set. 19 | If not, you can just move the unzipped xtools folder to a folder of 20 | your choosing. 21 | On OS X, the default installation folder is /Developer/xtools. 22 | On Windows, the default installation folder is C:\Program Files\Adobe\xtools. 23 | 24 | Enjoy. And remember: 25 | "The Software shall be used for Good, not Evil." 26 | 27 | xbytor@gmail.com 28 | December, 2004 29 | -------------------------------------------------------------------------------- /src/xtools/README.md: -------------------------------------------------------------------------------- 1 | # xtools 2 | 3 | 注意,此为经过裁减的xtools项目,仅保留了LabelPlus所需的部分文件。 4 | -------------------------------------------------------------------------------- /src/xtools/xlib/LogWindow.js: -------------------------------------------------------------------------------- 1 | // 2 | // LogWindow 3 | // This is UI code that provides a window for logging information 4 | // 5 | // $Id: LogWindow.js,v 1.15 2010/03/29 02:23:24 anonymous Exp $ 6 | // Copyright: (c)2005, xbytor 7 | // License: http://www.opensource.org/licenses/bsd-license.php 8 | // Contact: xbytor@gmail.com 9 | // 10 | //@show include 11 | 12 | LogWindow = function LogWindow(title, bounds, text) { 13 | var self = this; 14 | 15 | self.title = (title || 'Log Window'); 16 | self.bounds = (bounds || [100,100,740,580]); 17 | self.text = (text ? text : ''); 18 | self.useTS = false; 19 | self.textType = 'edittext'; // or 'statictext' 20 | self.inset = 15; 21 | self.debug = false; 22 | 23 | LogWindow.prototype.textBounds = function() { 24 | var self = this; 25 | var ins = self.inset; 26 | var bnds = self.bounds; 27 | var tbnds = [ins,ins,bnds[2]-bnds[0]-ins,bnds[3]-bnds[1]-35]; 28 | return tbnds; 29 | } 30 | LogWindow.prototype.btnPanelBounds = function() { 31 | var self = this; 32 | var ins = self.inset; 33 | var bnds = self.bounds; 34 | var tbnds = [ins,bnds[3]-bnds[1]-35,bnds[2]-bnds[0]-ins,bnds[3]-bnds[1]]; 35 | return tbnds; 36 | } 37 | 38 | LogWindow.prototype.setText = function setText(text) { 39 | var self = this; 40 | self.text = text; 41 | //fullStop(); 42 | if (self.win != null) { 43 | try { self.win.log.text = self.text; } catch (e) {} 44 | } 45 | } 46 | LogWindow.prototype.init = function(text) { 47 | var self = this; 48 | if (!text) text = ''; 49 | self.win = new Window('dialog', self.title, self.bounds); 50 | var win = self.win; 51 | win.owner = self; 52 | win.log = win.add(self.textType, self.textBounds(), text, 53 | {multiline:true}); 54 | win.btnPanel = win.add('panel', self.btnPanelBounds()); 55 | var pnl = win.btnPanel; 56 | pnl.okBtn = pnl.add('button', [15,5,115,25], 'OK', {name:'ok'}); 57 | pnl.clearBtn = pnl.add('button', [150,5,265,25], 'Clear', {name:'clear'}); 58 | if (self.debug) { 59 | pnl.debugBtn = pnl.add('button', [300,5,415,25], 'Debug', 60 | {name:'debug'}); 61 | } 62 | pnl.saveBtn = pnl.add('button', [450,5,565,25], 'Save', {name:'save'}); 63 | self.setupCallbacks(); 64 | } 65 | LogWindow.prototype.setupCallbacks = function() { 66 | var self = this; 67 | var pnl = self.win.btnPanel; 68 | 69 | pnl.okBtn.onClick = function() { this.parent.parent.owner.okBtn(); } 70 | pnl.clearBtn.onClick = function() { this.parent.parent.owner.clearBtn(); } 71 | if (self.debug) { 72 | pnl.debugBtn.onClick = function() { 73 | this.parent.parent.owner.debugBtn(); 74 | } 75 | } 76 | pnl.saveBtn.onClick = function() { this.parent.parent.owner.saveBtn(); } 77 | } 78 | LogWindow.prototype.okBtn = function() { this.close(1); } 79 | LogWindow.prototype.clearBtn = function() { this.clear(); } 80 | LogWindow.prototype.debugBtn = function() { $.level = 1; debugger; } 81 | LogWindow.prototype.saveBtn = function() { 82 | var self = this; 83 | // self.setText(self.text + self._prefix() + '\r\n'); 84 | self.save(); 85 | } 86 | 87 | LogWindow.prototype.save = function() { 88 | try { 89 | var self = this; 90 | var f = LogWindow.selectFileSave("Log File", 91 | "Log file:*.log,All files:*", 92 | "/c/temp"); 93 | if (f) { 94 | f.open("w") || throwError(f.error); 95 | try { f.write(self.text); } 96 | finally { try { f.close(); } catch (e) {} } 97 | } 98 | } catch (e) { 99 | alert(e.toSource()); 100 | } 101 | } 102 | 103 | LogWindow.prototype.show = function(text) { 104 | var self = this; 105 | if (self.win == undefined) { 106 | self.init(); 107 | } 108 | self.setText(text || self.text); 109 | return self.win.show(); 110 | } 111 | LogWindow.prototype.close = function(v) { 112 | var self = this; 113 | self.win.close(v); 114 | self.win = undefined; 115 | } 116 | LogWindow.prototype._prefix = function() { 117 | var self = this; 118 | if (self.useTS) { 119 | return LogWindow.toISODateString() + "$ "; 120 | } 121 | return ''; 122 | } 123 | LogWindow.prototype.prefix = LogWindow.prototype._prefix; 124 | LogWindow.prototype.append = function(str) { 125 | var self = this; 126 | self.setText(self.text + self.prefix() + str + '\r\n'); 127 | } 128 | LogWindow.prototype.clear = function clear() { 129 | this.setText(''); 130 | } 131 | 132 | LogWindow.toISODateString = function (date) { 133 | if (!date) date = new Date(); 134 | var str = ''; 135 | function _zeroPad(val) { return (val < 10) ? '0' + val : val; } 136 | if (date instanceof Date) { 137 | str = date.getFullYear() + '-' + 138 | _zeroPad(date.getMonth()+1) + '-' + 139 | _zeroPad(date.getDate()) + ' ' + 140 | _zeroPad(date.getHours()) + ':' + 141 | _zeroPad(date.getMinutes()) + ':' + 142 | _zeroPad(date.getSeconds()); 143 | } 144 | return str; 145 | } 146 | 147 | LogWindow.selectFileSave = function(prompt, select, startFolder) { 148 | var oldFolder = Folder.current; 149 | if (startFolder) { 150 | if (typeof(startFolder) == "object") { 151 | if (!(startFolder instanceof "Folder")) { 152 | throw "Folder object wrong type"; 153 | } 154 | Folder.current = startFolder; 155 | } else if (typeof(startFolder) == "string") { 156 | var s = startFolder; 157 | startFolder = new Folder(s); 158 | if (startFolder.exists) { 159 | Folder.current = startFolder; 160 | } else { 161 | startFolder = undefined; 162 | // throw "Folder " + s + "does not exist"; 163 | } 164 | } 165 | } 166 | var file = File.saveDialog(prompt, select); 167 | //alert("File " + file.path + '/' + file.name + " selected"); 168 | if (Folder.current == startFolder) { 169 | Folder.current = oldFolder; 170 | } 171 | return file; 172 | }; 173 | }; 174 | 175 | LogWindow.open = function(str, title) { 176 | var logwin = new LogWindow(title, undefined, str); 177 | logwin.show(); 178 | return logwin; 179 | }; 180 | 181 | function throwError(e) { 182 | throw e; 183 | }; 184 | 185 | "LogWindow.js"; 186 | // EOF 187 | 188 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "preserveConstEnums": true, 4 | "sourceMap": true, 5 | "removeComments": false, 6 | "strictNullChecks": true, 7 | "strictPropertyInitialization": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "lib": ["es5"], // exclude DOM and ScriptHost 10 | "types": ["./node_modules/photoshop.d.ts/dist/cc"] 11 | }, 12 | "files": [ 13 | "src/main.ts", 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | photoshop.d.ts@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.npmmirror.com/photoshop.d.ts/-/photoshop.d.ts-1.0.0.tgz" 8 | integrity sha512-bNJdjg+4ZFoxHh3ZVz1PY8Gi7r/2Xa597EsJS/ZzZCXyebvYQWNWhLuP/2iwjOf148NH7X5AF5mqrhTFxBxwoQ== 9 | 10 | typescript@^4.9.5: 11 | version "4.9.5" 12 | resolved "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz" 13 | integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== 14 | --------------------------------------------------------------------------------