背景
随着发展,活动会场页面的题图运营需要线上模板化,而自研的导购素材制作平台接入了海棠-创意中心,通过平台能力,将素材模板化,并且通过配置化的方式生成多种场景化,个性化的素材。但是创意中心的素材模板是基于SVG的,而会场页面的题图基本是基于Photoshop(PS)输出,源文件是PSD。由于SVG是面向矢量图形的标记语言,而PS是以位图处理为中心的图像处理软件,大多时候,PS无法直接导出SVG文件。
为了能让会场页面的题图模板接入到导购素材制作平台,同时降低设计师的使用门槛,我们需要在导购素材制作平台中实现直接将PSD转成SVG的功能,在线化的将PSD转成SVG,然后导入到创意中心,将题图模板化。
使用psd.js解析PSD
PSD文档是一个二进制文件,官网有相关的格式说明文档。目前,在前端解析PSD文档比较成熟的方案是使用 psd.js ,使用方法和API可以参见这里。
PSD的JSON结构
可以通过如下的方式导出PSD文档的JSON结构。
const PSD = require('psd');
const psd = PSD.fromFile(file); // file为psd文档的存储路径
psd.parse();
console.log(psd.tree().export());
JSON结构大概如下:
{ children:
[ { type: 'group',
visible: false,
opacity: 1,
blendingMode: 'normal',
name: 'Version D',
left: 0,
right: 900,
top: 0,
bottom: 600,
height: 600,
width: 900,
children:
[ { type: 'layer',
visible: true,
opacity: 1,
blendingMode: 'normal',
name: 'Make a change and save.',
left: 275,
right: 636,
top: 435,
bottom: 466,
height: 31,
width: 361,
mask: {},
text:
{ value: 'Make a change and save.',
font:
{ name: 'HelveticaNeue-Light',
sizes: [ 33 ],
colors: [ [ 85, 96, 110, 255 ] ],
alignment: [ 'center' ] },
left: 0,
top: 0,
right: 0,
bottom: 0,
transform: { xx: 1, xy: 0, yx: 0, yy: 1, tx: 456, ty: 459 } },
image: {} } ] } ],
document:
{ width: 900,
height: 600,
resources:
{ layerComps:
[ { id: 692243163, name: 'Version A', capturedInfo: 1 },
{ id: 725235304, name: 'Version B', capturedInfo: 1 },
{ id: 730932877, name: 'Version C', capturedInfo: 1 } ],
guides: [],
slices: [] } } }
最外层的children
描述的是图层信息,是一个数组类型的对象,document
描述的是PSD文档全局信息,除了width
和height
其他不必过多关注,我们需要重点关注的是children
下的图层信息。
psd.js也支持导出单个图层的JSON。
// 访问图层节点
const node = psd.tree().childrenAtPath('Version A/Matte')[0];
// or
const node =psd.tree().childrenAtPath(['Version A', 'Matte'])[0];
// 获取图层信息
node.export();
JSON结构字段说明
图层信息都记录在children
数组中,数组的item包含一些公共字段,比如type
、name
、visible
、top
、bottom
、left
、right
等,同时,也根据不同的图层类型,存在一些特殊字段,比如文字图层,会用text
字段记录文字相关的信息,比如字体、大小、颜色、对齐方式等。下面简单介绍一些重要的字段。
| 字段 | 说明 | | ---- | ---- | | type | 图层类型,group表示分组,layer表示普通图层 | | name | 图层名称 | | visible | 是否可见 | | opacity | 透明层,0~1 | | blendingMode | 图层模式 | | width/height | 图层内容的宽和高 | | top/bottom/left/top | 图层内容相对文档的范围区域 | | mask | 蒙层路径信息 | | image | 图层图像信息 |
![8ca2ce38960d564a6e72fa788d91c0fc.png](https://i-blog.csdnimg.cn/blog_migrate/9aee5be6b8f0a6e7d6f0612701584217.jpeg)
获取图层的具体信息
通过export
方法导出的JSON数据并非图层的所有信息,如果要获取一些具体信息,比如路径节点、渐变、描边等,可以通过get
方法获取。
const node = psd.tree().childrenAtPath(['Version A', 'Matte'])[0];
// 获取图层的路径
const vectorMask = node.get('vectorMask');
vectorMask.parse();
const data = vectorMask.export();
const { paths = [] } = data;
paths.forEach(path => {
// 变量路径节点
});
get
方法的参数对应着需要获取的图像信息的类型,支持的类型可以参照这里
由于SVG自身特性的限制,我们不需要用到所有PSD图层的所有信息,只需要关注如下几个主要的即可。
| 信息类型 | 说明 | | ---- | ---- | | solidColor | 填充色 | | gradientFill | 渐变色 | | typeTool | 文字 | | vectorMask | 路径 | | vectorStroke | 描边 |
生成SVG文档根标签
SVG文档,最外层的标签是svg
,记录文档的编码、命名空间、视口(viewBox)大小等。svg
标签的内容可以通过psd.js导出的document信息生成
const generateSvg = function(doc, content) {
const { width, height } = doc;
return (
`<?xml version="1.0" encoding="UTF-8"?>
<!-- generated by lst -->
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 ${this.width} ${this.height}"
enable-background="new 0 0 ${this.width} ${this.height}"
xml:space="preserve"
>
${content}
</svg>`);
}
处理图像图层
最基本的,将PSD图层已image的形式转成SVG。SVG通过image
标签显示图片,支持内联和外链两种方式。
<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- link -->
<image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" height="200" width="200"/>
<!-- base64 -->
<image xlink:href="data:image/png;base64,......" height="200" width="200"/>
</svg>
使用psd.js将图层转成png
const node = psd.tree().childrenAtPath(['图层1'])[0];
const pngData = node.layer.image.toPng();
如何通过SVG显示图片知道了,如何用psd.js将PSD图层转成图片也知道,那么接下来要做的就是将psd.js转换的图片数据嵌到SVG文档中。
基于Base64编码内联
Base64编码内联的本质就是将Base64编码的内容以字符串的形式嵌到image
标签的href
属性中,所以,关键的步骤就是如何将图片的数据转换成Base64编码的字符串。
const toBase64 = function(image) {
return new Promise((resolve, reject) => {
const chunks = [];
image.pack(); // [1]
image.on('data', (chunk) => {
chunks.push(chunk); // [2]
});
image.on('end', () => {
resolve(`data:image/png;base64,${Buffer.concat(chunks).toString('base64')}`); // [3]
});
image.on('error', (err) => {
reject(err);
});
});
}
const embedToImage = function(href, width, height) {
return `<image xlink:href="${href}" width="${width}" height="${height}"/>
`
}
const node = psd.tree().childrenAtPath(['图层1'])[0];
const pngData = node.layer.image.toPng();
toBase64(pngData).then(content => {
const image = embedToImage(content, node.get('width'), node.get('height'));
// ...
});
关键步骤:
- [1] 通过
pack
方法将图片数据转成stream对象 - [2] 基于stream的
data
事件,获取流数据 - [3] 通过
Buffer
将流数据转换成Base64字符串
基于CDN地址外链
内联的方式有个好处,就是图片的数据可以打包到SVG文档中,不依赖外部环境,但也有个弊端,就是会使SVG的体积变大。我们可以借助CDN,将图片先上传到CDN上,拿到CDN地址后,通过外链的方式将图片嵌到SVG文档中。 对于集团,我们可以借助TPS平台,将图片上传到集团的CDN。
const Tps = require('@ali/tps-node');
const path = require('path');
const fs = require('fs');
const toLink = function(image) {
return new Promise((resolve, reject) => {
const name = `tmp_png_${new Date().getTime()}.png`;
const fileName = path.resolve('temp', name);
image.pack()
.pipe(fs.createWriteStream(fileName)) // [1]
.on('finish', () => {
resolve(fileName);
}).on('error', (err) => {
reject(err);
});
}).then(fileName => {
// [2]
const tps = new Tps({
accesstoken: 'xxxxxxxxxx'
});
return tps.upload(fileName, {
empId: 123456,
nick: '花名',
folder: 'ps-to-svg'
}).then(({ url }) => {
fs.unlinkSync(fileName);
return url;
});
}).then((url) => {
return url;
});
}
const embedToImage = function(href, width, height) {
return `<image xlink:href="${href}" width="${width}" height="${height}"/>
`
}
const node = psd.tree().childrenAtPath(['图层1'])[0];
const pngData = node.layer.image.toPng();
toLink(pngData).then(link => {
const image = embedToImage(link, node.get('width'), node.get('height'));
// ...
});
关键步骤在于:
- [1] 将文件缓存在本地
- [2] 上传到CDN,拿到地址
处理文字
另外一个会经常遇到的场景,就是处理PSD中的文字图层。SVG支持用text
和tspan
来显示文字,具体用法可以参加这里(text)和这里(tspan)
基本的处理
最基本的,通过图层JSON对象的text
字段获取到文字的内容、字体族名称、大小、颜色、对齐方式以及定位。
| 字段 | 说明 | | ---- | ---- | | value | 内容 | | font | 字体属性 | | font.name | 字体族名称 | | font.sizes | 字体大小 | | font.colors | 字体颜色 | | font.alignment | 对齐方式,center
、left
或者right
| | top/bottom/left/right | 文字的区域 | | transform | 文字的变形矩阵 |
需要注意的是,字体大小和颜色是一个数组,这是因为PS支持对一段文本中的其中一部分文字编辑字体样式,从而导致一段文本中,可能存在多个字体大小,或者字体颜色,因此字体的大小和颜色是用数组来记录的。
复杂的情况暂且放一放,这里我们先介绍比较简单的处理方式,使用SVG的text
标签显示PSD字体图层。
拿到图层的文字信息数据后,将字段的值与svg标签的属性做一个映射即可,映射关系如下
| PSD字段 | SVG标签属性 | | ---- | ---- | | value | 标签内容 | | font.name | font-family | | font.sizes[0] | font-size | | font.colors[0] | fill | | font.alignment | text-anchor | | left | x | | top + font.sizes[0] | y | | transform | transform |
因此,转换的逻辑大概如下:
const toHex = (n) => {
return parseInt(n, 10).toString(16).padStart(2, '0');
};
const toHexColor = (c = []) => {
if (typeof c === 'string') {
return c;
}
const [ r = 0, g = 0, b = 0 ] = c;
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
const embedToText = function(text) {
const { value, font, left, top } = text;
const { name, sizes, leadings, colors } = font;
return `<text x="${left}" y="${top + leadings[0]}" style="font-family: ${name}; font-size: ${sizes[0]}px" fill="${toHexColor(colors[0])}">${value}</text>`;
}
const node = psd.tree().childrenAtPath(['文字1'])[0];
const text = embedToText(node.export().text);
console.log(text);
// <text x="60" y="20" style="font-family: FZChaoCuHei-M10; font-size: 14px" fill="#DF2211">阿里巴巴零售通</text>
有几个地方我认为需要注意的:字体颜色,大小,字体族以及文字的定位。
字体颜色
通过代码我们可以看到,colors
字段取到的颜色的值,不是十六进制的RGB颜色值,取到的值是一个数组,例如
const color = text.font.colors[0];
console.log(color); // [ 85, 96, 110, 255 ]
数组由四个整数组成,分别对应着颜色通道的Red、Green、Blue、Alpha,因此需要做一个转换。在PSD中,颜色的值基本都是按颜色通道分开存储。那么Alpha怎么处理呢?可以使用svg标签的fill-opacity
属性进行处理。
计算大小
我在处理字体大小是,花了一些时间和功夫。
首先,PS的字体支持多种单位,最基础的pt
,还有px
,还有in
等等,但是通过psd.js获取到的PSD信息,没法获取字体大小的单位,因此,我们需要做一个简化,比如要求设计师统一使用px
,因此当我们获取到字体大小的值时,一律作为px
来计算。
然后,有的PSD文档,取到的图层字体大小,并非是一个整数,而是一个很小的小数,但是在PSD中,显示却是正常的大小,这是为什么呢?
不知道,可能是不同版本的PS导致的问题吧。
![a619c55b6180392993fcf215ccc829de.png](https://i-blog.csdnimg.cn/blog_migrate/528ac75a0a5be2b92f8dbc7fcc9b76d2.jpeg)
如上图,右边的文字明显比左边的大,但是它的字体大小值只有2.44,很奇怪。后来,在psd.js的issues找到了解法,参加这里。
因此,关于文字图层字体大小的计算实现如下:
const computeFontSize = function(node, defValue = 24) {
const { text } = node.export();
const typeTool = node.get('typeTool');
typeTool.parse();
const sizes = typeTool.sizes();
let size;
if (sizes && sizes[0]) {
if (text.transform.yy !== 1) {
size = Math.round((sizes[0] * text.transform.yy) * 100) * 0.01;
} else { // transform.yy为1时,sizes[0]的值就是字体显示大小的值,不需要计算
size = sizes[0];
}
} else {
size = defValue; // 默认
}
}
字体族
获取字体族也是一个让我比较头疼的问题。即使文字图层可能只用了一种字体,但不知为何,有的版本导出的PSD中,font.name记录的并非对应的字体,导致导出的效果与PSD稿不一致。经过反复的尝(cai)试(keng),最终找到了一个比较靠谱的解决方案。
const resolveFontFamily = function(node) {
const { text } = node.export();
const typeTool = node.get('typeTool');
const fontFamily = typeTool.engineData.ResourceDict.FontSet
.filter(f => !f.Synthetic)
.map(f => f.Name);
return fontFamily[0] ? fontFamily[0] : text.font.name;
}
另外一个地方,PSD文档中文本图层记录的字体族,并非我们平时看到的Font Family,而是PostScript Name,关于如何获取字体信息,推荐一个网站,opentype.js,上传字体后,可以看到字体的详细信息,以及符号集。
![32bacb2ee024183beb73a510b98f56df.png](https://i-blog.csdnimg.cn/blog_migrate/c0f45504d8ff478adf43c948416799fa.jpeg)
而opentype.js也是一个可靠强大的字体包解析工具库,亲测好使,墙裂推荐。
定位
关于文字图层的定位,SVG的text
标签提供两种方式。
x
和y
属性transform
属性
而psd.js导出的PSD数据中,可以通过相应的字段,计算得到。
- x = text.left;
- y = text.top + leading;
- transform = text.transform;
经(you)过(shi)实(cai)践(keng),我推荐使用transform
进行文字定位。实现如下:
const resolveTransformAttrValue = function(text) {
const { transform } = text;
return `matrix(1 0 0 1 ${transform.tx} ${transform.ty})`;
}