【CEP教程-10】图层处理那些事

【CEP教程-10】图层处理那些事

前面三篇文章,我系统的介绍了Action Manager的基本逻辑和使用方法。希望通过这三部曲能够让大家对Ps脚本编程中的黑魔法有一个更深的认识,并且可以自己上手写一些AM代码,基本上目前绝大多数的场景,我都不需要去网上找别人的代码来抄,而是可以自己去挖掘最终自己写出来想要的功能逻辑。

今天这篇教程,我们会顺着AM继续实战,介绍Ps中最重要的一块 – 图层的常见操作,在课程结尾我们会沉淀出一份JS库文件,封装好了过程中写好的代码,便于日后使用。让我们现在开始吧~~

图层的基本操作

图层模块,可以说是Ps最重要的一个部分,绝大多数的插件、功能、脚本都躲不开图层的操作,比如要获取选中的图层的信息,遍历、移动、删除修改图层等。 所以,如果我们能够把图层的常见操作做一个封装,以后用到这些功能的时候,就会非常方便了。

Layer Component

我们可以创建一个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
/**
* 图层类的构造函数,传入一个图层ID
* @param id
* @constructor
*/
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
/**
* 图层类的构造函数,传入一个图层ID
* @param id
* @constructor
*/
function Layer(id) {
this.id = id;
}

// ------- 类方法 -------
/**
* 获取当前选中的图层
* @return {Layer[]}
*/
Layer.getSelectedLayers = function () {
// 创建一个ActionReference
var selectedLayersReference = new ActionReference();
// 给这个AR设置我们需要从AD中获取的属性值
selectedLayersReference.putProperty(charIDToTypeID("Prpr"), stringIDToTypeID("targetLayersIDs"));
// 目标对象是当前选中的文档
selectedLayersReference.putEnumerated(charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
var desc = executeActionGet(selectedLayersReference); // 拿到一个ActionDescriptor
var layers = [];
if (desc.hasKey(stringIDToTypeID("targetLayersIDs"))) {
// 拿到的是一个ID列表,我们遍历它把每个id拿出来,然后实例化Layer对象
var list = desc.getList(targetLayersTypeId);
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中篇里头那张图么?

property type

有了图层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
/**
* 图层类的构造函数,传入一个图层ID
* @param id
* @constructor
*/
function Layer(id) {
this.id = id;
}

// ------- 实例方法 -------
/**
* 获取当前实例图层的名称
* @return {string}
*/
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 "));
}

/**
* 获取当前实例图层的层级
* @return {number}
*/
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"));
}

/**
* 获取当前实例图层的类型
* @return {number}
*/
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"));
}

/**
* 获取当前实例图层的尺寸
* @return {{x: number, width: number, y: number, height: number}}
*/
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)};
}

/**
* 判断当前图层的显示/隐藏
* @return {boolean}
*/
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类当中。上面这些属性都是比较简单的,下面介绍获取稍微复杂一点的,如下图

Vector 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
/**
* 获取形状图层的填充颜色
* @return {null|*[]}
*/
Layer.prototype.solidFill = function () {
var kind = this.kind();
if (kind === 4) { // 只有形状图层才能获取到图层填充属性
var layerReference = new ActionReference();
// 形状图层的填充和其它属性在adjuestment下面
layerReference.putProperty(charIDToTypeID("Prpr"), stringIDToTypeID("adjustment"));
layerReference.putIdentifier(charIDToTypeID("Lyr "), this.id);
var descriptor = executeActionGet(layerReference);
var adjustment = descriptor.getList(stringIDToTypeID("adjustment")); // adjustment是一个ActionList
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;
}


/**
* 获取图层描边效果
* @return {{size: *, color: {red: *, green: *, blue: *}, opacity: *}|null}
*/
Layer.prototype.strokeFx = function () {
var layerReference = new ActionReference();
// 所有的图层效果,都在layerEffects下面
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
/**
* 修改图层名称
* @param newNameString
*/
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();
// 当前文档的图层数量属性key
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;
}
// 开始逐级遍历图层index,根据index来获取到图层实例
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
/**
* 高效遍历图层
* @param callback
*/
Layer.loopLayers = function (callback) {
var ref = new ActionReference();
// 当前文档的图层数量属性key
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;
}
// 开始逐级遍历图层index,根据index来获取到图层实例
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) { // 3是文字图层
// 打印文字图层的内容
// ...
}

});

总结

本篇文章介绍了图层的常见操作方法,并通过前面文章介绍的AM知识,自己封装脚本API,这样可以不断的完善和补充官方DOM API的不足,这是很多深度插件开发者常见的做法,市面上也有很多前辈大佬自己共享出来的这些封装好的代码库,也可以去拿来用,唯一的问题是这些代码大部分都是个人自己编写维护的,质量和特性不稳定,需要自己对其结果负责。

如果你长期从事这方面的工作,仍然建议你自己摸索学习,然后不断自己补足自己需要的API来满足需要。本篇文章的代码可以在下面的github库上找到,这些代码只是图层操作的中的一小部分,不过也是很常见的一部分,剩下的,你可以根据自己的需要和前面文章介绍的知识,自己来补全它。

https://github.com/cutterman-cn/cep-panel-start.git

分支: layers

下一篇文章我们会介绍Ps的另外一个叫生成器的模块,这个东西稍微有点边缘化,用的人不多,但是它有一些非常重要的特性是独有的,我们后续的篇章中有一些内容会用到这些特性,所以我会专门开一篇详细介绍,敬请期待。

另外,我最近在开始考虑是否要将这个系列课程转成视频的形式,如今视频课程非常流行,我还不确定那种更好,也想听听大家的建议,有想法欢迎在下面评论。

评论