本系列文章涉及的主题是PhotoShop plugin开发。
要做的:
由两篇文章组成:
基础绘制篇--以PhotoShop的Document为舞台,在上面绘制文字和任意形状
图层操作篇--PS最强大的是图层操作,通过操作图层,我们可以完成各种想做的事情,最经典的就是用于UI自动切图.
有了这些PSDom概念后,你会发现其实开发PS插件蛮简单的,PSDom非常强大。
不做的:
1) 不涉及Channel操作,对位图像素操作我们不关心
2) 不涉及PhotoShop界面编程。因为PS界面编程随着版本变化,有好多种方式,一直在改变。
C++方式,强大,但是难度较大,不是三言两语说得清楚的
As3 Flex方式,简单易用,和PSDom交互方便,但是适合Adobe CS系列
Html5方式,简单应用,和PSDom交互方便,但是适合Adobe CC系列
但不管如何,核心的PhotoShop操作是通过PSDom API公开出来的,
这个不管版本如何变,其本身DOM是不会发生较大变化的。
因此才更有研究的价值
PSDom的开发环境--ExtendScript ToolKit:
在安装Adobe cs/cc系列时,会自动安装(我装的是CS5.0)
如果在Windows下开发,请见后缀名改为.jsx
如果使用.js后缀的话,可能会由Windows Scripting Host(WSH)来执行,而不是CS 脚本解析器。防冲突!
通过这两篇文章,希望能够让大家在PhotoShop中为所欲为!!!
让我们来关注第一篇吧:
1)PS DOM(文档对象模型)用来操纵PS中的各个对象,可以使用AppleScript(mac专用),VBScript(windows专用),以及JavaScript(跨平台,本文档仅使用js代码来演示).
2)PS DOM中关键类图(竖线表示包含关系,且第三层开始的所有类都是所属于Document):
3)与绘制相关的对象:
二维绘制可以归类为三个绘制范畴:
1、位图/图像操作:
PS Dom通过Application.Document.Channel(通道)对象进行某个通道的数据操作(例如你可以分别对位图的四个通道R G B A 进行操作),这不是本文所关注的。
2、文本绘制:
PS Dom通过使用Application.Document.ArtLayer.TextItem进行文本的显示和相关操作。
由上面的寻址关系可以看到TextItem文本对象是被附加到PS中的一个Layer上的。
3、矢量绘制:
PS Dom 通过使用:
Application.Document.PathItem
Application.Document.PathItem.SubPathItem Application.Document.PathItem.SubPathItem.PathPoint
这三个类来进行相关操作的(实际上还有其他一些辅助类,具体我们在后面了解)
由上面的寻址关系可以看到PathItem路径对象是并没有像TextItem一样是被附加到PS中的一个Layer上的,而是独立于layer对象的。
4)JS运行调试环境和API参考文档:
安装好PhostShop CS5或更高的版本后,在你的PS安装目录下可以看到如下目录:
其中JavaScript Ref和Scripting Guide是你必须要进行查阅的参考,因为PS DOM开发网络上的信息非常少,所以这两篇文档是你基本唯一的依靠
Utilities 目录下:
scriptListener插件,用于PS Action录制时候将录制的步骤的js代码记录下来并输出到桌面文件。 是一个非常重要的具有开发功能插件,以后会有专门文章,目前不关注。
Sample Scripts 目录下:
双击运行
里面都是js演示代码:
双击后跳出Yes/No Alert界面:
Yes会打开PS并运行代码,显示脚本运行后的效果
No会打开JS开发环境,可以进行脚本开发,修改,运行,debug等
5)常用的基础代码结构流程:
1、js代码文件的开始
//每个JS文件的开始部分,这些代码基本可以说是固定的
// 如果有 #target photoshop,就直接打开Ps运行js脚本
//如果没有,则打开ps的脚本编辑器而不是直接在ps中运行
#target photoshop
// in case we double clicked the file
app.bringToFront();
2、单位标尺等数据的保存与恢复:
// 存储当前的状态
var startRulerUnits = app.preferences.rulerUnits;
var startTypeUnits = app.preferences.typeUnits;
var startDisplayDialogs = app.displayDialogs;
//设置新的状态,我们使用像素来作为标尺和文档中各个对象的计量单位
//当然也可以设置其他计量单位,例如厘米,英寸等
app.preferences.rulerUnits = Units.PIXELS;
app.preferences.typeUnits = TypeUnits.PIXELS;
app.displayDialogs = DialogModes.NO;
//之所以这么做,是因为ps的开发是基于状态机模式的
//所有图形化操作基本都是状态机,opengl d3d gdi quartz2d.......
3、如果你PS打开了很多程序,希望只显示现在正在操作的那个文档,那么可以使用下面代码先关闭所有文档
// 关闭所有打开的文档
while (app.documents.length != 0)
{
app.activeDocument.close();
}
4、使用open全局函数,打开一个PSD文件,并将该文件设置为当前活动的Document
var doc = open(File(app.path + "/示例/mytest.psd"));
app.activeDocument = buttonDoc;
5、 针对各种文档进行相关的操作
针对文档对象以及文档中的各个对象进行操作的代码是需要你来实现的!!
6、操作完文档后,需要恢复到原来的状态(恢复到开始时记录下来的状态值)
// 恢复到修改前的状态
app.preferences.rulerUnits = startRulerUnits;
app.preferences.typeUnits = startTypeUnits;
app.displayDialogs = startDisplayDialogs;
//状态机恢复default状态
- 文本操作(TextItem)
需求描述:
1、打印出ps所有的Font对象的信息,目的是为了了解TextFont对象主要属性: Family|name|postScriptName|style
function printAllInstalledTextFontInfo()
{
var allFonts = [];
for(var i = 0; i < app.fonts.length; i++)
{
var str = "{"+app.fonts[i].family+"|"+app.fonts[i].name+"|"
+app.fonts[i].postScriptName+"|"+app.fonts[i].style+"}";
allFonts.push (str);
}
alert(allFonts);
}
2、不要从现有的PSD载入,而是从无到有通过代码创建文档,层等
//创建一个Document对象
var docRef = app.documents.add(400, 300, 72);
//创建一个ArtLayer对象
var newTextLayer = docRef.artLayers.add();
//注意:一个文本对象必须要依附于一个Layer对象,并且Layer的kind必须是TEXT类型
newTextLayer.kind = LayerKind.TEXT;
3、使用微软雅黑并blod字体样式
//当设置为TEXT类型时,PS自动会创建一个TextItem对象给Layer,因此可以直接引用
newTextLayer.textItem.font = "MicrosoftYaHeiUI-Blod";
//这里需要注意的是,TextItem.font的数据类型是字符串,该字符串是使用了TextFont
//对象中的postScriptName,这一点务必要注意。
//由这里看出,前面 printAllInstalledTextFontInfo函数的作用了,你可以打印出来,查找
//postScriptName的名称,然后记住是用这个名称来设置字体name和style
4、绘制文字内容为"随风而行的PSDOM Demo"
//设置要显示的内容
newTextLayer.textItem.contents = "随风而行的PSDOM Demo";
5、设置文字的大小
newTextLayer.textItem.size = 36;
6、文字颜色为红色(SolidColor对象)
var textColor = new SolidColor;
textColor.rgb.red = 255;
textColor.rgb.green = 0;
textColor.rgb.blue = 0;
newTextLayer.textItem.color = textColor;
//这样就可以显示文字了
其他的文字相关属性请查阅文档
7)路径操作(PathItem)---核心操作
1、先来一段示例代码,能够对PathItem有个比较直观的了解
我们编写一个函数,用来显示多边形:
function DrawPolygon() {
//PS是状态机,基于当前选中的状态进行操作
var doc = app.activeDocument;
//获取参数的数量,使用js可变参数
//因为多边形参数不确定,例如三角形,三个顶点,n边形,n个顶点
var y = arguments.length;
var i = 0;
var lineArray = [];
for (i = 0; i < y; i++) {
//创建一个PathPointInfo对象
//里面包含绘制点的相关信息
lineArray[i] = new PathPointInfo;
//多边形是凸包,没有任何曲线段表示,因此每个点都是CORNERPOINT类型
//如果是曲线的话,那么每个点的类似是SMOOTHPOINT
lineArray[i].kind = PointKind.CORNERPOINT;
//要绘制的点的坐标,来源于参数,类型为[x,y];
//对于非曲线来说,leftDirection = rightDirection=anchor
lineArray[i].anchor = arguments[i];
lineArray[i].leftDirection = lineArray[i].anchor;
lineArray[i].rightDirection = lineArray[i].anchor;
}
//到此处,所有的绘制点的信息都保存在lineArray数组中
//创建一个SubPathInfo对象
var lineSubPathArray = new SubPathInfo();
//SubPathiInfo.entireSubPath指向了要绘制的顶点数据数组
lineSubPathArray.entireSubPath = lineArray;
//设置SubPathiInfo.closed为true,这样在strokePath时候,会自动封闭整个路径
//否则如果为false的话,那么会缺少最后一条线段,导致路径非封闭状态。
lineSubPathArray.closed = true;
//设置ShapeOperation为Add模式,叠加模式,前景层直接覆盖到背景层上
//还有其他也写操作,可以理解为布尔操作,例如前景和背景取并集,交集,差集等
lineSubPathArray.operation = ShapeOperation.SHAPEADD;
//创建一个PathItem对象,使用的是doc.pathItems.add方法
//注意,我们会发现是doc而不像TextItem是属于层对象的。
var myPathItem = doc.pathItems.add("myPath" , [lineSubPathArray]);
//调用PathItem的描边函数
//矢量图形绘制可以分为边的绘制以及封闭形体的填充两种操作
//strokePath用来进行边的绘制
//fillPath则用来进行填充内容
myPathItem.strokePath(ToolType.PENCIL);
//绘制好后,将PathItem去除掉,由于已经描边渲染了,所有所有效果都输出到
//像素缓冲区了,因此不需要该PathItem了
//如果你需要后续进行顶点级别的操作的话,那你也可以保留着,不要remove掉
myPathItem.remove();
}
2、测试多边形绘制:
//从两个点生成4个绘制点,绘制Rect
function DrawRect(left,top,right,bottom)
{
DrawPolygon( [left,top], [right,top], [right,bottom], [left,bottom] );
}
//由于strokePath时使用的颜色是基于当前的前景色的
//注意,如果是填充封闭路径fillPath的话,则使用指定颜色作为参数,但是描边是基于前
//景色的操作
//为了防止干扰,因此先记录下当前的前景色
var saveColor = app.foregroundColor;
//生成一个红色的SolidColor对象
var newColor = new SolidColor;
newColor.rgb.red = 255;
newColor.rgb.green = 0;
newColor.rgb.blue = 0;
//设置前景色为红色,并绘制三角形
app.foregroundColor = newColor;
DrawPolygon([250,10],[350,10],[250,100]);
//修改颜色为绿色
newColor.rgb.red = 0;
newColor.rgb.green = 255;
newColor.rgb.blue = 0;
//设置前景色为绿色,并绘制四边形
app.foregroundColor = newColor;
DrawRect(10,100,100,200);
//修改颜色为蓝色,绘制8角形
newColor.rgb.red = 0;
newColor.rgb.green = 0;
newColor.rgb.blue = 255;
app.foregroundColor = newColor;
DrawPolygon([36.9999885559082,13.9999985694885],[165.99999666214,13.9999985694885],[185.999989509583,33.9999973773956],[185.999989509583,61.9999945163727],[165.99999666214,81.9999992847443],[36.9999885559082,81.9999992847443],[16.9999957084656,61.9999945163727],[16.9999957084656,33.9999973773956]);
//完成后,将前景色恢复到以前记录下来的颜色
app.foregroundColor = saveColor;
在PhotoShop显示的结果如下:(我们使用Brush进行绘制,如果线条的话,可以使用Pencil)
如果使用填充模式,在PhotoShop中获得的效果:
3、PathItem对象模型:
由此可见,PathItem对象的读写是使用不同的类来表示的,切记!!
4、贝塞尔曲线曲面研究:
pathItem中的PathPoint有三个很重要的属性
pathPoint.anchor[x,y] The X and Y coordinates of the anchor point of the curve
pathPoint.leftDirection[a,b] The location of the left-direction endpoint (’in’position).
pathPoint.rightDirection[e,f] The location of the right-direction endpoint (’out’position).
For paths that are straight segments (not curved), the coordinates of all three points are the same.
如果是直线的话,leftDirection = rightDirection = anchor
但是如果是曲线的话,文档中的解释是
For curved segements, the the coordinates are different. The difference between the anchor point and the left or right direction points determines the arc of the curve. You use the left direction point to bend the curve "outward" or make it convex; you use the right direction point to bend the curve "inward" or make it concave.
这些描述非常抽象,而且ps dom文档只演示了非曲线曲面的demo,对于曲线曲面并没有demo,而且网上也很难查到相关资料
因此只有抽取数据进行查看了解其数据的格式以及猜测strokePath是如何绘制的
为了研究strokePath是如何绘制曲线的,必须先要取得矢量对象中所有曲线的点,看看是如何组成的,所以先将矢量对象所有的点输出到文件,以利于查看:
function saveFile(msg1,msg2,msg3,types)
{
//弹出saveFile对话框,save类型为txt文件
var file = File.saveDialog("Saving TXT file.", "TXT Files: *.txt");
if ( file == null ) return;
//如果已经存在,弹框确认是否覆盖重写
if (file.exists) {
if(!confirm(file.fsName + " exists.\\\\rOver write?")) return;
}
//打开要写的文件流,并且设置编码为utf16格式存储
file.open("w"); // open as write
file.encoding = "UTF16";
file.write("\\\\uFEFF"); // Unicode marker
//将anchor数据写入文件流
file.write("pts:");
file.write(msg1);
//将left Direction points数据写入文件流
file.write("lts:");
file.write(msg2);
file.write("rts:");
file.write(msg3);
//将right Direction points数据写入文件流
file.write("tps:");
file.write(types);
file.write("\\\\n");
//关闭文件流,写入完成
file.close();
}
//将每个pathItem中的每个subPathItem中的pathPoint值输出到文件参看具体数据
for(var i = 0; i < buttonDoc.pathItems.length; i++)
{
var subItems = buttonDoc.pathItems[i].subPathItems;
for(var j = 0; j < subItems.length; j++)
{
var subItem = subItems[j];
var pathPoints = subItem.pathPoints;
var pts = [];
var lefts = [];
var rights = [];
var types = []
for(var k = 0; k < pathPoints.length; k++)
{
pts.push(pathPoints[k].anchor);
lefts.push(pathPoints[k].leftDirection);
rights.push(pathPoints[k].rightDirection);
types.push(pathPoints[k].kind);
}
saveFile(pts,lefts,rights,types);
}
}
上面代码运行后会在桌面生成一个文件,例如叫PathItem.txt
我们将一个圆角矩形输出到文件,并且对浮点数进行四舍五入后获得所有数据,进行分析:
1、顶点的定义是左手系,顺时针方式 [0,1,2,3,4,5,6,7]
如上图所画形状。多边形的面是有正反面之分,ps中按照左手顺时针为正面。
2、线段是以Line_Strip或Line_Strip_Loop方式连接的
上图来源于opengl的图元类型中的线图源类型
ps使用的是:
Line_Strip(如果SubPathItem.closed = false)
Line_Strip_loop(如果SubPathItem.closed = true).
备注,OpenGL的顶点定义是右手系逆时针。而psdom中和d3d一样使用左手系表示
Line_Strip n个顶点确定n-1条线段 n >= 2
Line_Strip_Loop n个顶点确定n条线段,最后一条线段首位相连 n>=2
3、PathItem贝塞尔3次曲线中四个顶点的含义
如果要绘制上面的曲线,则顶点如下:
pathPoint0.anchor = p0;
pathPoint0.leftDirection = p0;
pathPoint0.rightDirection = r0;
pathPoint1.anchor = p1;
pathPoint1.leftDirection = l1;
pathPoint1.rightDirection = r1;
pathPoint1.anchor = p2;
pathPoint1.leftDirection = l2;
pathPoint1.rightDirection = p0;
具体还是来看一个完整的绘制圆角矩形的例子吧
function DrawRoundRetange(left,top,right,bottom,radius)
{
if(radius <= 0.01)
{
DrawRetangle(left,top,right,bottom);
return;
}
if(radius>=(Math.min(right-left, bottom-top))/2.0)
{
alert("半径太大");
//如何处理,要研究
//PS中、一般的情况下,是8个点
//但是在半径太大或者其他一些情况下,貌似用6个点
//目前还没法逆向出来,以后再研究吧!!
return;
}
var doc = app.activeDocument;
var i = 0;
var lineArray = [];
//下面代码以半径和顶点为参数,设置锚点和定位点
//具体算法上图演示
//这些算法需要有一定数学基础的,我正想开辟一个文章集,名为
//数学之美
lineArray[0] = new PathPointInfo;
lineArray[0].kind = PointKind.SMOOTHPOINT;
lineArray[0].anchor = [left+radius,top];
lineArray[0].leftDirection =lineArray[0].anchor;
lineArray[0].rightDirection = [left,top];
lineArray[1] = new PathPointInfo;
lineArray[1].kind = PointKind.SMOOTHPOINT;
lineArray[1].anchor = [right-radius,top];
lineArray[1].leftDirection = [right,top];
lineArray[1].rightDirection = lineArray[1].anchor;
lineArray[2] = new PathPointInfo;
lineArray[2].kind = PointKind.SMOOTHPOINT;
lineArray[2].anchor = [right,top+radius];
lineArray[2].leftDirection = lineArray[2].anchor;;
lineArray[2].rightDirection = [right,top];
lineArray[3] = new PathPointInfo;
lineArray[3].kind = PointKind.SMOOTHPOINT;
lineArray[3].anchor = [right,bottom-radius];
lineArray[3].leftDirection = [right,bottom];
lineArray[3].rightDirection = lineArray[3].anchor;
lineArray[4] = new PathPointInfo;
lineArray[4].kind = PointKind.SMOOTHPOINT;
lineArray[4].anchor = [right -radius,bottom];
lineArray[4].leftDirection = lineArray[4].anchor;
lineArray[4].rightDirection =[right,bottom];
lineArray[5] = new PathPointInfo;
lineArray[5].kind = PointKind.SMOOTHPOINT;
lineArray[5].anchor = [left+radius,bottom];
lineArray[5].leftDirection = [left,bottom];
lineArray[5].rightDirection = lineArray[5].anchor;
lineArray[6] = new PathPointInfo;
lineArray[6].kind = PointKind.SMOOTHPOINT;
lineArray[6].anchor = [left,bottom-radius];
lineArray[6].leftDirection = lineArray[6].anchor;
lineArray[6].rightDirection = [left,bottom];
lineArray[7] = new PathPointInfo;
lineArray[7].kind = PointKind.SMOOTHPOINT;
lineArray[7].anchor = [left,top+radius];
lineArray[7].leftDirection = [left,top];
lineArray[7].rightDirection = lineArray[7].anchor;
var lineSubPathArray = new SubPathInfo();
lineSubPathArray.closed = true;
lineSubPathArray.operation = ShapeOperation.SHAPEADD;
lineSubPathArray.entireSubPath = lineArray;
var myPathItem = doc.pathItems.add("myRoundedRetangle" ,
[lineSubPathArray]);
myPathItem.strokePath(ToolType.PENCIL);
//myPathItem.fillPath ();
myPathItem.remove();
}
我们来测试一下:
// 存储当前的状态
var startRulerUnits = app.preferences.rulerUnits;
var startTypeUnits = app.preferences.typeUnits;
var startDisplayDialogs = app.displayDialogs;
//设置新的状态
app.preferences.rulerUnits = Units.PIXELS;
app.preferences.typeUnits = TypeUnits.PIXELS;
app.displayDialogs = DialogModes.NO;
// 关闭所有打开的文档
while (app.documents.length != 0) {
app.activeDocument.close();
}
//创建一个新的500*500大小 dpi=72的文档对象,名称为PathItemTest
//并且设定为当前对象
var doc = app.documents.add(500,500,72, "PathItemTest");
app.activeDocument = doc;
//调用上面的函数
drawRoundRetange(20,20,200,200,50);
//恢复为系统初始化的状态
app.preferences.rulerUnits = startRulerUnits;
app.preferences.typeUnits = startTypeUnits;
app.displayDialogs = startDisplayDialogs;
至此,我们掌控Photoshop的第一篇结束,下一篇我们来关注如何进行PS Layer的操作,这也是非常有趣的一个话题。
注: 关于参数曲线曲面,其实还是有很多数学知识的,包括前段时间发的OpenGL太阳系Demo这篇bolg,并没有很详细的讲解代码,因为都涉及到很多计算机图形学的数学方法,因此随风计划开辟一个名为数学之美的文集,使用基于javascript /typescript的Canvas2D,WebGL来演示各种动画效果,体验数学之美之强大!
演示代码将在第二篇层操作发布时候一起提交到github,到时可以进行下载。