首发于 For Codes

使用psd.js将PSD转成SVG -- 基础篇(文字&图片)

背景

随着发展,活动会场页面的题图运营需要线上模板化,而自研的导购素材制作平台接入了 海棠-创意中心 ,通过平台能力,将素材模板化,并且通过配置化的方式生成多种场景化,个性化的素材。但是创意中心的素材模板是基于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 | 图层图像信息 |



获取图层的具体信息

通过 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导致的问题吧。



如上图,右边的文字明显比左边的大,但是它的字体大小值只有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;