【CEP教程-12】如何从Ps中导出图片

【CEP教程-12】如何从Ps中导出图片

在做Ps插件开发的时候,一个非常常见的功能就是导出图片,有时候我们需要导出整个PSD文档,有时候我们需要导出某个图层或某个几个图层,市面上所有的和UI切图有关的产品和插件,都是运用了插件的图像导出能力来完成这个诉求。本篇文章我们就主要来介绍如何从Ps中获取图像,或许各种格式的图像。

1. 输出文档图片内容

对于导出一整个文档来说,其实是比较简单的,我们查看DOM的API文档就能找到对应的方法

export document

这个方法,就是Ps默认的导出到web格式的实现,导出的格式,其中重点关注最后一个参数ExportOptionsSaveForWeb,通过这个参数的配置,可以输出不同格式、质量的图片,这里给一些例子

1
2
3
4
5
6
7
8
// 输出PNG格式图片
var pngOption = new ExportOptionsSaveForWeb();
pngOption.format = SaveDocumentType.PNG;
pngOption.PNG8 = false; // true就是输出png8格式,false对应输出png24
pngOption.quality = 100;

var file = new File("path/to/filename");
app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, pngOption);

同样的,如果要输出其它格式的图片,设置对应的format就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
// 输出JPEG图片格式
var jpegOption = new ExportOptionsSaveForWeb();
jpegOption.format = SaveDocumentType.JPEG;
jpegOption.optimized = true;
jpegOption.quality = 100;

// 输出GIF
var gifOption = new ExportOptionsSaveForWeb();
gifOption.colors = this.config.gifValue;
gifOption.PNG8 = true;
gifOption.colorReduction = ColorReductionType.SELECTIVE; // 可选择
gifOption.quality = 0; // 这里设置0是为了解决lossy设置不生效的bug
gifOption.dither = Dither.NONE; // 无仿色

上面这3种图片格式是比较常见的,PS自身提供的fomat类型非常多,但是一般我们也用不上,这里就不多做介绍了,基本上上面3中类型大体够用了。

2. 输出图层内容

上面的代码,可以将当前打开的整个PSD文档进行图片输出,但是很多时候,我们不要输出整个文档,我们只想导出某个图层的内容,那这个时候该怎么办呢?我们找遍DOM API也找不到可以单独输出图层的方法,Action Manager也没有现成的办法。这种时候,我们可以通过手动操作PS来完成这个事情,然后将这些动作用不同的脚本片段串起来,就可以达到目的了。

整体操作流程如下: 选中需要导出的图层 -> 右键,选择复制图层 -> 在弹出的框里头选择新建文档 -> 该图层会被复制出一个新文档来 -> 菜单栏,图片,裁切,将边缘透明元素裁剪掉,就得到了该图层一个完整的新文档了,这样,我们就可以通过上面的方法将此文档进行导出,流程操作示意图如下:

manual export layer

于是,我们将这几个操作步骤,通过脚本来进行封装,然后串联起来,就实现了导出某个图层的脚本了

由于这些操作都是对Ps进行设置,所以你可以在Ps输出的日志文件(ScriptingListenerJS.log)里头找到对应的Action Manager代码,直接抄作业就可以了

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
// 步骤一:复制选中的图层到一个新的文档
// 确保当前有选中的图层
function duplicateLayer() {
var desc1 = new ActionDescriptor();
var ref1 = new ActionReference();
ref1.putClass(stringIDToTypeID("document"));
desc1.putReference(stringIDToTypeID("null"), ref1);
var ref2 = new ActionReference();
ref2.putEnumerated(stringIDToTypeID("layer"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
desc1.putReference(stringIDToTypeID("using"), ref2);
desc1.putInteger(stringIDToTypeID("version"), 5);
executeAction(stringIDToTypeID("make"), desc1, DialogModes.NO);
}

// 步骤二: 对新复制出来的文档,裁切掉透明的边缘部分
// 确保选中了这个新复制出来的图层
function trimDocument() {
var desc1 = new ActionDescriptor();
desc1.putEnumerated(stringIDToTypeID("trimBasedOn"), stringIDToTypeID("trimBasedOn"), stringIDToTypeID("transparency"));
desc1.putBoolean(stringIDToTypeID("top"), true);
desc1.putBoolean(stringIDToTypeID("bottom"), true);
desc1.putBoolean(stringIDToTypeID("left"), true);
desc1.putBoolean(stringIDToTypeID("right"), true);
executeAction(stringIDToTypeID("trim"), desc1, DialogModes.NO);
}

// 步骤三: 将此新复制出来的文档进行输出
// 支持输出对应的格式
function exportDocument(format, path) {
var options = new ExportOptionsSaveForWeb();
if (format == SaveDocumentType.PNG) {
options.format = SaveDocumentType.PNG;
options.PNG8 = false;
options.quality = 100;
} else if (format == SaveDocumentType.JPEG) {
options.format = SaveDocumentType.JPEG;
options.optimized = true;
options.quality = this.config.jpegValue;
} else if (format == SaveDocumentType.COMPUSERVEGIF) {
options.format = SaveDocumentType.COMPUSERVEGIF;
options.colors = this.config.gifValue;
options.PNG8 = true;
options.colorReduction = ColorReductionType.SELECTIVE; // 可选择
options.quality = 0; // 这里设置0是为了解决lossy设置不生效的bug
options.dither = Dither.NONE; // 无仿色
}

var file = new File(path);
app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, options);
}

有了这些分步骤的脚本函数之后,我们就可以将他们串起来,挨个执行一遍,就可以输出想要的图层了,就好比Ps自带的那个动作一样一样滴。

3. 遍历输出多个图层

上面的代码组合,可以帮助我们输出单个图层内容,当我们需要输出多个图层,或者遍历图层输出的时候,需要一些特殊的处理,比如每次新复制出来的图层文档,在导出之后得把它关闭了,重新回到目标文档,以及需要记录目标图层的索引,进行循环遍历。下面以循环输出选中的图层为例子来展示一下,当用户选中多个图层的时候,遍历输出选中的每一个图层。

我在【CEP教程-10】图层处理那些事那篇教程中写了一个Layer类,里面有获取用户选中图层的方法,这里就直接引用了,没有印象的小伙伴自行复习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自行引入layer.jsx文件

// 获取当前选中的图层
var layers = Layer.getSelectedLayers();
if (layers.length === 0) {
alert("You have not selected layer yet!");
return
}
// 遍历每个图层进行导出
for (var i=0; i<layers.length; i++) {
var layer = layers[i];
layers.select(); // 设置图层为选中状态
// 这里保存一下当前目标文档,因为接下来会打开新的文档,防止引用错乱
var targetDocument = app.activeDocument;
duplicateLayer();
trimDocument();
exportDocument(SaveDocumentType.PNG, "/xx/yy/file.png");
// 导出完毕后关闭这个新文档,不用保存
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
// 接着重新回到目标文档
app.activeDocument = targetDocument;
}

上面这种输出多个图层的方式,是目前市面上绝大多数插件都采用的图层输出方式,稳定可靠也比较方便,对于复制出来的图层文档,你还可以在导出之前做一些别的操作,比如缩放,调整图像效果等等,扩展性很好。我入行的第一款插件切图工具就是基于这个原理实现的。当然,要产品化一个切图工具,里头还有很多细节问题需要处理。

4. 更快的遍历输出

上面的多图层遍历方式,是非常常见和通用的一种办法,但是有一个小缺点:效率不高。

因为每次复制图层到一个新文档的开销是比较大的,当你需要输出很多图层的时候,耗时就会比较长,于是是否有更快一点的办法呢?答案也是有的,但是这里我不打算贴代码了,我介绍一些思路,感兴趣的小伙伴可以按照这个思路自己写一下代码,如果你全程认真看完我的教程的话,相信是可以自己写出来这些代码的。

考虑到每个图层都要复制出一个新文档效率比较低,我们可以这样:

  1. 将当前文档复制一份出来,后续的操作都在新复制出来的文档中进行,不影响原稿
  2. 选中复制出来的文档,隐藏掉所有的图层
  3. 选中并显示第一个要导出的图层
  4. 裁切文档的透明元素
  5. 导出当前文档
  6. 回退历史步骤到裁切文档那一步
  7. 隐藏当前图层
  8. 选中并显示下一个要导出的图层,回到步骤4

这个方法,由于只复制一次新文档,后续的步骤都在此文档上进行,就会省去很多开销,速度会快一些,流程里头多了一步历史步骤的回退操作,这个在DOM API里头有对应的方法,相信大家也都能学会。

5. 输出SVG/PDF格式图片

上面我们提到的输出图片格式包含了PNG/JPEG/GIF等常见图片格式,有时候,我们会需要用到SVG/PDF这样的图片格式,他们的输出方式和前面有一些不太一样,单独辟出来介绍。

SVG是一种矢量图形格式,所以要输出它,对原始图形有要求,并不是所有内容都可以成功输出SVG的,你的图层最好是形状类型,这样才能正常输出SVG,PS没有提供相关的API和方法让我们完成这个功能,但是PS其实自带了这个特性,选中图层,鼠标右键,可以看到一个拷贝SVG的选项

copy svg

这就是PS提供给我们的功能,并且,它其实是官方自己写好的JSX脚本文件,本质上点这个选项,就是执行该JSX文件,这个文件我们可以在如下地址找到

{PS安装路径}/Required/CopySVGToClipboard.jsx

这个脚本给我们提供了一个生成当前图层SVG文本的能力,我们可以直接借用此能力来进行SVG输出。我们不需要把这个文件的代码抄过来(代码非常多),我们可以直接引用这个文件就可以了,我在前面的文章中介绍了,JSX运行时给我们提供了一个**$.eval**函数,可以引用执行独立的JSX脚本文件

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
// 找到photoshop的安装路径
function getAppPath() {
var kexecutablePathStr = stringIDToTypeID("executablePath");
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putProperty(charIDToTypeID('Prpr'), kexecutablePathStr);
ref.putEnumerated(charIDToTypeID('capp'), charIDToTypeID('Ordn'), charIDToTypeID('Trgt'));
desc.putReference(charIDToTypeID('null'), ref);
var result = executeAction(charIDToTypeID('getd'), desc, DialogModes.NO);
return File.decode(result.getPath(kexecutablePathStr));
}

// 导出当前图层到SVG
function exportSVG(filename) {
// mac和win的路径不一样
var appFolder = { Windows: "/", Macintosh: "/../" };
// 找到jsx脚本的位置
var svgFile = new File(getAppPath() + appFolder[File.fs] + "Required/CopySVGToClipboard.jsx");
// 引用并执行这个脚本
$.evalFile(svgFile.absoluteURI);
// 这个params是全局参数,给svgFile使用的,表示我们要对哪个图层进行SVG生成
var params = { layerId: app.activeDocument.activeLayer.id, layerScale: 1, documentId: app.activeDocument.id };
// 生成SVG文彬内容
var svgText = svg.createSVGText();

// 将svg文本内容写入本地文件
var file = new File(filename);
file.open("w");
file.encoding = "UTF-8";
file.lineFeed = "Unix";
file.write(svgText);
file.close();
}

导出PDF则要更简单一些,PS默认的另存为那个框,就有PDF选项,我们自己实际操作一下,然后从日志输出里头抄代码就可以了,但是它是针对文档的,所以如果你是图层,也记得单独弄成文档的形态再进行操作。

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 exportPDF(path) {
var idsave = charIDToTypeID( "save" );
var desc322 = new ActionDescriptor();
var idAs = charIDToTypeID( "As " );
var desc323 = new ActionDescriptor();
//desc323.putString( stringIDToTypeID( "pdfPresetFilename" ), "High Quality Print" );
let idpdfCompatibilityLevel = stringIDToTypeID( "pdfCompatibilityLevel" );
desc323.putEnumerated( idpdfCompatibilityLevel, idpdfCompatibilityLevel, stringIDToTypeID( "pdf15" ));
desc323.putBoolean( stringIDToTypeID( "pdfPreserveEditing" ), false );
//desc323.putBoolean( stringIDToTypeID( "pdfEmbedThumbnails" ), true );
desc323.putInteger( stringIDToTypeID( "pdfCompressionType" ), 7 );
desc323.putBoolean( stringIDToTypeID( "pdfIncludeProfile" ), false );
desc322.putObject( idAs, charIDToTypeID( "PhtP" ), desc323 );
// @ts-ignore
desc322.putPath( charIDToTypeID( "In " ), new File( path ) );
desc322.putInteger( charIDToTypeID( "DocI" ), activeDocument.id );
desc322.putBoolean( charIDToTypeID( "Cpy " ), true );
desc322.putBoolean( charIDToTypeID( "Lyrs" ), false );
var idsaveStage = stringIDToTypeID( "saveStage" );
var idsaveStageType = stringIDToTypeID( "saveStageType" );
var idsaveSucceeded = stringIDToTypeID( "saveSucceeded" );
desc322.putEnumerated( idsaveStage, idsaveStageType, idsaveSucceeded );
executeAction( idsave, desc322, DialogModes.NO );
}

6. 后台导出图片

前文介绍的这些图片输出的方法,本质上,都是在模拟用户的真实操作,就好比录制了一系列动作一般,所以,这些方法在输出图片的时候,都会阻塞UI进程,用户能够看到当前图片的各种操作行为,新建一个文档,关闭一个文档等等。这就带来一个问题,当我们需要输出很多图片的时候,用户的Ps就堵在那里了,必须等所有的任务都执行结束之后才能进行操作,Ps这个期间就跟卡死一样,体验总体来说不太好。

那有没有能够在后台默默导出图片的方法呢?

答案是有的,就是生成器,生成器相关的知识,我在上一篇文章已经详细的介绍了,忘了的小朋友可以翻回去复习一下,用生成器来输出图片,可以在子线程中进行,不会影响用户的操作,可以做到无感,在某些特定场景下是非常有价值的。

7. 其它图片输出方式

除了前文提到的document.export方法之外,其实还有一些其它的API可以进行文件保存操作

A) Action Manager命令

AM提供了一个export命令,也可以进行图片导出,并且支持很多参数的配置,总体对标的是Ps原生的这个功能

export command

该命令的具体参数,大家可以在ScriptListenerJS.log里头找到,我就不再贴代码了

B) saveAs方法

DOM API里头还提供了一个saveAs方法,也可以进行PNG/GIF/JPEG格式的输出

1
2
3
4
var saveFile = new File("path/to/filename.png");
var pngSaveOptions = new PNGSaveOptions();
pngSaveOptions.interlaced = exportInfo.pngInterlaced;
app.activeDocument.saveAs(saveFile, pngSaveOptions, true, Extension.LOWERCASE);

上面这些都是JSX提供的内容,除此之外,也可以通过C++的插件方式进行图片导出,这块我目前还没有研究,后续调研之后再写文章给大家分享

总结

本篇文章介绍了在Ps中输出图片的各种办法,图片导出是一个很常见的功能,希望对大家有帮助。

下一篇文章,我们来介绍Nodejs在CEP插件开发中的应用,敬请期待~~

评论