零基础的 PhotoShop CEP 6 开发教程 「 8 」A
《零基础的 PhotoShop CEP 6 开发教程》系列目录
「 0 」目录
「 1 」配置开发环境
「 2 」CEP 文件结构
「 3 」CEP 的运行机制
「 4 」Hello World !
「 5 」事件(EVENTS)
「 6 」调用 JSX 并传递信息
「 7 」UI - HTML 开发的一些细节
「 8 」API - 文件读写与二进制数据
「 9 」签名打包与 ZXPSignCmd
「 X 」CEP 更新到 6.1版了
这篇文章介绍的是 CEP 的文件的读写与二进制数据处理的相关内容。
首先要说明的是由于 CEP 的 JavaScript 运行在 Node.js 引擎上,所以能够使用 Node.js 的 fs
模块(有关fs
可以参考 fs 模块 - 《JavaScript 标准参考教程(alpha)》。)就能进行文件的读写了,如下面的例子,
var fs = require('fs'); //引入 fs 模型
fs.readFile("D:/autorun.inf",function(err,data){ console.log(data)}) //读取文件并交给回调函数
Node.js 的文件读写操作完全能替代 CEP 的提供的 API,但还是推荐使用 CEP 的 API ,毕竟是官方标准,各宿主之间的兼容性更有保障。
此外后面还花了大篇幅讲了 JavaScript 的二进制处理相关知识供参考。
Adobe 为 CEP 提供了自己的 API 来方便进行读写操作,实际上和使用 Node.js 的 fs
没有什么区别,不过 CEP 的接口的方法都是同步的,更容易理解。
CEP 文件操作的方法放在 window.cep.fs
对象中。
文件路径
这里要介绍 CEP 中的文件路径。
CEP 中的路径是 Unix 风格的 /
分隔,而不是 Windows 风格的 \\
,但是由于 CEP 扩展可以同时在 Windows 和 OS X 中运行,所以就有了跨平台问题,这个后面会说如何解决跨平台问题。
-
/
代表根目录, CEP 中为 PhotoShop 安装目录的根目录,比如D:/
-
./
、.
代表当前目录,同时如果没有前缀也表示当前目录,比如cc
等同于./cc
, CEP 中是 PS 安装目录 ,如D:/PS/Adobe Photoshop CC 2015
-
../
、..
代表上级目录,可以连续使用:../../
跨平台路径处理
由于 OSX 和 Windows 上路径格式的不同,所以我们常常得对其处理,虽然 Node.js 和 CEP 提供的文件相关接口都是统一使用 Unix 风格的路径格式,但难免要处理来自本地的路径,要想简单的处理路径格式问题,可以使用 Node.js 自带的 Path 模块提供的功能。
var path = require('path'); //使用前先引入 path 模块
path.join("foo", "bar");
- 拼接路径
使用path.join();
来替代手动用+
拼接路径,在不同系统上自动使用相应分隔符(要注意的是 CEP 提供的接口无论在 Windows 还是 OS X 上都是用/
):
path.join("AAA", "fff"); // 取代 "AAA"+"/" +"fff"
//在 OS X 上:AAA/fff
//在 Windows 上: AAA\\fff
- 路径标准化
如果你觉得使用+
拼接路径根据方便也无妨,path 还有一个路径标准化功能,在最终使用路径前用path.normalize()
处理路径就好了,路径标准化还有去除路径中无效字符的功能(比如sd///ds
变成sd/ds
)。
path.normalize("Files/Adobe/CEP/");
//在 OS X 上:Files/Adobe/CEP/
//在 Windows 上: Files\Adobe\CEP\"
- 获得相对路径的绝对路径
使用path.resolve();
可以得到如./
这样相对路径的绝对路径
path.resolve("./");
// "D:\PS\Adobe Photoshop CC 2015"
常用路径:
*这里的 cs
指 CSInterface 对象,即:var cs = new CSInterface();
。
值 | 路径 | 例子 |
---|---|---|
process.execPath |
Node.js 引擎可执行文件。 | D:\PS\Adobe Photoshop CC 2015\Required\CEP\CEPHtmlEngine\CEPHtmlEngine.exe |
__dirname |
扩展所在目录 | C:/Program Files (x86)/Common Files/Adobe/CEP/extensions/fonTags |
__filename |
当前文件目录 | C:/Program Files (x86)/Common Files/Adobe/CEP/extensions/fonTags/index.html |
cs.getSystemPath(SystemPath.USER_DATA) |
系统用户数据文件夹 | C:/Users/语冰/AppData/Roaming |
cs.getSystemPath(SystemPath.COMMON_FILES) |
系统公共库文件夹 | C:/Program Files/Common Files |
cs.getSystemPath(SystemPath.MY_DOCUMENTS) |
“我的文档” | C:/Users/不知语冰/Documents |
cs.getSystemPath(SystemPath.HOST_APPLICATION) |
宿主应用程序可执行文件 | D:/PS/Adobe Photoshop CC 2015/Photoshop.exe |
读取文件
window.cep.fs.readFile(路径, 方式编码)
,提供文件路径,和文本编码(默认按 UTF-8 处理),返回一个对象,其中
-
.err
存放返回错误信息 -
.data
以字符串形式存放读取返回的数据,
var path = "D:/1.txt";
var result = window.cep.fs.readFile(path);
if (0 == result.err)// err 为 0 读取成功
{
console.log(result.data);
}
else
{
console.log("读取错误:" + result.err);// 失败
}
Base64 模式读取
Base64 是一种把二进制数据以文本(64个可打印字符)形式表示的编码方法。由于 CEP 的 readFile()
只能读取文件后返回的 .data
只能是字符串,所以如果你要读取一个非文本的二进制文件,比如一张图片,就需要使用 Base64 模式读取,这样返回的.data
中会是你读取文件数据的 Base64 编码后的字符串,否则读取的数据会被强制装换为字符串会丢失内容。
像 readFile()
第二个参数传入 "Base64"
即可以 Base64 模式读取。
var path = "D:/1.txt";
var result = window.cep.fs.readFile(path ,"Base64");
result.data //Base64 字符串: "77+977+977+977+977+977+977+977+977+977+977+977+977…v73v↵v73vv73vv73vv73vv73vv73vv73vv73vv73vv73vv70="
对于读取到的 Base64 的字符串,可以 CEP 提供了 cep.encoding.convertion
里的方法来进行转换,
-
.b64_to_ascii(base64str)
Base64 字符串 以 ascii 编码转换为字符串 -
.b64_to_utf8(base64str)
Base64 字符串 以 UTF-8 编码转换为字符串 -
.b64_to_binary(base64str)
Base64 字符串以二进制编码转换为字符串
另外还有反过来的方法:.ascii_to_b64(ascii)
、.utf8_to_b64(str)
、.binary_to_b64(binary)
。
要注意的是 .b64_to_binary()
和 .binary_to_b64()
实际上是就是 window.atob() 和 window.btoa(),而这 2 个方法返回的是字符串,而且是不支持 Unicode 字符的(比如中文),所以不要因为名字是 binary 就以为它能处理二进制数据,它们是用来处理含不可传输的 ASCII 控制字符的。
要处理其非文本的二进制数据的 Base64 ,可以使用 Node.js 的 Buffer(关于 Buffer 后面有说):
var result = window.cep.fs.readFile( "D:/1.txt","Base64");
var buf = new Buffer (result.data, 'base64');
写入文件
与 CEP 的 readFile()
一样,写入文件接受的数据也是字符串:
var data = "文本内容";
var result = window.cep.fs.writeFile( "D:/1.TXT", data);
if (0 == result.err) {
// 成功·
}
else {
// 失败
}
要想对文件写入二进制数据,必须得使用 Base64 模式,并给 writeFile()
Base64 字符串 下面这个例子展示的是读取一个二进制文件,并原样写出的过程:
var inf = window.cep.fs.readFile ("D:/A.ico", "Base64"); //以 Base64 模式读入文件
window.cep.fs.writeFile ("D:/B.ico", inf.data, "Base64");//以 Base64 模式写出文件
新建目录
window.cep.fs.makedir (path)
在指定位置新建文件夹
var result = window.cep.fs.makedir(__dirname+"/"+"EEE");
if (0 == result.err) {
console.log("成功")
}
else {
console.log("错误:" + result.err)
}
删除文件
window.cep.fs.deleteFile(path);
用法同上,注意只能删除文件,不能删除文件夹
重命名文件
rename(oldPath, newPath)
可以重命名文件和文件夹。
var result = window.cep.fs.rename(__dirname+"/EEE" ,__dirname+"/EEE2" );
if (0 == result.err)
{
console.log("重命名成功");
}
else
{
console.log("错误:" + result.err);
}
读取目录中文件列表
window.cep.fs.readdir(path);
可以读取一个文件夹中存放的文件和文件夹(只是一级)列表。
读取的列表以文件名数组的形式存放在返回值的.data
里
var result = window.cep.fs.readdir(__dirname );
if (0 == result.err)
{
console.log( result.data);
// [".idea", "css", "CSXS", "EEE", "font", "img", "js", "jsx", "tem", ".debug", "1.TXT", "index.html"]
}
else
{
console.log("错误:" + result.err);
}
判断路径是文件还是路径
window.cep.fs.stat(path).data.isDirectory()
和 window.cep.fs.stat(path).data.isFile()
可以检测一个路径是文件还是文件夹。
var result = window.cep.fs.stat(__dirname + "/" + "EEE");
if (0 == result.err)
{
if (result.data.isDirectory() == true)
{
console.log("这是个文件夹");
}
else if (result.data.isFile() == true)
{
console.log("这是个文件");
}
}
else
{
console.log("错误:" + result.err)
}
获取文件最后一次被修改的时间
读取 window.cep.fs.stat(path).data.mtime
可以获取文件文件最后一次被修改的时间
设置文件权限
window.cep.fs.chmod (path, mode)
设置指定文件的权限,权限是用 Unix 风格的 chmod 数字表示权限。
在 Windows 上,只能设 0 为文件加上只读属性,777 取消只读属性。
打开、保存文件对话框
window.cep.fs.showOpenDialog (allowMultipleSelection, chooseDirectory, title, initialPath, fileTypes)
window.cep.fs.ShowOpenDialogEx (allowMultipleSelection, chooseDirectory, title, initialPath, fileTypes, friendlyFilePrefix, prompt)
window.cep.fs.showSaveDialogEx (title, initialPath, fileTypes, defaultName, friendlyFilePrefix, prompt, nameFieldLabel)
这个方法能弹出一个系统的文件选择对话框,用来选择或者保存文件,得到的文件以数组的形式存放在返回值的 .data
里。
参数的作用为:
showOpenDialog (允许多选, 选择目录模式, 标题, 初始路径, 指定文件类型数组)
ShowOpenDialogEx (允许多选, 选择目录模式, 标题, 初始路径, 指定文件类型数组, 文件类型说明, prompt消息)
showSaveDialogEx (标题, 初始, 文件类型, 默认名称, 文件类型说明, prompt信息, 名称字段标签)
其中 prompt信息
, 名称字段标签
是 OSX 中才起作用的。
选择目录
弹出选择目录对话框,这时只有标题
、初始路径
参数起作用。
var result = window.cep.fs.showOpenDialog (true, true, "标题", "D:/", "")
result.data
//["E:/Qt_Project"]
选择目录模式
打开文件
var result = window.cep.fs.showOpenDialogEx (false, false, "标题", "D:/", ["txt","ico"] , "文件类型说明");
result;
打开文件对话框
保存文件
result = window.cep.fs.showSaveDialogEx ("标题", "D:/", ["txt"], "默认名称.TXT", "文件类型说明");
if (0 == result.err)
{
if(result.data.length==0)
{
console.log("用户放弃了保存");
}
else
{
console.log(result.data);
}
}
else
{
console.log("错误:" + result.err)
}
保存文件对话框
JavaScript 的二进制处理
JavaScript 当年起草的时候并没有想到自己以后应用范围会那么广,所以一开始并没有处理文件\二进制数据的功能,对文件\二进制数据的相关功能是后来一点点补增的,这让 JavaScript 文件\二进制数据处理的知识相较于其他语言要更杂乱,更麻烦。
Blob 和 File 对象
Blob 对象是 ECMAScript 5 标准才引入的新内容,Blod 是一个存放二进制数据的容器,然而 Blob 对象成员属性只有 2 个 :size
表示数据长度,type
表示数据的类型(mime type),并不能读写已经放入 Blod 中的数据,他大多数情况下的作用只是用来生成一个可以下载的文件(还有剪贴板、拖拽操作可能会用到):
var myBlob = new Blob(["这是文本"], { "type" : "text\/xml" }); //把数据装入 Blob
var href = window.URL.createObjectURL(blob); //生成下载链接
File 对像继承了 Blob 对象,并增加了 name
,lastModifiedDate
等文件相关属性,但依然和 Blob 一样并不能读写已经放入的数据。
ArrayBuffer 和 TypedArray、DataView 对象
ArrayBuffer
要对二进制数据读写,需要使用缓冲数组:ArrayBuffer ,这个对象实际上对应的就是传统语言中的数组,即在创建申请所需长度的内存,数组内容存放在连续的内存中,并且长度不能动态增减。
在 JavaScript 中 ArrayBuffer 本身只有创建指定长度 ArrayBuffer 的功能,要对 ArrayBuffer 进行修改需要使用数据视图对象 TypedArray 或者 DataView。
var buf = new ArrayBuffer(32); // 创建 32 字节长度的 ArrayBuffer
DataView
数据视图要解决的问题是按何种数据类型去处理数据,举例来说就是一个字节(8位)是把他当成取值范围 0~255 的无符号整数(Uint8) 还是取值范围 -128~127 的带符号整数(Int8)处理。 JavaScript 支持以下数据类型:
数据类型 | 字节长度 | 含义 | 对应的传统语言类型 |
---|---|---|---|
Int8 | 1 | 8 位带符号整数 | signed char |
Uint8 | 1 | 8 位无符号整数 | unsigned char |
Uint8C | 1 | 8 位无符号整数(自动过滤溢出) | unsigned char |
Int16 | 2 | 16 位带符号整数 | short |
Uint16 | 2 | 16 位无符号整数 | unsigned short |
Int32 | 4 | 32 位带符号整数 | int |
Uint32 | 4 | 32 位无符号的整数 | unsigned int |
Float32 | 4 | 32 位浮点数 | float |
Float64 | 8 | 64 位浮点数 | double |
一个 DataView 只是一个视图,并不存储数据,他指向一个 ArrayBuffer ,我们使用 DataView 的各种方法按想要的数据类型去读写他指向的 ArrayBuffer :
var buf = new ArrayBuffer(32); // 创建 32 字节长度的 ArrayBuffer
var dataView = new DataView(buf);// 以 buf 为数据创建视图 dataView
dataView.setUint8(3,0xff); //把 dataView 对应数据按 Uint8(无符号 8 位整数 0~255)处理,在第 4 位 Uint8 中写入 0xff
dataView.getUint8(3); // 按 Uint8 读取第 4 位。 返回 255(即 0xff)
DataView 有 3 个属性:
-
buffer
:指向的 ArrayBuffer; -
byteOffset
:数据偏移值 -
byteLength
:数据长度
表示的是 DataView 可以操作 buffer
从 byteOffset
开始 byteLength
长度的数据,其可以在创建对象时指定(也可修改):
var buf = new ArrayBuffer(8);// 创建 8字节长度的 ArrayBuffer
var dataView = new DataView(buf, 3, 2);// 以 buf 的第 4 位开始取 2 位长度的数据创建视图 dataView
默认 DataView 会以 ArrayBuffer 的所有数据创建视图。
DataView 读写数据的 .getXXX ,和 setXXX 方法都是从指定字节位来读取数据的,这意味着如果你要依次读取 Uint16 类型(一个 Uint16 占 2 个字节)的数据,你要使用的是 .getUint16(0)
, .getUint16(2)
, .getUint16(4)
, .getUint16(6)
,需要你手动处理数据类型长度,这有灵活操作的好处,但通常这样只会增加让程序员手动处理数据类型长度的麻烦。所以如果我们不是为了更灵活自由的处理数据类型,或者指定字节序的话更常用 TypedArray (类型化数组)。
TypedArray
TypedArray 类型化数组,和 DataView 实际上的功能是一样,都是为数据操作指定数据类型,不同的是 DataView 是拿到数据后进行可以按各种数据类型进行操作,而 TypedArray 是先按指定的数据类型拿到数据,以后都按这种数据类型进行操作。这样操作带来的好处就是无需程序员手动处理数据类型长度,并且减少每次操作都指定数据类型的麻烦,并且 TypedArray 虽然也是对 ArrayBuffer 的封装,但 TypedArray 可以和数组一样用下标操的方式操作,编写代码和调试都更加方便。
TypedArray 实际上有一组对象,名字为 XXXArray 的形式,XXX 在前面 DataView 那张数据类型表上有。分别是:Int8Array
、Uint8Array
、Uint8ClampedArray
、Int16Array
、Uint16Array
、Int32Arra
、Uint32Array
、Float32Array
、Float64Array
。
var buf = new ArrayBuffer(8); // 创建 8 字节长度的 ArrayBuffer
var u8 = new Uint8Array(buf);// 以创建 buf 创建 TypedArray
//同 DataView 一样 TypedArray 也可以只截取 ArrayBuffer 的一部分:
var u8_2 = new Uint8Array(buf,4,2);// 以创建 buf 的第 5 位开始取 2 字节长度创建 TypedArray
//TypedArray 可以直接指定要创建长度,省去手动创建 ArrayBuffer 的过程
var f64a = new Float64Array(8);
//TypedArray 还可以直接输入数组
var x = new Uint8Array([1, 1,4,222]);
x // [1, 1,4,222]
//TypedArray 还可以根据另一个 TypedArray 克隆
var x = new Uint8Array([1, 1,4,222]);
var y = new Uint8Array(x);
x // [1, 1,4,222]
y // [1, 1,4,222]
//TypedArray 还可以与另一个 TypedArray 共用一个 ArrayBuffer
var x = new Uint8Array([1, 1,4,222]);
var y = new Uint8Array(x.buffer); // x.buffer 获取 x 指向的 ArrayBuffer
x // [1, 1,4,222]
y // [1, 1,4,222]
y[0] // 1;
x[0] = 2;
y[0] // 2;
TypedArray 使用数组的操作方式读写指向的 ArrayBuffer ,普通数组的操作方法和属性,对TypedArray数组完全适用。
TypedArray 的 .buffer
属性表示指向的 ArrayBuffer 。
TypedArray 有 length
属性,和 byteLength
2个长度属性,其中 length
表示 TypedArray 数组成员个数,也就是有多少个,而 byteLength
是这些成员共占多少字节长度。
Buffer
与上面说的 JavaScript 原生的二进制数据操纵方法不同,Buffer 是 Node.js 提供的对象,Buffer 与 TypedArray 类似,都是以数组的操作方式来处理二进制数据,只是它不依赖于 ArrayBuffer ,但是 Buffer 也并不是直接存储数据,它也是指向一块数据,所以多个 Buffer 对象的实例也可以共用一块数据(使用 .slice())。
相较于 TypedArray ,Buffer 在创建时会有更好的性能表现,因为 ArrayBuffer 创建时会把申请的内存全白写 0 ,而 Buffer 创建时没有这步操作(所以新创建的 Buffer 里会有随机数据)。
var buf = new Buffer(16); //创建 64 个字节长度的 Buffer
// Buffer 也能从普通数组创建
var x = new Buffer([1,2,3,4]);
// 由于 TypedArray 也有数组的特征,所以可以直接把 TypedArray 转化成 Buffer ,
var v = new Buffer(new Uint8Array([1, 1,4,222]));
v // {0: 1, 1: 1, 2: 4, 3: 222}
// 反过来 Buffer 也能这样转成 TypedArray
var g = new Uint8Array(v);
g //[1, 1,4,222]
// Buffer 能根据另一个 Buffer 克隆
var y = new Buffer(x);
y // [1,2,3]
// 所谓克隆自然是深拷贝,与原 Buffer 已经毫无关系了
y[0] = 222;
x[0] // 1
//而使用 .slice() 得到的是指向原 Buffer 指针
z = x.slice(0) //截取 x 从 0 位置到结尾,并赋值给 z ,这截取的只是指针,也就是 x 和 z 共用一段数据
z //{0: 1, 1: 2, 2: 3,}
z[0] = 222; // z 改变 x 也会同时改变
x[0] // 222
Buffer 也有 readInt8
、readUInt16LE
、writeFloatBE
这样指定数据类型和字节序的方法,使用起来和 DataView 差不多,不在繁述。