【CEP教程-11】生成器

【CEP教程-11】生成器

这篇文章,我给大家介绍一下Photoshop一个比较冷门的扩展部分,叫生成器(英文generator),这个东西不在CEP插件面板这个体系内,了解和用的人也比较少。但是呢它确实也是Ps提供给开发者进行扩展定制的一个模块,并且它提供了一些非常独有的特性,对我们插件开发也有较大的助益,所以我单独开一个章节来进行介绍。

背景介绍

生成器,是Photoshop在2015年随着CC2015这个版本一起发布的。当时,由于移动端开始兴起,移动UI设计开始成为主流,随之诞生了一个叫Sketch的设计工具,主打UI设计,相比Ps它更轻巧在UI设计领域效率更高。Adobe也感觉到了压力,于是开始尝试在Ps上融入一些新的元素来提升UI设计能力,所以在CC2015中集成了两个东西:

  1. Device Preview CC,一款可以在App预览设计稿的插件

Device Preview CC

  1. Design Space,一个专注UI的设计空间

Design Space

这个东西属于实验性质的,后来发现不太行,在下一个版本就废弃了,转向了一个更独立的产品Adobe XD。

为了输出这样的产品功能,Photoshop引入了nodejs,并提出了生成器这个概念,支持我们用nodejs来写一些扩展,这个生成器的核心实现叫Generator-builtin,同时和这个生成器一起发布的是一个叫图像资源(image assets)的扩展,它可以监听图层名称,然后从后台默默的进行图像输出,Adobe将这些东西都开源了,源码我们可以在这里找到

https://github.com/adobe-photoshop/generator-core

它能做什么

理论上,有了nodejs运行时,你能做的事情非常多,几乎nodejs提供给你的所有功能都能用,比如读取本地文件,运行本地的其它软件,做一些复杂的计算任务等等,以及能够借助node生态提供的丰富的库和组件,真的是如虎添翼。

但是,这些都不是这篇文章要讲的,我想介绍的是这个生成器给我们提供了一些其它地方没有的功能特性,能够在我们做插件开发的时候更加便利,甚至是非它不可。

  1. 特性1: 获取图像原始数据的能力
  2. 特性2: 后台异步操作的能力

我们在下面的内容会围绕这两个特性展开介绍,并通过一个实际的例子来使用这个两个特性,看它能做出来什么样的东西。

安装和入门

1. 生成器架构

generator architecture

Photoshop引入了nodejs,然后在启动的时候,通过pipe调起nodejs运行时核心generator-builtin,该核心通过KLVR机制与Ps进行通信和交互,同时会加载放在指定位置的用户编写的扩展并运行。

2. 开启生成器

生成器默认是关闭的,可以通过 首选项 -> 增效工具 -> 启用生成器 开启它。

open generator

开启之后,我们可以从 菜单栏 -> 文件 中看到一个生成的选项,二级菜单中就是被加载出来的扩展了。

generator list

3. 安装位置

和CEP插件的安装位置不同,生成器的扩展安装在如下位置

Mac

1
2
/Library/Application Support/Adobe/Plug-Ins/CC/Generator
/Applications/Adobe Photoshop 2021/Plug-ins/Generator

Win

1
2
C:\Program Files\Common Files\Adobe\Plug-Ins\CC\Generator
{Photoshop安装路径}\Plug-ins

生成器核心在启动的时候,会从上面路径进行遍历查找并加载正确的扩展。

3. 配置开发环境

对于安装在上面位置的扩展,Ps只会在启动的时候去加载他们,在Ps运行过程中如果插件修改,是不会重新加载的。这显然无法满足开发的需要,所以我们需要配置它的开发模式。

打开首选项 -> 增效工具

  1. 关闭启用生成器
  2. 打开启用远程连接
  3. 在密码那里填password,名称随便填

generator list

接着重启生成器,开发模式就配置好了

确保你本地安装了node环境,然后我们从github上分别下载生成器核心和它的demo项目,下载下来后随便放哪里都行,不用放到上面路径,那里是存放发布后的扩展的位置。

https://github.com/adobe-photoshop/generator-core

https://github.com/adobe-photoshop/generator-getting-started/

将这两个工程,存放到如下的路径结构

1
2
3
| - generator-core
| - plugins
| - - | -- generator-getting-started

generator plugins directory

进入generator-core目录,执行安装模块依赖

1
npm install

安装完成之后,执行如下命令加载demo扩展

1
2
cd path/to/generator-core
node app.js -v -f /absolute/path/to/plugins

上面命令有一个需要特别注意的点是 /absolute/path/to/plugins 这个参数需要是绝对路径,不能是相对路径 ../plugins

该命令执行完成,会在控制台输出一堆信息,同时你会在Ps菜单栏 -> 生成 里头看到Tutorial选项,表示我们的demo扩展已经加载成功了。

使用进阶

generator-getting-started工程项目,我们可以看到主要就是两个文件package.json, main.js,第一个是扩展配置文件,配置一些扩展的基本属性,main.js是扩展的入口文件。入口文件对外暴露init方法提供给生成器核心进行初始化,详细代码大家可以自行阅读main.js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
(function () {
"use strict";

// 核心调用此方法,传递两个参数
// generator 核心对象,是所有接口方法调用的主体
// config 生成器的一些配置,包含Ps安装路径,版本等信息
function init(generator, config) {

}

// 对外暴露init方法,提供给generator-builtin调用
exports.init = init;
}())

接下来的部分,我们针对生成器的几个重点特性来展开介绍,并通过结合这些特性,我们来开发一个实例项目:做一款实时预览插件面板,能够实时展示当前编辑的图。

1. 菜单入口

菜单入口,指的是生成器加载完我们的扩展之后,在菜单栏 -> 生成 里头出现我们的扩展名称。这个工作不是必须的,如果我不想要在菜单栏头出名称,可以省略此步骤,不过依然推荐大家添加一个菜单,它有两个好处:

  1. 标识我们插件的加载状态
  2. 它也可以作为一个点击的触发器完成一些功能

我们来看看如何添加菜单栏

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
(function () {
"use strict";

// 两个全局变量保存传进来的参数
var _generator = null,
_config = null;

// 定义我们的扩展id和菜单栏显示名称
var plugin_id = "generator-demo-plugin";
var menu_name = "generator-demo-plugin";

// 核心调用此方法,传递两个参数
// generator 核心对象,是所有接口方法调用的主体
// config 生成器的一些配置,包含Ps安装路径,版本等信息
function init(generator, config) {
_generator = generator;
_config = config;

// 添加菜单栏
_generator.addMenuItem(plugin_id, menu_name, true, false).then(
function () {
console.log("Menu created", plugin_id);
}, function () {
console.error("Menu creation failed", plugin_id);
}
);
// 当我们点击那个菜单栏选项的时候,触发此回调
_generator.onPhotoshopEvent("generatorMenuChanged", handleGeneratorMenuClicked);
}

function handleGeneratorMenuClicked(event) {
// 点击其它生成器名称也会触发此回调,需要在这里做过滤
var menu = event.generatorMenuChanged;
if (!menu || menu.name !== plugin_id) {
return;
}
console.log("you click the plugin menu");
}

// 对外暴露init方法,提供给generator-builtin调用
exports.init = init;
}())

保存之后,继续执行命令,就能在Ps中看到我们的扩展名称被显示出来了

1
node app.js -v -f /absolute/path/to/plugins

plugin loaded

2. 获取文档信息

如果你还记得我前面文章介绍的Action Manager的内容的话,有提到文档树的概念,包含了当前文档的各个属性等内容,它可以通过AM代码一点点的去获取,但是这种方式非常繁琐,生成器给我们提供了一个现成的API,让我们可以获取当前文档JSON结构信息,非常方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// ... 接上面的代码

// 获取文档信息,如果不传documentId,就获取当前文档
function getDocumentInfo(documentId) {
_generator.getDocumentInfo(documentId).then(
function (document) {
console.log("Received complete document:", JSON.stringify(document, null, 4));
},
function (err) {
console.error("[Tutorial] Error in getDocumentInfo:", err);
}
).done();
}

执行完成之后,会在控制台输出当前文档的一些信息和图层的json数据结构

get document info

3. 获取图像数据

前面提到了,生成器的一个很重要的特性:获取图像原始数据,常规的CEP面板,或者ExtensionScript都没有给我们提供这样的能力,只有生成器这里有

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
// ... 接上面的代码
/**
* 获取指定文档图片
* @param documentId 文档id
* @param bounds 文档尺寸数据
* @return {Promise<void>}
*/
async function getImage(documentId, bounds) {
try {
var pixmap = await _generator.getDocumentPixmap(documentId, {
clipToDocumentBounds: true,
inputRect: bounds,
outputRect: bounds,
clipBounds: bounds,
scaleX: 1,
scaleY: 1,
convertToWorkingRGBProfile: true,
maxDimension: 30000,
});
console.log(pixmap);
} catch (e) {
console.error(`get document pixmap error[${e}]`);
}
}

我们将上面getDocumentInfo拿到的文档信息,将id和bounds传进去,就可以获取到此文档的pixmap图像数据

document pixmap

pixmap是一个数据对象,里头的属性pixels就是该文档的图片原始数据,它是一个ARGB的Buffer,拿到这个图像数据之后,我们可以通过把它转成PNG图像格式,保存到本地,就相当于输出了当前Ps的图像内容了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...接上面代码

// 将图像保存到本地,这里用到了一个jimp的库,需要提前安装 npm install --save jimp
function saveImageToPNG(pixmap) {
var Jimp = require('jimp')
var offset = 0;
var image = new Jimp(pixmap.width, pixmap.height, function (err, image) {
let buffer = image.bitmap.data;
for (let i = 0; i < pixmap.pixels.length; i++) {
buffer[offset] = pixmap.pixels[offset + 1] // R
buffer[offset + 1] = pixmap.pixels[offset + 2] // G
buffer[offset + 2] = pixmap.pixels[offset + 3] // B
buffer[offset + 3] = pixmap.pixels[offset] // Alpha
offset = offset + pixmap.channelCount;
}
image.write("/Users/xiaoqiang/Desktop/output-image.png");
});
}

上面的两块代码,我们可以进行Ps的图像导出操作,这里关系到了前文提到的第二个特性: 后台异步操作,这是什么意思呢?

对于Ps来说,如果你想要导出一张图片,当前提供的能力,基本都是需要在UI界面上进行的,比如你调用DOM API的document.export方法导出图片,或者用AM的excuteAction(“export”)命令,它都是会阻塞当前Ps界面直到图片导出完成,这在一些切图插件上还是OK的,但是如果你要做预览插件这样的,就不行了,实时预览插件,需要持续不断的输出图片,放在UI进程上做,用户就没法使用PS了。但是生成提供的获取pixmap数据,是不会阻塞UI线程的,用户依然可以界面上进行作图,所有的行为都在后台默默完成。这个特性,是目前Ps仅有的在生成器里头提供的。

4. 事件

生成器还提供了事件监听功能,可以监听到Ps的一些行为,这里的事件名称和类型,是生成器独有的,和之前介绍过的什么CSXSEvent之类的,没有什么关系。

事件也是生成器一个很重要的功能,当宿主发生变化的时候,生成器的扩展可以据此做一些操作。比如我们下文要做的实时预览插件,就是需要监听Ps的imageChanged事件,当编辑的图像发生变化的时候,就获取该图片数据,然后发送给插件面板进行展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

_generator.onPhotoshopEvent("currentDocumentChanged", handleCurrentDocumentChanged);
_generator.onPhotoshopEvent("imageChanged", handleImageChanged);
_generator.onPhotoshopEvent("toolChanged", handleToolChanged);

function handleCurrentDocumentChanged(id) {
console.log("handleCurrentDocumentChanged: "+id)
}

function handleImageChanged(document) {
console.log("Image " + document.id + " was changed:");//, stringify(document));
}

function handleToolChanged(document){
console.log("Tool changed " + document.id + " was changed:");//, stringify(document));
}

5. 执行JSX脚本

生成器的扩展是纯nodejs的,它提供了一个接口让我们可以执行JSX代码,generator对象提供了两个api,一个可以执行代码片段,一个可以执行代码文件。我们在接下来的例子中,会用到执行jsx代码来和插件面板进行通信。

1
2
3
4
5
6
// 运行一个jsx文件
const file = path.join(__dirname, 'photoshop', 'jsx', 'eventDispatch.jsx');
_generator.evaluateJSXFile(file, {data: JSON.stringify(ret)});

// 运行一段jsx代码
_generator.evaluateJSXString(`alert("version: ${config.vcname}")`);

6. 和CEP面板进行通信

很多时候,我们以插件面板对外呈现作为插件的主体,生成器部分主要作为一个后台运行的部分,于是就涉及到CEP面板和生成器的扩展之间如何进行通信交互数据。

有这么两种种方案:

  1. websocket

因为需要做双向通信,可以在生成器里头起一个websocket server,CEP面板作为client,进行ws通信,这也是比较推荐的方式。生成器基于nodejs运行时,有非常多可用的现成ws库用来搭建ws server,有需要的自行搜索一下。不过这个方案整体要复杂一些,我这个简单的例子不采用这个方案。

  1. CSXSEvent

我在之前的篇章里头介绍过这个事件的概念和使用,这个事件主要用在ExtensionScript和JS面板之间的通信,上一个环节我们介绍了生成器可以执行ExtensionScript代码,所以可以通过它来派发一个事件通知插件面板。我接下来重点讲这个方案的实现。

1. 插件面板 -> 生成器

插件面板给生成器发送指令,可以通过JSX的代码来完成,注意要指定你的生成器扩展ID

1
2
3
4
5
6
7
8
9
10
11
12
// 这是在JSX层执行的代码
// 发送消息给生成器,可以传递参数
function sendToGenerator (param) {
try {
var generatorDesc = new ActionDescriptor();
// 这里要指定生成的那个扩展ID
generatorDesc.putString (stringIDToTypeID("name"), "generator-demo-plugin");
generatorDesc.putString (stringIDToTypeID("sampleAttribute"), param);
executeAction (stringIDToTypeID("generateAssets"), generatorDesc, DialogModes.NO);
} catch (e) {
}
}

在生成器扩展侧,通过和上面菜单点击一样的方式来获取到插件面板发送过来的消息和参数

1
2
3
4
5
6
7
8
9
10
11
_generator.onPhotoshopEvent('generatorMenuChanged', (evt: any) => {
let menu = evt.generatorMenuChanged;
if (!menu || menu.name !== plugin_id) {
return;
}
// 这个就是插件面板传递过来的参数
const attr = evt.generatorMenuChanged.sampleAttribute;
if (attr) {
console.log(`receive message from panel[${attr}]`);
}
});

2. 生成器 -> 插件面板

反过来如果想要生成器发送消息给插件面板,需要在生成器侧执行一段JSX脚本,派发一个CSXS事件,然后在插件面板侧监听该事件。在生成器工程里头新建一个jsx文件,用来派发事件

event-dispatch.jsx

1
2
3
4
var eventObj = new CSXSEvent();
eventObj.type = "com.generator.demo.plugin";
eventObj.data = params.data; // data是传递进来自定义参数
eventObj.dispatch();

在nodejs脚本里头执行这个jsx脚本文件

1
2
const file = path.join(__dirname, 'event-dispatch.jsx');
_generator.evaluateJSXFile(file, {data: JSON.stringify({"from": "generator", "success": 1})});

然后在CEP面板的JS侧,进行这个事件监听

1
2
3
4
5
6
7
8
const csInterface = new CSInterface();
csInterface.addEventListener('com.generator.demo.plugin', handleGeneratorEvent);

// 监听从生成器发送过来的消息回调
function handleGeneratorEvent(evt) {
const ret = evt.data;
console.log(`from[${ret.from}] value[${ret.value}]`);
}

这样,我们就实现了生成器和插件面板之间的双向通信了

实时预览插件设计

以上的内容,就是和生成器有关的一些比较重要的特性介绍,除此之外生成器的核心还提供了许多API和功能,我就不一一罗列了,大家可以自己去看核心代码的接口注释。上面介绍的这些内容都是我认为比较重要的特性,以及它是构成我们下面这个案例必不可少的点。

要实现一个实时预览插件,大体是如下几个环节

  1. 监听当前图像的变化(用事件来监听)
  2. 获取图像原始数据(用getPixmap方法)
  3. 将图像数据发送给插件面板进行呈现(上面介绍的通信机制)

考虑到图像数据一般比较大,将二进制数据直接通信比较消耗内存,我们可以在生成器这边将图像数据保存到本地图片中,然后将图片地址发送给插件面板,插件面板读取图片进行展示就可以了

由于生成器是随着PS启动就开始运作了,我们可以在插件面板中放一个按钮,点击后,发送一个消息给生成器,表示我要开始预览了,生成器再开始工作。

整个插件完成的样子大概是这样

live preview plugin

完整代码比较多,不在文章里头贴了,案例的代码可以在这里找到

  1. 生成器代码

https://gitee.com/cutterman-cn/generator-getting-started

  1. 插件面板代码

https://gitee.com/cutterman-cn/cep-panel-start/tree/generator/

总结

这篇文章我们介绍了生成器整个扩展模块的使用,并用此开发了一个实例插件,生成器还提供了许多其它功能,推荐大家去实际研究一下。整个案例插件也非常基础,只是跑通流程而已,真正可产品化的实时预览插件要复杂的多的多,我自己开发并商用的实时预览插件在这里,有兴趣可以下载试用。

https://www.psmirror.cn

下一篇文章,我们继续CEP插件的开发内容,会重点介绍图像导出的一些方案,敬请期待~~

评论