这里要介绍的 ExtendScript 是 Adobe 为旗下软件的提供的自动化脚本,前面已经提到过了,CEP 提供了的是扩展运行的环境和配套的界面方面的支持,而扩展要调用宿主的功能更多的还要依赖于使用 ExtendScript。
前面的文章已经多次提到过 ExtendScript 了,所以本文直接从 ExtendScript 具体的使用方法来介绍,其他的请参照先前文章:
「 1 」Hello World! – ExtendScript 调试
「 3 」API 的使用 – 执行 ExtendScript 代码
「 3 」API 的使用 – ExtendScript 事件监听
ExtendScript 通常可以使用 JavaScript、 AppleScript、VBScript 三种语言,其中 AppleScript 的版本功能要少一些,而 JavaScript 是最为常用的版本。CEP 中可以使用的也是 JavaScript 。
不同宿主的 ExtendScript 各有不同,毕竟 Photoshop、Illustrator 、InDesign 之间相差那么大,不过基本的编程风格是一样的,本文介绍的是 Photoshop 脚本。
本文只介绍 ExtendScript 的基本使用方法,和常用的操作,而调用宿主的各种功能请参照 Adobe 官方的参考文档(Reference),本文也翻译了一部分 Photoshop CC JavaScript Reference 供入门者查阅。
运行 ExtendScript
要调试和运行 ExtendScript 有 3 种方法:
- 第一种就是把写好的代码保存为 .jsx 文件,使用 Photoshop 菜单里“文件 – 脚本 – 浏览”功能打开这个文件来运行。
- 第二种是在 CEP 中使用
evalScript()
方法执行 ExtendScript 代码,这可以在 CEP 远程调试的控制台中使用,如果没有安装 Adobe ExtendScript Toolkit 的话,可以使用这种方法。 - 第三种就是安装 Adobe ExtendScript Toolkit CC ,在 Adobe ExtendScript Toolkit CC 中运行 ExtendScript 代码。推荐使用第三种方法。
Adobe ExtendScript Toolkit
你可以使用 Adobe Creative Cloud 桌面程序下载 ExtendScript Toolkit,如果使用的非正版或者没有 Creative Cloud 可以使用下面下的载地址
Windows (94 MB) | AdobeExtendScriptToolkit_4_LS22.exe [百度网盘] |
OS X (96 MB) | AdobeExtendScriptToolkit_4_LS22.dmg |
Adobe ExtendScript Toolkit 的使用方法非常简单,选择好宿主程序,在编辑框中写代码再点击 运行即可。
另外一个有用的功能是 Data Browser 窗口可以实时查看宿主应用的 DOM,这可以让你查看对象中有什么成员:
Holle World
首先,就从一个 Holle World 示例来了解 ExtendScript 大体是如何操作 PhotoShop 的:
//创建一个尺寸为 400 像素 X 400 像素的文档,分辨率为 72 ,文档名称为:"一个脚本创建的文件"。
//并且用变量 newDoc 记录这个图层。
var newDoc = app.documents.add(UnitValue("400 px"), UnitValue("400 px"), 72 ,"一个脚本创建的文件");
// 在 newDoc 中添加一个图层。并且用变量 newLayer 记录这个图层。
var newLayer = newDoc.artLayers.add();
//把图层 newLayer 的图层类型变为”文本“ ,图层转换为文本图层。
newLayer.kind = LayerKind.TEXT
//把图层 newLayer 的文本内容类型变为”文本框“。
newLayer.textItem.kind = TextType.PARAGRAPHTEXT
//设置图层 newLayer 的文本框宽度与高度。
newLayer.textItem.width = "300px"
newLayer.textItem.height = "150px"
//设置图层 newLayer 的文本框位置,横坐标 50 像素,纵坐标 100 像素。
newLayer.textItem.position= [UnitValue("50px"), UnitValue("100px")]
//设置 newLayer 的文本字体大小为“40 点”。
newLayer.textItem.size = UnitValue("40 pt")
//设置 newLayer 的文本内容。
newLayer.textItem.contents= "Holle World!"
//设置 newLayer 的文本框对齐方式为居中对齐。
newLayer.textItem.justification = Justification.CENTER
//创建一个色彩变量 c ,颜色为 #77bb11。
var c = new SolidColor();
c.rgb.hexValue = "77bb11";
//设置 newLayer 的文本颜色为 c。
newLayer.textItem.color = c;
另外 ExtendScript 没有控制台对象(console) ,不能使用 console.log()
这样的语句输出信息到控制台,不过 ExtendScript 中有起到同样作用的 $.writeln()
和
$.write()
。
DOM 操作
上面的示例,展现的是 ExtendScript 中最常用的用法:操作 DOM 中的对象来来操作文档。比如 app.documents
是所有打开的文档合集,可以通过索引号就能获得各个文档的对象: var doc = app.documents[0]
。
而一个对象有其“属性”和“方法”,修改属性可以修改类似于高度、宽度这样的数据:app.activeDocument.activeLayer.textItem.width = "300 px"
。
而方法通常是调用某个功能,比如这个调整图层亮度和对比度:app.activeDocument.activeLayer.adjustBrightnessContrast( 100, 45 )
。
DOM 操作虽然不能完成全部 PhotoShop 的操作(用动作代理的方式能完成几乎全部操作),但是使用最为直观和简单,是 ExtendScript 的基础。
DOM 中的各种对象可以在 ExtendScript Toolkit 的 Data Browser 窗口中实时查看,而各种对象的具体使用方法,需要参考 Adobe 官方发布的 《Photoshop CC 2015 JavaScript Reference》文档,由于这是英文的,对入门有些阻碍所以我翻译了常用部分:「 6 」 Photoshop 中文脚本参考 。
另外一些官方参考文档中没有的对象,还可以在 ExtendScript Toolkit 的 Object Model Viewer (按 F1 调出)中找到简单的介绍。
单位
单位是要最先介绍的,因为各种属性都会用的单位,比如尺寸是像素(px)、英寸(in),还有颜色的表示。
尺寸单位
尺寸单位是使用 UnitValue 对象,使用起来很简单,用 UnitValue ()
方法就能创建一个单位值。
可以直接在表达式里使用:
app.documents.add(UnitValue("400 px"), UnitValue("400 px"));
也可以先创建变量再使用:
var a = UnitValue("400 px")
app.documents.add(a, a);
用到的单位名可以是下面这些:
像素 | px | 点 | pt |
英寸 | in | 派卡 | pc |
厘米 | cm | 百分比 | % |
毫米 | mm |
更多介绍请查看:「 6 」 Photoshop 中文脚本参考 – UnitValue
颜色
颜色相对于尺寸单位,使用起来要麻烦一些,必须用变量存储才可使用:
var color = new SolidColor();
color.rgb.red = 255;
color.rgb.green = 128;
color.rgb.blue = 200;
app.activeDocument.activeLayer.textItem.color = color //设置当前图层文本颜色
通常会用颜色十六进制值代码来减少代码量:
var color = new SolidColor();
color.rgb.hexValue = "FFEEAA";
更多介绍请查看:「 6 」 Photoshop 中文脚本参考 – SolidColor
图层遍历
图层的合集有 artLayers
、layerSets
和 layers
三种,其中 artLayers
是文档根目录的图层的集合,layerSets
是图层组的合集。而 layers
是所有图层和图层组的集合。
简单来说我们可以 artLayers 和 layerSets 取得找到文档根目录图层或图层组。如:
//输出文档根目录“图层”名到控制台:
for (var i =0; i< app.activeDocument.artLayers.length; i++)
{
$.writeln("图层:" + app.activeDocument.artLayers[i].name) //输出图层名到控制台
}
//输出文档根目录“图层组”名到控制台:
for (var i =0; i< app.activeDocument.layerSets.length; i++)
{
$.writeln("图层组:" + app.activeDocument.layerSets[i].name) //输出图层组名到控制台
}
不过这两种只能检索到根目录一层的图层和图层组,而要遍历包括放在图层组中的所有图层和图层组需要使用 layers
。遍历到图层组 layerSet 后,可以用 layerSet.layers 继续遍历图层组中包含的内容:
var doc = app.activeDocument
var out ="" //创建存储遍历结果的变量
getLayers(doc.layers, 0);//遍历图层
alert(out );//输出遍历结果
function getLayers(layers, count)
{
for (var i =0; i<layers.length; i++)
{
if(layers[i].typename == "LayerSet")//判断是否是图层组
{
out = out + "\n " + repeat(" ",count) +"[" + layers[i].name +"]";
getLayers(layers[i].layers, count+1);//递归
}
else
{
out = out + "\n " + repeat(" ",count) + layers[i].name;
}
}
}
//重复字符串,这个用于生成缩进
function repeat(str, n)
{
var arr = new Array(n+1);
return arr.join(str);
}
文件与路径
在 Extension 中使用文件(读取或写入)都需要使用 File 对象,使用起来很简单,不过要注意由于\
在 Extension 中有转义符的作用,所以必须双写,像下面这样:
var a = File("E:\\备份\\新D.txt")
另外要注意的是在 Extension 内部使用的文件路径是 URI 形式的,也就是会把非 ASCII 字符转义成 ASCII 字符,比如上面这个路径是这样的:
/e/%E5%A4%87%E4%BB%BD%E6%B3%A8%E5%86%8C%E8%A1%A8%E5%A4%87%E4%BB%BD%5B2013-8-31%5D.reg
要想得到当前平台风格的的路径,需要使用 a.fsName
。
各种类型路径示例 | ||
a.fsName |
当前系统风格路径名 | E:\备份\新D.txt |
a.fullName |
完整路径 | /e/备份/新D.txt |
a.displayName |
文件显示名称 | 新D.txt |
a.name |
文件名 | /e/%E5%A4%87%E4%BB%BD/%E6%96%B0D.txt |
a.absoluteURI |
绝对路径 URI | /e/%E5%A4%87%E4%BB%BD/%E6%96%B0D.txt |
a.relative |
相对路径 URI | /e/%E5%A4%87%E4%BB%BD/%E6%96%B0D.txt |
动作代理
除了直接操作 DOM ,还有一种更加底层的操作宿主的方式:Action Manager ,与 DOM 不同,DOM 只是封装了宿主一部的操作,而动作代理几乎能完成所有宿主的操作。
比如下面展示使用动作代理改变当前文字图层消除锯齿方式为 “Windows LCD” 这使用 DOM 操作是无法完成的,DOM 的常数 antiAliasMethod
里没有收录 “Windows LCD”:
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putProperty( charIDToTypeID( "Prpr" ), charIDToTypeID( "AntA" ) );
ref.putEnumerated( charIDToTypeID( "TxLr" ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Trgt" ) );
desc.putReference( charIDToTypeID( "null" ), ref); // 向描述符中添加 ActionReference 类型的数据。
desc.putEnumerated( charIDToTypeID( "T " ), charIDToTypeID( "Annt" ), stringIDToTypeID( "antiAliasPlatformLCD" ) );
executeAction( charIDToTypeID( "setd" ), desc, DialogModes.NO );
看起来这比直接操作 DOM 要复杂多了,
这里来解释一下:
动作代理的关键方法是 executeAction( 事件, 动作描述符, 对话框模式 )
这个函数会执行一个动作事件。事件是一个 TypeID (实际运行 ID),它可以用 charIDToTypeID()
或 stringIDToTypeID
从特定的字符串转换得来。比如上面这个示例就是执行了一个 "setd"
事件,这个是代表“设置”事件。
另一个关键是动作描述符: ActionDescriptor
它存放有动作用到的各种参数。各种参数作为键值对存放在 ActionDescriptor
中,比如上面例子中向描述符 desc
中添加了一个 ActionReference 类型数据,和一个 Enumerated 类型的数据。一个 ActionDescriptor 可以有许多键值对,一个特殊的值类型是 ActionReference
, 这种类型会存放多种指定的请求类的数据。另外还有一种类型是 ActionList 它是用处类似于数组的数据类型。
可以简单理解 ActionDescriptor、ActionReference、ActionList 都是一种容器,用来存放各种指定数据类型的数据,这是直接调用 PhotoShop 底层接口的中间层,作为各种指令在 ExtendScript 与 PhotoShop 底层之间的代理。
操作监听器
关于动作代理的资料,Adobe 公布的很少,不过 Adobe 发布了一个操作监听插件,可以监听 PhotoShop 的每一步操作,并输出为 ExtendScript 的动作代理代码,这样就可以很方便的获得想要得到的操作的动作代理代码,用于修改。
动作监听器下载地址 :Scripting Listener Plug-in for Windows
动作监听器的使用很简单,把下载下来的监听器的 ScriptListener.8li 文件放入 PhotoShop 的 Plug-ins 目录,再重启 PhotoShop 就行了。
运行中的监听器没有界面,其输出的内容会放在桌面上的一个名为 ScriptingListenerJS.log
文本文件中,如果你在桌面上找到了这个文件,说明监听器运行成功了。要注意的是这个监听器是无法关闭的,要关闭只有把 ScriptListener.8li 文件从 Plug-ins 目录删除或者移出,不需要使用时记得要删除它,否则当你使用 Photoshop 时它会一直记录,输出的脚本文件体积越来越大,直到撑满硬盘。
在 Photoshop 中每一步操作都会被输出到这个文本文件中,所以为了查看输出的内容推荐使用一个可以实时监控文件内容的文本编辑器,比如 Everedit、虹吸墨等:
常量列表
在动作代理会用到许多 ID ,比如 charIDToTypeID( "Prpr" )
,这是 PhotoShop 的内部常量,每一个常量会有 charID(字符 ID,4 个字节)、StringID(字符串 ID,不定长度)、typeID(实际运行 ID,数字)。使用时要使用 typeID ,不过为了方便记忆和书写(或者随版本变化实际运行 ID 改变了),通常会用使用字符 ID 再通过 charIDToTypeID( )
、 stringIDToTypeID ( )
、 typeIDToCharID()
、typeIDToStringID( )
这几个函数来转化。
详细的常量列表可以参考:「 附录 · 1 」PhotoShop ExtendScript 常数表
示例:通过动作代理获取所有选中图层
上面的示例比较简单,只是简单的执行一个命令,这里展示一个执行命令并从返回值中获取数据的例子。这也是一个常用但无法直接操作 DOM 来完成的功能,ExtendScript 中可以用 app.activeDocument.activeLayer
获得当前选中图层,但是无法在多选情况下获得多选的图层,这个例子就是获取所有选中图层:
//获取所有选中图层的 ItemIndex ,存放在变量 selectedLayers 中:
var selectedLayers = getSelectedLayerItemIndex ();
//遍历图层名:
$.writeln("---------------------");
for (var i = 0; i < selectedLayers.length; ++i)
{
$.writeln("选中图层 ["+selectedLayers[i]+"]: " + layerItemIndexToName(selectedLayers[i]) );
}
//遍历图层(返回图层对象):
$.writeln("---------------------");
for (var i = 0; i < selectedLayers.length; ++i)
{
$.writeln(getArtLayerByItemIndex(selectedLayers[i]));
}
//取得所有被选择中的图层的 ItemIndex 。
getSelectedLayerItemIndex = function()
{
var resultLayerItemIndexs = []; //存放结果的数组
var backgroundIndexOffset = 0;
//取选中的图层:
var ref = new ActionReference();
var args = new ActionDescriptor();
ref.putProperty( charIDToTypeID('Prpr') , stringIDToTypeID("targetLayers") );
ref.putEnumerated( charIDToTypeID('Dcmn'), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') );
args.putReference( charIDToTypeID('null'), ref );
var resultDesc = executeAction( charIDToTypeID('getd'), args, DialogModes.NO );
//只选中了一个图层:
if (! resultDesc.hasKey( stringIDToTypeID("targetLayers") ))
{
resultLayerItemIndexs.push( app.activeDocument.activeLayer.itemIndex);
return resultLayerItemIndexs;
}
//选中了多个图层:
var selIndexList = resultDesc.getList( stringIDToTypeID("targetLayers") );
for (i = 0; i < selIndexList.count; ++i)
{
//ItemIndex 是从 1 开始,而 PS 内部序号是从 0 开始,所以这里需要 + 1。
resultLayerItemIndexs.push(selIndexList.getReference(i).getIndex( charIDToTypeID('Lyr ') ) + 1 );
$.writeln("r: " + selIndexList.getReference(i).getIndex( charIDToTypeID('Lyr ') ) + "+" + (1));
}
return resultLayerItemIndexs;
}
//根据图层 ItemIndex 获取图层名称
layerItemIndexToName = function( ItemIndex )
{
// 判断背景图层是否存在。背景图层无论存在与否始终占有是 0 位,所如果背景图层不存在,第一个图层就是内部序号就是 1 而不是 0 ,所以要 + 1。
backgroundIndexOffset = 1;
try {
if (app.activeDocument.backgroundLayer)
backgroundIndexOffset = 0;
}
catch (err)
{}
var ref = new ActionReference();
ref.putProperty( app.charIDToTypeID('Prpr'), stringIDToTypeID("name") );
//ItemIndex 是从 1 开始,而 PS 内部序号是从 0 开始,所以这里需要 - 1。
ref.putIndex( charIDToTypeID('Lyr '), ItemIndex -1 + backgroundIndexOffset );
var resultDesc = executeActionGet( ref );
return resultDesc.getString(stringIDToTypeID("name"));
}
//根据 ItemIndex 返回图层或图层组对象
getArtLayerByItemIndex = function(ItemIndex)
{
//要找的图层不在根目录,遍历所有图层组去寻找
return findInLayers(app.activeDocument.layers);//遍历图层
function findInLayers(layers)
{
for (var i =0; i<layers.length; i++)
{
if(layers[i].typename == "LayerSet")//判断是否是图层组
{
//要找到图层图层就是本图层组,直接返回结果:
if( layers[i].itemIndex == ItemIndex)
{
return layers[i];
}
//要找到图层不是本图层组,寻找图层组中的图层,找到就返回结果。如果没有找到这执行 catch ,如果找到多个同名图层则忽略 catch:
try
{
var tryF = layers[i].layers.getByName (layerItemIndexToName(ItemIndex))
if(tryF.itemIndex == ItemIndex){ return tryF;}
}
catch (err)
{
//要找的图层不是图层组中的图层,接着寻找本图层组中的图层组:
var r = findInLayers(layers[i].layerSets);
if(r != undefined){return r;}
continue;
}
//找到多个同名图层, 比较每个图层的 ItemIndex :
var r = findInLayers(layers[i].artLayers);
if(r != undefined){return r;}
}
else
{
// 比较图层 ItemIndex :
if( layers[i].itemIndex == ItemIndex)
{
return layers[i];
}
}
}
}
}
xtools
xtools 是一个第三方的 PhotoShop ExtendScript 开发工具包,有可直接使用的脚本(xapps)和用于开发的库(xlibs),很值得使用和参考。