前面三篇文章,我系统的介绍了Action Manager 的基本逻辑和使用方法。希望通过这三部曲能够让大家对Ps脚本编程中的黑魔法有一个更深的认识,并且可以自己上手写一些AM代码,基本上目前绝大多数的场景,我都不需要去网上找别人的代码来抄,而是可以自己去挖掘最终自己写出来想要的功能逻辑。
今天这篇教程,我们会顺着AM继续实战,介绍Ps中最重要的一块 – 图层 的常见操作,在课程结尾我们会沉淀出一份JS库文件,封装好了过程中写好的代码,便于日后使用。让我们现在开始吧~~
图层的基本操作 图层模块,可以说是Ps最重要的一个部分,绝大多数的插件、功能、脚本都躲不开图层的操作,比如要获取选中的图层的信息,遍历、移动、删除修改图层等。 所以,如果我们能够把图层的常见操作做一个封装,以后用到这些功能的时候,就会非常方便了。
我们可以创建一个Layer.jsx 文件,封装一个Layer 对象,通过Javascript的原型链来挂载我们需要的函数,这样以后针对每个图层操作,都是一个Layer实例,实例维护了一个id属性,这个id就是图层的id,因为Ps的图层有一些特点,它可以支持名称重复,图层的索引位置又是会被动态更改的,只有id是始终维持不变的,所以用它来作为基础索引操作是最合适的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Layer (id ) { this .id = id; } Layer .prototype .name = function ( ) { }
上面的代码采用的是原生JS实现,一方面直接在Ps中就能跑,另外一方面便于大家理解。我自己通常会更喜欢用TypeScript来写,这样更有利于代码维护,不过考虑到本文的读者可能有很多对TS不熟悉,于是都将采用原生JS来介绍。
1. 获取图层 我们的设计思路是每个需要操作的Ps图层能够对应上面的一个Layer 实例,所以我们先从获取图层开始,将获取到的图层,映射到Layer 对象上。由于获取图层和实例无关,我们可以使用类方法 挂到Layer 对象上,通常我们都是从获取用户当前选中的图层 开始。要获取用户选中的图层,根据前面文章介绍过的方法,通过遍历Document,可以拿到一个叫targetLayersIDs 这个属性,这个就是当前选中的图层的id列表,这就满足我们的诉求了,我们把这个id列表拿出来,实例化出Layer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function Layer (id ) { this .id = id; } Layer .getSelectedLayers = function ( ) { var selectedLayersReference = new ActionReference (); selectedLayersReference.putProperty (charIDToTypeID ("Prpr" ), stringIDToTypeID ("targetLayersIDs" )); selectedLayersReference.putEnumerated (charIDToTypeID ("Dcmn" ), charIDToTypeID ("Ordn" ), charIDToTypeID ("Trgt" )); var desc = executeActionGet (selectedLayersReference); var layers = []; if (desc.hasKey (stringIDToTypeID ("targetLayersIDs" ))) { var list = desc.getList (stringIDToTypeID ("targetLayersIDs" )); for (var i=0 ; i<list.count ; i++) { var ar = list.getReference (i); var layerId = ar.getIdentifier (); layers.push (new Layer (layerId)); } } return layers; }
这样,getSelectedLayers 方法就可以拿到当前选中图层的id,并且返回了一个Layer实例数组,后续我们就可以根据这个数组中的Layer实例进行操作了。
2.获取图层属性 有了Layer 实例之后,结合前面文章我们介绍的知识,就可以通过图层id来获取该图层的所有属性了,还记得AM中篇 里头那张图么?
有了图层id之后,我们就可以通过AM代码,根据这个id来获取该图层的各种属性值,于是我们继续扩展Layer类的方法,将这些属性的获取封装成函数,挂在Layer类的原型链上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 function Layer (id ) { this .id = id; } Layer .prototype .name = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), charIDToTypeID ("Nm " )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); return descriptor.getString (charIDToTypeID ("Nm " )); } Layer .prototype .index = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), charIDToTypeID ("ItmI" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); return descriptor.getInteger (charIDToTypeID ("ItmI" )); } Layer .prototype .kind = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), stringIDToTypeID ("layerKind" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); return descriptor.getInteger (stringIDToTypeID ("layerKind" )); } Layer .prototype .bounds = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), stringIDToTypeID ("bounds" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var layerDescriptor = executeActionGet (layerReference); var rectangle = layerDescriptor.getObjectValue (stringIDToTypeID ("bounds" )); var left = rectangle.getUnitDoubleValue (charIDToTypeID ("Left" )); var top = rectangle.getUnitDoubleValue (charIDToTypeID ("Top " )); var right = rectangle.getUnitDoubleValue (charIDToTypeID ("Rght" )); var bottom = rectangle.getUnitDoubleValue (charIDToTypeID ("Btom" )); return {x : left, y : top, width : (right - left), height : (bottom - top)}; } Layer .prototype .visible = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), charIDToTypeID ("Vsbl" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); if (descriptor.hasKey (charIDToTypeID ("Vsbl" )) == false ) return false ; return descriptor.getBoolean (charIDToTypeID ("Vsbl" )); }
上面的几个函数,都是通过图层id来获取对应的图层属性,几乎所有的图层属性都可以通过这种方式来获取,大家可以根据【CEP教程-8】Action Manager从好奇到劝退 - 中篇 这篇文章中介绍的方法,来拿到图层所有的属性,并且将这些属性的获取封装成对应的方法,补充到上面的Layer类当中。上面这些属性都是比较简单的,下面介绍获取稍微复杂一点的,如下图
一个形状图层,有一个颜色填充,和一个描边的图层效果,我们希望获取到这两个数据,我们可以从遍历图层属性中得到对应属性值的JSON结构如下
从json数据结构就很容易能够把对应的数据获取出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 Layer .prototype .solidFill = function ( ) { var kind = this .kind (); if (kind === 4 ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), stringIDToTypeID ("adjustment" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); var adjustment = descriptor.getList (stringIDToTypeID ("adjustment" )); var result = []; for (var i = 0 ; i < adjustment.count ; i++) { var item = adjustment.getObjectValue (i); var color = item.getObjectValue (stringIDToTypeID ("color" )); var red = color.getInteger (stringIDToTypeID ("red" )); var green = color.getInteger (stringIDToTypeID ("grain" )); var blue = color.getInteger (stringIDToTypeID ("blue" )); result.push ({"red" : red, "green" : green, "blue" : blue}); } return result; } return null ; } Layer .prototype .strokeFx = function ( ) { var layerReference = new ActionReference (); layerReference.putProperty (charIDToTypeID ("Prpr" ), stringIDToTypeID ("layerEffects" )); layerReference.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var descriptor = executeActionGet (layerReference); var layerEffects = descriptor.getList (stringIDToTypeID ("layerEffects" )); var frameFX = layerEffects.getObjectValue (stringIDToTypeID ("frameFX" )); var enabled = frameFX.getBoolean (stringIDToTypeID ("enabled" )); if (enabled) { var size = frameFX.getInteger (stringIDToTypeID ("size" )); var opacity = frameFX.getInteger (stringIDToTypeID ("opacity" )); var color = frameFX.getObjectValue (stringIDToTypeID ("color" )); var red = color.getInteger (stringIDToTypeID ("red" )); var green = color.getInteger (stringIDToTypeID ("grain" )); var blue = color.getInteger (stringIDToTypeID ("blue" )); return { size : size, opacity : opacity, color : {red : red, green : green, blue : blue} } } return null ; }
上面这两个方法相对来说复杂一些,因为需要获取的属性值藏的比较深,需要逐步去挖掘出来,不过思路都是一样的,难度也不太大。值得一个注意的点是颜色中的green ,有写地方写的是grain ,是一个意思,看到了不要觉得奇怪。另外还有对于形状图层而言,它本身也有一个描边,这个描边在adjustment 里头,所以为了区分图层效果的描边,所有的图层效果获取的方法都加上FX 的后缀。
3. 修改和操作图层 上面介绍了图层信息的获取,除了获取图层的信息之外,我们还需要对图层做很多操作,比如选中图层,移动图层,修改图层的位置等等,我们同样可以将这些常用的操作封装起来,补充到我们的类对象当中。相比获取信息而言,针对图层的操作,有一个优势是,很多操作都会在ScriptingListenerJS.log 有AM代码输出,大多数时候,我们只要拷贝里头的代码就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Layer .prototype .select = function ( ) { var current = new ActionReference (); current.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); var desc = new ActionDescriptor (); desc.putReference (charIDToTypeID ("null" ), current); executeAction ( charIDToTypeID ( "slct" ), desc , DialogModes .NO ); } Layer .prototype .show = function ( ) { var desc1 = new ActionDescriptor (); var list1 = new ActionList (); var ref1 = new ActionReference (); ref1.putIdentifier (charIDToTypeID ("Lyr " ), this .id );; list1.putReference (ref1); desc1.putList (charIDToTypeID ("null" ), list1); executeAction (charIDToTypeID ("Shw " ), desc1, DialogModes .NO ); } Layer .prototype .hide = function ( ) { var current = new ActionReference (); var desc242 = new ActionDescriptor (); var list10 = new ActionList (); current.putIdentifier (charIDToTypeID ("Lyr " ), this .id );; list10.putReference ( current ); desc242.putList ( charIDToTypeID ( "null" ), list10 ); executeAction ( charIDToTypeID ( "Hd " ), desc242, DialogModes .NO ); } Layer .prototype .rasterize = function ( ) { var desc7 = new ActionDescriptor (); var ref4 = new ActionReference (); ref4.putIdentifier (charIDToTypeID ("Lyr " ), this .id ); desc7.putReference ( charIDToTypeID ( "null" ), ref4 ); executeAction ( stringIDToTypeID ( "rasterizeLayer" ), desc7, DialogModes .NO ); }
其它更多的一些方法,大家根据自己的需要,从ScriptingListenerJS.log 文件的输出中拷贝代码进来就可以了,我这里不多赘述。有一个地方需要强调一下的是,我们通过id索引来修改图层信息的方式是有限制的,id的属性你可以任意给,但是有些信息的修改,只能在当前选中图层 上才能生效,如果Layer 实例对应的图层当前不是选中图层,你对它执行操作是没有任何效果的,还可能会报错。比如你想要修改图层的名称,就只能针对选中的图层才行,你不能随便提供一个图层id然后修改它的name属性,这种情况,一般我们先将id的图层设置为选中状态,然后在进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Layer .prototype .setName = function (newNameString ) { var desc26 = new ActionDescriptor (); var ref13 = new ActionReference (); ref13.putEnumerated ( charIDToTypeID ( "Lyr " ), charIDToTypeID ( "Ordn" ), charIDToTypeID ( "Trgt" )); desc26.putReference ( charIDToTypeID ( "null" ), ref13 ); var desc27 = new ActionDescriptor (); desc27.putString ( charIDToTypeID ( "Nm " ), newNameString); desc26.putObject ( charIDToTypeID ( "T " ), charIDToTypeID ( "Lyr " ), desc27 ); executeAction ( charIDToTypeID ( "setd" ), desc26, DialogModes .NO ); } var layer = new Layer (100 );layer.select (); layer.setName ("another awesome name" );
4. 图层的遍历 很多时候,我们需要对图层做遍历,然后针对目标需要的图层做操作,比如我希望删除掉所有隐藏的图层,或者我想替换掉所有的智能对象图层里头的内容等等,这种情况都需要做图层遍历。图层的结构不复杂,和系统文件夹差不多,存在一个图层组的概念,图层组可以继续包含图层组,所以常规来讲,我们要遍历所有图层的话,需要判断当前图层是否是图层组 ,然后进行递归遍历,大体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 function loopLayers (layers ) { for (var i=0 ; i<layers.length ; i++) { var layer = layers[i]; if (layer.typename == "LayerSet" ) { loopLayers (layer.layers ); } else { } } } loopLayers (app.activeDocument .layers );
这是大家最常用的方式,非常好理解,但是它的问题也非常明显,就是效率及其低下,当你的图层数量很大的时候,处理耗时很长,如果再做一些耗时的图层处理操作,整个过程就会非常长,经常会出现整个Ps卡死好久不能动弹。
于是我们需要一个更高效的图层遍历办法! 这个办法的逻辑是:图层虽然有空间的嵌套关系,但是在图层顺序上是一维的,也就是图层的index值从0 - xxx这样顺序递增的,于是只要我们拿到图层总数量N,然后做0-N的遍历就可以,复杂度立马减少到O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function loopLayers ( ) { var ref = new ActionReference (); ref.putProperty (charIDToTypeID ("Prpr" ), charIDToTypeID ('NmbL' )); ref.putEnumerated ( charIDToTypeID ('Dcmn' ), charIDToTypeID ('Ordn' ), charIDToTypeID ('Trgt' ) ); var desc = executeActionGet (ref); var layerCount = desc.getInteger (charIDToTypeID ('NmbL' )); var i = 0 ; try { activeDocument.backgroundLayer ; } catch (e) { i = 1 ; } for (i; i<layerCount; i++) { var ref = new ActionReference (); ref.putIndex ( charIDToTypeID ( 'Lyr ' ), i ); var desc = executeActionGet (ref); var id = desc.getInteger (stringIDToTypeID ( 'layerID' )); var layer = new Layer (id); } }
我实际测试了一下,一个3907个图层数量的大PSD文档,打印出图层中文字图层的内容,用递归的方法需要8分钟多,用第二种方法只需要3秒,完全不是一个数量级!强烈推荐!我们可以把这个遍历的方法做一下简单的改造,就可以放到Layer类的方法当中,给它传递一个函数回调,将每个图层传递给整个回调函数,我们就可以进行二次处理了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Layer .loopLayers = function (callback ) { var ref = new ActionReference (); ref.putProperty (charIDToTypeID ("Prpr" ), charIDToTypeID ('NmbL' )); ref.putEnumerated ( charIDToTypeID ('Dcmn' ), charIDToTypeID ('Ordn' ), charIDToTypeID ('Trgt' ) ); var desc = executeActionGet (ref); var layerCount = desc.getInteger (charIDToTypeID ('NmbL' )); var i = 0 ; try { activeDocument.backgroundLayer ; } catch (e) { i = 1 ; } for (i; i<layerCount; i++) { var ref = new ActionReference (); ref.putIndex ( charIDToTypeID ( 'Lyr ' ), i ); var desc = executeActionGet (ref); var id = desc.getInteger (stringIDToTypeID ( 'layerID' )); var layer = new Layer (id); callback && callback (layer); } } Layer .loopLayers (function (layer ) { if (layer.kind () === 3 ) { } });
总结 本篇文章介绍了图层 的常见操作方法,并通过前面文章介绍的AM知识,自己封装脚本API,这样可以不断的完善和补充官方DOM API的不足,这是很多深度插件开发者常见的做法,市面上也有很多前辈大佬自己共享出来的这些封装好的代码库,也可以去拿来用,唯一的问题是这些代码大部分都是个人自己编写维护的,质量和特性不稳定,需要自己对其结果负责。
如果你长期从事这方面的工作,仍然建议你自己摸索学习,然后不断自己补足自己需要的API来满足需要。本篇文章的代码可以在下面的github库上找到,这些代码只是图层操作的中的一小部分,不过也是很常见的一部分,剩下的,你可以根据自己的需要和前面文章介绍的知识,自己来补全它。
https://github.com/cutterman-cn/cep-panel-start.git
分支: layers
下一篇文章我们会介绍Ps的另外一个叫生成器 的模块,这个东西稍微有点边缘化,用的人不多,但是它有一些非常重要的特性是独有的,我们后续的篇章中有一些内容会用到这些特性,所以我会专门开一篇详细介绍,敬请期待。
另外,我最近在开始考虑是否要将这个系列课程转成视频的形式,如今视频课程非常流行,我还不确定那种更好,也想听听大家的建议,有想法欢迎在下面评论。